vue 使用 VS Code 断点调试

直接在 Chrome 的调试窗口中调试 Vue 代码有诸多不便, 好在 Visual Studio Code 中提供了 Debugger for Chrome 插件,能够通过配置直接在 VS Code 断点调试代码, 并且在 VS Code 的调试窗口看到 Chrome 中 console 相同的值,这篇文章就来介绍一下这个配置过程。
1.开启 Chrome 远程调试端口

首先我们需要在远程调试打开的状态下启动 Chrome, 这样 VS Code 才能 attach 到 Chrome 上。
Windows

右键点击 Chrome 的快捷方式图标,选择属性
在目标一栏,最后加上 --remote-debugging-port=9222,注意要用空格隔开

macOS

打开控制台

执行命令 /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222

Linux

打开控制台
执行命令 google-chrome --remote-debugging-port=9222

2.安装 Chrome Debug 插件

点击 Visual Studio Code 左侧边栏的扩展按钮, 然后在搜索框输入Debugger for Chrome 并安装插件,再输入,安装完成后点击 reload 重启。

3.创建 Debug 配置文件

点击 Visual Studio Code 左侧边栏的 调试 按钮, 在弹出的调试配置窗口中点击 设置 小齿轮, 然后选择 chrome, VS Code 将会在工作区根目录生成.vscode 目录,里面会有一个 lanch.json 文件并会自动打开

用下面的配置文件覆盖自动生成的 lanch.json 文件内容。

注意:URL中的端口号要跟WEBPACK配置的启动端口号一致。

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "attach",
      "name": "Attach to Chrome",
      "port": 9222,
      "webRoot": "${workspaceRoot}/src",
      "url": "http://localhost:8080/#/",
      "sourceMaps": true,
      "sourceMapPathOverrides": {
        "webpack:///src/*": "${webRoot}/*"
      }
    }
  ]
}

4.修改 webpack 配置

如果是基于 webpack 打包的 vue 项目, 可能会存在断点不匹配的问题, 还需要做些修改:

1.打开根目录下的 config 目录下的 index.js 文件

2.将dev 节点下的 devtool 值改为 ‘eval-source-map’

3.将dev节点下的 cacheBusting 值改为 false

5.开启调试

上述配置完成之后:

  1. 通过第一步的方式以远程调试打开的方式打开 Chrome

  2. 在 vue 项目中执行 npm run dev 以调试方式启动项目

  3. 点击 VS Code 左侧边栏的调试按钮,选择 Attach to Chrome 并点击绿色开始按钮,正常情况下就会出现调试控制条。

现在就可以在 vue 文件的 js 代码中打断点进行调试了。

vue 事件中的 (native)

.native是什么

.native - 监听组件根元素的原生事件。
主要是给自定义的组件添加原生事件。
# * 给普通的标签加事件,然后加native是无效的

 <el-dropdown class="submitBtn">
     <el-button type="primary" size="mini"> 更多菜单<i class="el-icon-arrow-down el-icon--right"></i>
     </el-button>
         <el-dropdown-menu slot="dropdown">
             <el-dropdown-item @click.native="click"> 双皮奶 </el-dropdown-item>
         </el-dropdown-menu>
 </el-dropdown>

将click的native去掉,思路如下

 子组件监听父组件给的click事件,
 子组件内部处理click事件然后向外发送click事件:$emit("click".fn)

<template>
  <!-- 此处自定义事件名也叫 `click` 所以在使用组件时加不加 `.native` 修饰符都可以 -->
  <button type="button" @click="$emit('click')"><slot /></button>
</template>

async await

1. 概述

async 函数是什么?一句话,它就是 Generator 函数的语法糖。
async-await 是promise和generator的语法糖。只是为了让我们书写代码时更加流畅,当然也增强了代码的可读性。简单来说:async-await 是建立在 promise机制之上的,并不能取代其地位。
async 用来表示函数是异步的,定义的函数会返回一个promise对象,可以使用then方法添加回调函数。
await 可以理解为是 async wait 的简写。await 必须出现在 async 函数内部,不能单独使用。
await 后面可以跟任何的JS 表达式。虽然说 await 可以等很多类型的东西,但是它最主要的意图是用来等待 Promise 对象的状态被 resolved。如果await的是 promise对象会造成异步函数停止执行并且等待 promise 的解决,如果等的是正常的表达式则立即执行。

2. 基本用法

2.1 返回 promise 对象

async 函数返回的是一个 promise 对象.

async function test (){
  return 'hello async';
}
let result = test();
console.log(result);
// Promise {<resolved>: "hello async"}

所以,async 函数返回的是一个 Promise 对象。从文档中也可以得到这个信息。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。所以 可以使用 then() 链来处理这个 Promise 对象,就像这样:

test().then(v => {
  console.log(v); // hello async
});

2.2 Promise 对象的状态变化

async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。也就是说,只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数。

2.3 await 命令

正常情况下,await 命令后面是一个 Promise 对象。如果不是,会被转成一个立即 resolve 的 Promise 对象。

async function f() {
  return await 123;
}

f().then(v => console.log(v))
// 123

上面代码中,await 命令的参数是数值123,它被转成 Promise 对象,并立即 resolve。

await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到。

async function f() {
  await Promise.reject('出错了');
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了

注意,上面代码中,await 语句前面没有 return,但是 reject 方法的参数依然传入了 catch 方法的回调函数。这里如果在 await 前面加上 return,效果是一样的。

只要一个 await 语句后面的 Promise 变为 reject,那么整个 async 函数都会中断执行。

async function f() {
  await Promise.reject('出错了');
  await Promise.resolve('hello world'); // 不会执行
}

上面代码中,第二个 await 语句是不会执行的,因为第一个 await 语句状态变成了 reject。

有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个 await 放在 try…catch 结构里面,这样不管这个异步操作是否成功,第二个 await 都会执行。

async function f() {
  try {
    await Promise.reject('出错了');
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// hello world

async await

Syntax Error: await is a reserved word

# 报错例:
async submit() {
    try {
        this.$refs[formName].validate(valid => {
            let res = await submit(...this.submitForm)
            this.$message.success(res.resultMessage)
        })
    } catch (err){
        console.log(err)
        this.$message.success(err.resultMessage)
    }
}

上例报错: Syntax Error: await is a reserved word
检查代码 分析: async 在submit()函数前面,里面 validate() 还有一个隐式函数 valid => {},async 应该写在隐式函数前面

#修改后例:
   submit() {
       try {
           this.$refs[formName].validate(async valid => {
               let res = await submit(...this.params)
               this.$message.success(res.resultMessage)
           })
       } catch (err){
           console.log(err)
           this.$message.success(err.resultMessage)
       }
   }

如果有多个 await 命令,可以统一放在 try…catch 结构中。

async main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);
    console.log('Final: ', val3);
  }
  catch (err) {
    console.error(err);
  }
}

await in for 循环

let arr = [1, 2, 3, 4, 5];
arr.forEach(async item => {
  await console.log(item)
});

node-vue-webpack 构建项目

vuw-cli 全局安装

npm install  -g vue-cli

构建项目

# 创建项目
mkdir vue-webpack
cd vue-webpack
# 创建模板
vue init webpack 
# 安装依赖包
npm install
# 运行项目
npm run dev
# 访问

http://localhost:8080

# 简单的Vue Webpack项目构建完成

image

安装cnpm

npm install -g cnpm --registry=https://registry.npm.taobao.org

跨域代理配置

#config目录下 index.js 文件配置如下, 可以配置多个

module.exports = {
  dev: {
    // Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {
      '/api': {
        target: 'http://10.43.22.175:9000',
        changeOrigin: true,
        ws: true,
        pathRewrite: {
          '^/api': '/api'
        },
        router: {
          // when request.headers.host == 'dev.localhost:3000',
          // override target 'http://www.example.org' to 'http://localhost:8000'
          '10.43.22.175:8080' : 'http://10.43.22.175:9000'
        }
      },
      '/activity': {
        target: 'http://10.43.22.149:8081',
        changeOrigin: true,
        ws: true,
        pathRewrite: {
          '^/activity': '/activity'
        },
        router: {
          // when request.headers.host == 'dev.localhost:3000',
          // override target 'http://www.example.org' to 'http://localhost:8000'
          '10.43.22.175:8080' : 'http://10.43.22.149:8081'
        }
      }
    }
  }
}

vue-router 参数

1. query

1.this.$router.push({path:'history', query:{id: '1111'}});
下一个页面通过this.$route.query.id访问路由参数
 总结:1. 强制刷新,query不会丢参
 2. query会拼接到url上 (/history?id=1111)

2. params

2.this.$router.push({'name': 'history', params: {id: '1111'}});
下一个页面通过this.$route.params.id访问路由参数
 总结:
    1. 页面刷新参数会丢失
    2. 不会拼接到url上 例 (/history)
    3.   router.js 中: 需要添加name属性
    {
      path: 'history',
      name: 'history',
      meta: {
        requireAuth: true,
        title: '历史记录'
      },
      component: resolve => require(['@/views/history.vue'], resolve)
    }
    4.注意: 用path访问 参数 undefined
        例:
        this.$router.push({'path': 'history', params: {id: '1111'}});
        # 下一个页面通过this.$route.params.id访问路由参数
        undefined

3.其他 :id

# 跳转
this.$router.push({'path': 'history/' + row.id});
or 
<router-link :to='`/history/${item.id}`'></router-link>

# router.js 中:
{
  path: 'history/:id',
  meta: {
    requireAuth: true,
    title: '历史记录'
  },
  component: resolve => require(['@/views/history.vue'], resolve)
},
下一个页面通过this.$route.params.id访问路由参数
1. 页面刷新参数不会丢失
2. 会拼接到url上 例 (/history/1111111) 

未完待续

编译不同环境 global 参数设置

获取 ‘npm run build dev’ 中的参数,修改 global

# 在 build.js 中添加如下修改

const yargs = require('yargs')
const fs = require('fs')
let envList = ['prod', 'test', 'dev', 'local', 'stage'];
let globConfig = (yargs.argv.config && envList.includes(yargs.argv.config)) ? yargs.argv.config : (yargs.argv._[0]? yargs.argv._[0]: envList[2]);
let versionConfig = yargs.argv.vers ? yargs.argv.vers : '1.0.0.0';
console.log('building for', globConfig,)
console.log(yargs.argv, yargs.argv._[0])

let glogFile = fs.readFileSync(path.resolve(__dirname, '../src/assets/js/global.js'), 'utf-8')
let curFile = glogFile.replace(/(let\s*env\s*\=\s*)(\d+)(\s*\;)/gi, (s0, s1, s2, s3) => {
  let config = 4
  // (globConfig === 'test') ? 2 : ((globConfig === 'prod') ? 1 : ((globConfig === 'dev') ? 3: globConfig === 'stage' ? 5 : 4))
  switch(globConfig) {
    case 'test':
      config = 2
      break;
    case 'prod':
      config = 1
      break;
    case 'dev':
      config = 3
      break;
    case 'stage':
      config = 5
      break;
    default:
      config = 4
  }
  return s1 + config + s3;
}).replace(/(let\s*version\s*\=\s*\")(.*)(\"\;)/gi, (s0, s1, s2, s3) => {
  return s1 + versionConfig + s3;
})

fs.writeFileSync(path.resolve(__dirname, '../src/assets/js/global.js'), curFile, 'utf-8');

vue + webpack 多模块静态文件导入

多个模块导入静态文件

项目结构目录
Image text

官方推荐的css及js引用方式如下:

<!-- 引入样式 -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.2.0/theme-chalk/index.css" rel="stylesheet"> 
<!-- 引入组件库 -->
<script src="https://lib.baomitu.com/vue/2.4.4/vue.min.js"></script> 
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.2/vue-router.min.js"></script>
<script src="https://unpkg.com/element-ui@2.2.0/lib/index.js"></script>

这种方式引入,如果环境只有内网,没有外网的话,会导致页面直接空白.(或cdn 访问不通、不在维护等……)

很简单把css的href及js的src的网址输入浏览器可以得到css和js的源码,直接拷贝到本地即可.

index.html 引入

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>xxxxx</title>
  <link href="/static/css/element-ui2.2.0/index.css" rel="stylesheet">
  <script src='/static/js/vue2.7.0/vue.min.js'></script>
  <script src='/static/js/vue-router3.0.2/vue-router.min.js'></script>
  <script src='/static/js/element-ui@2.2.0/index.js'></script>
</head>

静态文件配置(多模块、多服务、各服务端口不同)

# config 配置信息
assetsSubDirectory: 'static',
assetsPublicPath: '/',

# 项目是 vue+webpack 多模块
 开发环境中多个模块需要启动多个服务,各服务监听的端口不同,使用离线存储数据时存在跨域不共通的问题。
 解决思路: 采用第三方代理服务器,前端所有的请求到代理服务器由代理服务器进行请求分发
 实现:采用gulp-connect 做为代理服务器,代理转发的具体代码如下

const gulp = require('gulp');
const connect = require('gulp-connect');
const proxy = require('http-proxy-middleware');
const ipLib = require('ip');
const path = require('path');
const fs = require('fs')


let currentIp = ipLib.address()
let globPath = path.resolve(__dirname, '../src/assets/js/global.js')
const proxyPort = '7890'

gulp.task('server', function(){
  connect.server({
    port: proxyPort,
    host: '0.0.0.0',
    middleware: function(connect, opt) {
      return [
        proxy('/login', {
          target: 'http://www.localhost:8080', // target host
          changeOrigin: true,               // needed for virtual hosted sites
          ws: true,
          router: { // login
            [ currentIp + ':'+proxyPort] : 'http://' + currentIp + ':10258/'
          }
        }),
        proxy('/static', {
          target: 'http://www.localhost:8080', // target host
          changeOrigin: true,               // needed for virtual hosted sites
          ws: true,
          router: { // static
            [ currentIp + ':'+proxyPort] : 'http://' + currentIp + ':10258/'
          }
        }),
      ]
    }
  })
})

本地使用element ui 图标不显示或显示错乱的问题

但是问题来了,element-ui的index.css拷贝本地之后,会发现图标无法正常显示了.

原因

在本地的index.css中发现如下代码是网络请求icon的

@charset "UTF-8";.el-pagination--small .arrow.disabled,.el-table .hidden-columns,.el-table td.is-hidden>*,.el-table th.is-hidden>*,.el-table--hidden{visibility:hidden}@font-face{font-family:element-icons;src:url(fonts/element-icons.woff) format("woff"),url(fonts/element-icons.ttf) format("truetype");font-weight:400;

解决办法

访问 https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.2.0/theme-chalk/fonts/element-icons.woff
https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.2.0/theme-chalk/fonts/element-icons.ttf

文件自动下载下来,放到项目的/static/css/element-ui2.2.0/fonts文件夹下即可

Image text

vue 修改子组件样式

Scoped CSS

当 style 标签有 scoped 属性时,它的 CSS 只作用于当前组件中的元素。这类似于 Shadow DOM 中的样式封装。它有一些注意事项,但不需要任何 polyfill。它通过使用 PostCSS 来实现以下转换:

<style scoped>
.example {
  color: red;
}
</style>

<template>
  <div class="example">hi</div>
</template>

转换结果:

<style>
.example[data-v-f3f3eg9] {
  color: red;
}
</style>

<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>

混用本地和全局样式

你可以在一个组件中同时使用有 scoped 和非 scoped 样式:

<style>
/* 全局样式 */
</style>

<style scoped>
/* 本地样式 */
</style>

子组件的根元素

使用 scoped 后,父组件的样式将不会渗透到子组件中。不过一个子组件的根节点会同时受其父组件的 scoped CSS 和子组件的 scoped CSS 的影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。

深度作用选择器

如果你希望 scoped 样式中的一个选择器能够作用得“更深”,例如影响子组件,你可以使用 >>> 操作符:

<style scoped>
.a >>> .b { /* ... */ }
</style>

上述代码将会编译成:

.a[data-v-f3f3eg9] .b { /* ... */ }

有些像 Sass 之类的预处理器无法正确解析 >>>。这种情况下你可以使用 /deep/ 或 ::v-deep 操作符取而代之——两者都是 >>> 的别名,同样可以正常工作。

例:

 <template slot-scope="slotProps">
    <xx-input
    v-model="slotProps.option.record.name"
    :type="form.input.type"
    placeholder="请输入"
    :required="true"
    size="60"
    class="table-input"
  ></xx-input>
 </template>

<style lang="css" scoped>
.table-input /deep/ .input-no-label .input__content{
  margin-bottom: 0;
}
</style>

动态生成的内容

通过 v-html 创建的 DOM 内容不受 scoped 样式影响,但是你仍然可以通过深度作用选择器来为他们设置样式。

还有一些要留意

Scoped 样式不能代替 class考虑到浏览器渲染各种 CSS 选择器的方式,当 p { color: red } 是 scoped 时 (即与特性选择器组合使用时) 会慢很多倍。如果你使用 class 或者 id 取而代之,比如 .example { color: red },性能影响就会消除。

在递归组件中小心使用后代选择器! 对选择器 .a .b 中的 CSS 规则来说,如果匹配 .a 的元素包含一个递归子组件,则所有的子组件中的 .b 都将被这个规则匹配。

*不推荐使用全局样式

参考:https://vue-loader.vuejs.org/zh/guide/scoped-css.html

vue 的样式覆盖问题

一般想要覆盖第三方插件样式,可自己定义class,或者直接获取三方类名,添加样式,只需要在后面加 !important属性即可
.subitem-date-picker {
  width: 100% !important;
}

vue 重新渲染组件(重置或者更新)

动态组件 & 异步组件

组件根据传入的数据动态产生DOM结构, 只操作数据便可以重新渲染页面, 路由相同时页面不重新渲染或更新
一、当数据通过异步操作后,对之前加载的数据进行变更后,发现数据不生效。
二、A组件或者B组件触发数据更新,C组件数据更新了,但是C组件仍显示上一次数据。

方案一:v-if(可以重置生命周期)

当路由变更后,通过watch 监听,先去销毁当前的组件,然后再重现渲染。使用 v-if 可以解决这个问题
<template>
  <form-component :initModelForData="initModelForData" :subTitle="subTitle" :columnList="columnList" v-if="reFresh"></form-component>
</template>

<script>
  export default{
    data(){
      return {
        reFresh:true,
      }
    },
    watch:{
      $route(to, form) {
        this.reFresh= false
        this.$nextTick(()=>{
            this.reFresh = true
        })
      }
    }
}
</script>

这种方式虽然可以实现,但太不优雅

方案二 :key=’’(此处可触发watch和update)

通过vue key 实现,原理请查看官方文档。所以当key 值变更时,会自动的重新渲染。

<template>
  <form-component :initModelForData="initModelForData" :subTitle="subTitle" :columnList="columnList" :key="key"></form-component>
</template>

<script>
  export default{
    data(){
      return {
        key: 1
      }
    },
    watch:{
      $route(to, form) {
        ++this.key
      }
    }
}
</script>

java 集合框架

一、只有光头才能变强

1、 什么是Java集合API

Java集合框架API是用来表示和操作集合的统一框架,它包含接口、实现类、以及帮助程序员完成一些编程的算法。简言之,API在上层完成以下几件事:

● 编程更加省力,提高城程序速度和代码质量

● 非关联的API提高互操作性

● 节省学习使用新API成本

● 节省设计新API的时间

● 鼓励、促进软件重用

具体来说,有6个集合接口,最基本的是Collection接口,由三个接口Set、List、SortedSet继承,另外两个接口是Map、SortedMap,这两个接口不继承Collection,表示映射而不是真正的集合。

2、 什么是Iterator

一些集合类提供了内容遍历的功能,通过java.util.Iterator接口。这些接口允许遍历对象的集合。依次操作每个元素对象。当使用 Iterators时,在获得Iterator的时候包含一个集合快照。通常在遍历一个Iterator的时候不建议修改集合本省。

3、 Iterator与ListIterator有什么区别

Iterator:只能正向遍历集合,适用于获取移除元素。ListIerator:继承Iterator,可以双向列表的遍历,同样支持元素的修改。

4、 什么是HaspMap和Map

Map是接口,Java 集合框架中一部分,用于存储键值对,HashMap是用哈希算法实现Map的类。

5、 HashMap与HashTable有什么区别?对比Hashtable VS HashMap

两者都是用key-value方式获取数据。Hashtable是原始集合类之一(也称作遗留类)。HashMap作为新集合框架的一部分在Java2的1.2版本中加入。它们之间有一下区别:

● HashMap和Hashtable大致是等同的,除了非同步和空值(HashMap允许null值作为key和value,而Hashtable不可以)。

● HashMap没法保证映射的顺序一直不变,但是作为HashMap的子类LinkedHashMap,如果想要预知的顺序迭代(默认按照插入顺序),你可以很轻易的置换为HashMap,如果使用Hashtable就没那么容易了。

● HashMap不是同步的,而Hashtable是同步的。

● 迭代HashMap采用快速失败机制,而Hashtable不是,所以这是设计的考虑点。

6、 在Hashtable上下文中同步是什么意思

同步意味着在一个时间点只能有一个线程可以修改哈希表,任何线程在执行hashtable的更新操作前需要获取对象锁,其他线程等待锁的释放。

7、 什么叫做快速失败特性

从高级别层次来说快速失败是一个系统或软件对于其故障做出的响应。一个快速失败系统设计用来即时报告可能会导致失败的任何故障情况,它通常用来停止正常的操作而不是尝试继续做可能有缺陷的工作。当有问题发生时,快速失败系统即时可见地发错错误告警。在Java中,快速失败与iterators有关。如果一个iterator在集合对象上创建了,其它线程欲“结构化”的修改该集合对象,并发修改异常 (ConcurrentModificationException) 抛出。

8、 怎样使Hashmap同步

HashMap可以通过Map m = Collections.synchronizedMap(hashMap)来达到同步的效果。

9、 什么时候使用Hashtable,什么时候使用HashMap

基本的不同点是Hashtable同步HashMap不是的,所以无论什么时候有多个线程访问相同实例的可能时,就应该使用Hashtable,反之使用HashMap。非线程安全的数据结构能带来更好的性能。

如果在将来有一种可能—你需要按顺序获得键值对的方案时,HashMap是一个很好的选择,因为有HashMap的一个子类 LinkedHashMap。所以如果你想可预测的按顺序迭代(默认按插入的顺序),你可以很方便用LinkedHashMap替换HashMap。反观要是使用的Hashtable就没那么简单了。同时如果有多个线程访问HashMap,Collections.synchronizedMap()可以代替,总的来说HashMap更灵活。

10、为什么Vector类认为是废弃的或者是非官方地不推荐使用?或者说为什么我们应该一直使用ArrayList而不是Vector

你应该使用ArrayList而不是Vector是因为默认情况下你是非同步访问的,Vector同步了每个方法,你几乎从不要那样做,通常有想要同步的是整个操作序列。同步单个的操作也不安全(如果你迭代一个Vector,你还是要加锁,以避免其它线程在同一时刻改变集合).而且效率更慢。当然同样有锁的开销即使你不需要,这是个很糟糕的方法在默认情况下同步访问。你可以一直使用Collections.sychronizedList来装饰一个集合。

事实上Vector结合了“可变数组”的集合和同步每个操作的实现。这是另外一个设计上的缺陷。Vector还有些遗留的方法在枚举和元素获取的方法,这些方法不同于List接口,如果这些方法在代码中程序员更趋向于想用它。尽管枚举速度更快,但是他们不能检查如果集合在迭代的时候修改了,这样将导致问题。尽管以上诸多原因,oracle也从没宣称过要废弃Vector.

二、CopyOnWriteArrayList(Set)介绍

一般来说,我们会认为:CopyOnWriteArrayList是同步List的替代品,CopyOnWriteArraySet是同步Set的替代品。

无论是Hashtable–>ConcurrentHashMap,还是说Vector–>CopyOnWriteArrayList。JUC下支持并发的容器与老一代的线程安全类相比,总结起来就是加锁粒度的问题

Hashtable、Vector加锁的粒度大(直接在方法声明处使用synchronized)
ConcurrentHashMap、CopyOnWriteArrayList加锁粒度小(用各种的方式来实现线程安全,比如我们知道的ConcurrentHashMap用了cas锁、volatile等方式来实现线程安全..)
JUC下的线程安全容器在遍历的时候不会抛出ConcurrentModificationException异常

所以一般来说,我们都会使用JUC包下给我们提供的线程安全容器,而不是使用老一代的线程安全容器。

下面我们来看看CopyOnWriteArrayList是怎么实现的,为什么使用迭代器遍历的时候就不用额外加锁,也不会抛出ConcurrentModificationException异常。

2.1CopyOnWriteArrayList实现原理

我们还是先来回顾一下COW:

如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源。

概括一下CopyOnWriteArrayList源码注释介绍了什么:

CopyOnWriteArrayList是线程安全容器(相对于ArrayList),底层通过复制数组的方式来实现。
CopyOnWriteArrayList在遍历的使用不会抛出ConcurrentModificationException异常,并且遍历的时候就不用额外加锁
元素可以为null

2.1.1看一下CopyOnWriteArrayList基本的结构

/** 可重入锁对象 */
final transient ReentrantLock lock = new ReentrantLock();

/** CopyOnWriteArrayList底层由数组实现,volatile修饰 */
private transient volatile Object[] array;

/**
 * 得到数组
 */
final Object[] getArray() {
    return array;
}

/**
 * 设置数组
 */
final void setArray(Object[] a) {
    array = a;
}

/**
 * 初始化CopyOnWriteArrayList相当于初始化数组
 */
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

看起来挺简单的,CopyOnWriteArrayList底层就是数组,加锁就交由ReentrantLock来完成。

2.1.2常见方法的实现

根据上面的分析我们知道如果遍历Vector/SynchronizedList是需要自己手动加锁的。

CopyOnWriteArrayList使用迭代器遍历时不需要显示加锁,看看add()、clear()、remove()与get()方法的实现可能就有点眉目了。

首先我们可以看看add()方法

public boolean add(E e) {

    // 加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {

        // 得到原数组的长度和元素
        Object[] elements = getArray();
        int len = elements.length;

        // 复制出一个新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);

        // 添加时,将新元素添加到新数组中
        newElements[len] = e;

        // 将volatile Object[] array 的指向替换成新数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

通过代码我们可以知道:在添加的时候就上锁,并复制一个新数组,增加操作在新数组上完成,将array指向到新数组中,最后解锁。

再来看看size()方法:

public int size() {

    // 直接得到array数组的长度
    return getArray().length;
}

再来看看get()方法:

public E get(int index) {
    return get(getArray(), index);
}

final Object[] getArray() {
    return array;
}

那再来看看set()方法

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {

        // 得到原数组的旧值
        Object[] elements = getArray();
        E oldValue = get(elements, index);

        // 判断新值和旧值是否相等
        if (oldValue != element) {

            // 复制新数组,新值在新数组中完成
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;

            // 将array引用指向新数组
            setArray(newElements);
        } else {
            // Not quite a no-op; enssures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

对于remove()、clear()跟set()和add()是类似的,这里我就不再贴出代码了。

总结:

在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向。
写加锁,读不加锁

2.1.3剖析为什么遍历时不用调用者显式加锁

常用的方法实现我们已经基本了解了,但还是不知道为啥能够在容器遍历的时候对其进行修改而不抛出异常。所以,来看一下他的迭代器吧:

// 1. 返回的迭代器是COWIterator
public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}


// 2. 迭代器的成员属性
private final Object[] snapshot;
private int cursor;

// 3. 迭代器的构造方法
private COWIterator(Object[] elements, int initialCursor) {
    cursor = initialCursor;
    snapshot = elements;
}

// 4. 迭代器的方法...
public E next() {
    if (! hasNext())
        throw new NoSuchElementException();
    return (E) snapshot[cursor++];
}

//.... 可以发现的是,迭代器所有的操作都基于snapshot数组,而snapshot是传递进来的array数组

到这里,我们应该就可以想明白了!CopyOnWriteArrayList在使用迭代器遍历的时候,操作的都是原数组!

2.1.4CopyOnWriteArrayList缺点

看了上面的实现源码,我们应该也大概能分析出CopyOnWriteArrayList的缺点了。

内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据,经常要执行add()、set()、remove()的话,那是比较耗费内存的。
    因为我们知道每次add()、set()、remove()这些增删改操作都要复制一个数组出来。

数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
    从上面的例子也可以看出来,比如线程A在迭代CopyOnWriteArrayList容器的数据。线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了(已经调用setArray()了)。但是线程A迭代出来的是原有的数据。

2.1.5CopyOnWriteSet

CopyOnWriteArraySet的原理就是CopyOnWriteArrayList。

private final CopyOnWriteArrayList<E> al;

public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();
}

Nginx解决方案

1.方案场景

现有多台应用服务器,要实现所有的访问先访问到反向代理服务器上再转内部对应的应用。

反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。简而言之就是隐藏所有的服务器,与之相对的是正向代理,如我们常用的vpn,目的是隐藏访问客户端。

我们在这里打算采用Nginx作为方向代理的服务器

2.什么是nginx

Nginx("engine x")是一款是由俄罗斯的程序设计师Igor Sysoev开发高性能的 Web和 反向代理 服务器,也是一个 IMAP/POP3/SMTP 代理服务器。
在高连接并发的情况下,Nginx是Apache服务器不错的替代品。

3.nginx安装

在/usr/local/src下进行,否则后面会编译错误

3.1 安装编译工具和库文件

yum -y install make zlib zlib-devel gcc-c++ libtool  openssl openssl-devel

3.2 安装PCRE

PCRE 作用是让 Nginx 支持 Rewrite 功能。
下载 PCRE 安装包,下载地址: http://downloads.sourceforge.net/project/pcre/pcre/8.35/pcre-8.35.tar.gz

wget http://downloads.sourceforge.net/project/pcre/pcre/8.35/pcre-8.35.tar.gz

解压安装包

tar zxvf pcre-8.35.tar.gz

进入安装包目录

cd pcre-8.35

编译安装

./configure
make && make install

查看pcre版本

pcre-config --version

若有版本显示,表明安装成功

3.3 安装Nginx

下载 Nginx,下载地址:http://nginx.org/download/nginx-1.6.2.tar.gz

wget http://nginx.org/download/nginx-1.6.2.tar.gz

解压安装包

tar zxvf nginx-1.6.2.tar.gz

进入安装包目录

cd nginx-1.6.2

编译安装

./configure --prefix=/usr/local/webserver/nginx --with-http_stub_status_module --with-http_ssl_module --with-pcre=/usr/local/src/pcre-8.35
make
make isntall

注意,编译安装的路径要按照实际安装的路径来,本次是在/usr/local/src下

查看nginx版本

/usr/local/webserver/nginx/sbin/nginx -v

若有版本显示,表明安装成功

4.nginx的配置

nginx的配置文件在/usr/local/webserver/nginx/conf/nginx.conf下,我们只要修改nginx.conf的内容就可以修改Nginx的配置
配置文件的格式如下

main # 全局设置
events { # Nginx工作模式
    ....
}
http { # http设置
    ....
    upstream myproject { # 负载均衡服务器设置
        .....
    }
    server  { # 主机设置
        ....
        location { # URL匹配
            ....
        }
    }
    server  {
        ....
        location {
            ....
        }
    }
    ....
}

4.1 main模块

main区域是一个全局设置

#user 来指定Nginx Worker进程运行用户以及用户组,默认由nobody账号运行。
user  nobody;
#指定了Nginx要开启的子进程数。每个Nginx进程平均耗费10M~12M内存。根据经验,一般指定1个进程就足够了,如果是多核CPU,建议指定和CPU的数量一样的进程数即可。我这里写2,那么就会开启2个子进程,总共3个进程。
worker_processes  1;

#error_log 来定义全局错误日志文件。日志输出级别有debug、info、notice、warn、error、crit可供选择,其中,debug输出日志最为最详细,而crit输出日志最少。
error_log  logs/error.log;
error_log  logs/error.log  notice;
error_log  logs/error.log  info;
#pid 来指定进程id的存储文件位置。
pid        logs/nginx.pid

4.2 events模块

events模块来用指定nginx的工作模式和工作模式及连接数上限,一般是这样

events {
    use kqueue; #用来指定Nginx的工作模式
    #指定Nginx的单个进程的最大连接数,即接收到的前端的最大请求数,默认为1024
    worker_connections  1024;
}

4.3 http模块

http是Nginx的核心模块,它负责http服务器相关属性的配置,里面的server和upstream子模块至关重要,我们在设置方向代理、负债均衡以及虚拟目录等的时候,就是依赖于这两个模块的配置

http {
     #设定mime类型,类型由mime.type文件定义
    include       /etc/nginx/mime.types;
    #设定了默认的类型为二进制流,也就是当文件类型未定义时使用这种方式
    default_type  application/octet-stream;

    #用于设置日志的格式,和记录哪些参数,这里设置为main,刚好用于access_log来纪录这种类型。
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
    #设定日志文件,后面的main是日志的格式样式,对应于log_format的main。
    access_log    /var/log/nginx/access.log main;

    #用于开启高效文件传输模式。将tcp_nopush和tcp_nodelay两个指令设置为on用于防止网络阻塞。
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    #设置客户端连接保持活动的超时时间。在超过这个时间之后,服务器会关闭该连接。
    keepalive_timeout 10;

    #设定负载均衡的服务器列表
    upstream load_balance_server {
        ......
    }

   #HTTP服务器
   server {
       ......
    }
}
4.3.1 upstream模块

upstream主要负责负载均衡,通过一个简单的调度算法来实现客户端IP到后端服务器的负载均衡。

upstream test.com{
    ip_hash;
    server 123.206.117.62:80;
    server 123.206.117.62:80 down;
    server 123.206.117.62:8080  max_fails=3  fail_timeout=20s;
    server 123.206.117.62:8080;
}
  • 上面的代码,test.com是upstream指定的负载均衡器的名称,这个名称可以任意指定,在后面需要的地方直接调用即可。
  • ip_hash这是其中的一种负载均衡调度算法,Nginx的负载均衡模块目前支持4种调度算法
    • weight 轮询(默认)。每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某台服务器宕机,故障系统被自动剔除,使用户访问不受影响。weight。指定轮询权值,weight值越大,分配到的访问机率越高,主要用于后端每个服务器性能不均的情况下。
    • ip_hash。每个请求按访问IP的hash结果分配,这样来自同一个IP的访客固定访问一个后端服务器,有效解决了动态网页存在的session共享问题。
    • fair(第三方)。比上面两个更加智能的负载均衡算法。此种算法可以依据页面大小和加载时间长短智能地进行负载均衡,也就是根据后端服务器的响应时间来分配请求,响应时间短的优先分配。Nginx本身是不支持fair的,如果需要使用这种调度算法,必须下载Nginx的upstream_fair模块。
    • url_hash(第三方)。按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。Nginx本身是不支持url_hash的,如果需要使用这种调度算法,必须安装Nginx的hash软件包。
  • server指定的是各种服务器,包括服务器的ip、端口以及每个后端服务器在负载均衡中的状态,常用的状态有:
    1. down,表示当前的server暂时不参与负载均衡。
    2. backup,预留的备份机器。当其他所有的非backup机器出现故障或者忙的时候,才会请求backup机器,因此这台机器的压力最轻。
    3. max_fails,允许请求失败的次数,默认为1。当超过最大次数时,返回proxy_next_upstream 模块定义的错误。
    4. fail_timeout,在经历了max_fails次失败后,暂停服务的时间。max_fails可以和fail_timeout一起使用。
      注意:当负载调度算法为ip_hash时,后端服务器在负载均衡调度中的状态不能是weight和backup。其它情况可以是weight,表示负载的权重,默认为1,weight越大,权重越大。
4.3.2 server模块

server模块是http的子模块,它用来定一个虚拟主机,它的基本配置如下

server {
    #指定虚拟主机的服务端口
    listen 8080;
    #用来指定IP地址或者域名,多个域名之间用空格分开。
    server_name localhost 123.206.117.62 www.test.com;
    # 全局定义,如果都是这一个目录,这样定义最简单。
    root   /Users/marscheng/www; #示在这整个server虚拟主机内,全部的root web根目录。注意要和locate {}下面定义的区分开来。
    index  index.php index.html index.htm; #全局定义访问的默认首页地址。注意要和locate {}下面定义的区分开来。
    charset utf-8;#网页的默认编码格式
    #用来指定此虚拟主机的访问日志存放路径,最后的main用于指定访问日志的输出格式
    access_log  usr/local/var/log/host.access.log  main;
    #错误日志文件
    error_log  usr/local/var/log/host.error.log  error;
    #用于定位的配置
    location / {
          ....  
        }

}

location模块

location模块一般是在server中定义的,它在Nginx中用的最多,无论是负载均衡、反向代理还是虚拟域名,都跟它的配置有关。

location是用来定位,解析url,定位URL,所以,它也提供了强大的正则匹配功能,也支持条件判断匹配,用户可以通过location指令实现Nginx对动态、静态网页进行过滤处理。

  1. 设定默认首页和虚拟机目录

    location / {
      #指令用于指定访问根目录时,虚拟主机的web目录,这个目录可以是相对路径(相对路径是相对于nginx的安装目录)。也可以是绝对路径。
      root   /Users/marscheng/www;
      #用于设定我们只输入域名后访问的默认首页地址,有个先后顺序:index.php index.html index.htm,如果没有开启目录浏览权限,又找不到这些默认首页,就会报403错误。
      index  index.jsp index.html index.htm;
    }
    
  2. 用正则表达式匹配

    location ~ \.html$ { #匹配.html结尾的URL,用来解析html文件。里面的root也是一样,用来表示虚拟主机的根目录。
      root           /Users/marscheng/www;
      fastcgi_pass   127.0.0.1:9000;
      fastcgi_index  index.html;
      include        fastcgi.conf;
    }
    

这里只是简单介绍,详细的配置可以参考这篇文章:
http://seanlook.com/2015/05/17/nginx-location-rewrite/

网上还有一篇介绍Nginx比较好的,文章,地址如下:
http://www.jianshu.com/p/bed000e1830b

5.方案设计

根据场景设计了如下的配置方案:

http {
#此处省略一些基本配置

upstream product_server{
    server 9.236.2.35:8081;
}

upstream admin_server{
    server 9.236.2.36:8082;
}

upstream finance_server{
    server 9.236.2.37:8083;
}

server {
    #此处省略一些基本配置
    #根据不同正则匹配默认指向不同的的server
    location / {
        proxy_pass http://product_server;
    }

    location /product/{
        proxy_pass http://product_server;
    }

    location /admin/ {
        proxy_pass http://admin_server;
    }

    location /finance/ {
        proxy_pass http://finance_server;
    }
}

}