结构修改

This commit is contained in:
fxb 2018-11-25 12:37:54 +08:00
parent bd5ede5b94
commit b918c22e50
34 changed files with 455 additions and 3084 deletions

View File

@ -1,456 +1,456 @@
[id]:2018-10-01
[type]:项目
[tag]:node,vue,element-ui,axios,koa,redis,mysql
  毕业才刚刚两个多月而已,现在想想大学生活是那么的遥不可及,感觉已经过了好久好久,社会了两个月才明白学校的好啊。。。额,扯远了,自从毕业开始就想找个时间写下毕设的记录总结,结果找了好久好久到今天才开始动笔。
  我的毕业设计题目是:教学辅助系统的设计与实现,,是不是很俗。。。至于为啥是这个题目呢,完全是被导师坑了。。。。。
## 1、需求分析
  拿到这个题目想着这个可能被做了无数次了,就像着哪里能够做出点创新,,最后强行创新出了一个个性化组题(根据学生水平出题)和徽章激励(达到某个要求给予一个徽章)。最后就产生了如下需求,系统有学生端和管理端:
学生端:
- 个人资料设置
- 徽章激励机制
- 查看课程信息,下载课程资料
- 知识点检测及针对性训练
- 在线作业,考试
- 在线答疑,向老师或者学生提问
管理端:
- 课程管理,用户管理(需要管理员权限)
- 课程信息管理
- 课程公告管理
- 题库管理,支持单选,多选,填空,编程题,支持题目编组
- 发布作业,包括个性组题和手动组题
- 发布考试,包括随机出题和手动出题
- 自动判题,支持编程题判重
- 在线答疑,给学生解答
- 统计分析,包含测试统计和课程统计
洋洋洒洒需求列了一大堆,后面才发现是给自己挖坑,,答辩老师一看这类的题目就不感兴趣了,不论你做的咋样(况且我的演讲能力真的很一般),最后累死累活写了一大堆功能也没太高的分,,不过倒是让我的系统设计能力和代码能力有了不少的提高。
## 2、架构选择
  大三的时候了解到Node.js这个比较“奇葩"的异步语言再加上在公司实习了三个月也是用的node开发对node已经比较熟悉了于是就用它做了后台前端用最近比较火的vue.js做单页应用。当时还想着负载均衡啥的就没有用传统的sessioncookie机制转而用jwt做的基于token的身份认证同时后台接口也是类Restful风格的因为纯正的Rest接口太难设计了
总的来说后台用了以下技术和框架:
  总的来说后台用了以下技术和框架:
- 语言Node.js
- web框架kOA
- 前后台传输协议jwt
- 缓存redis
- 数据库mysql
- 编程题判题核心:[青岛大学OJ判题核心](https://github.com/QingdaoU/JudgeServer)
- 代码判重:[SIM](https://dickgrune.com/Programs/similarity_tester/)
前台技术如下:
- 框架Vue.js
- UI框架Element-UI
- 图表组件G2
## 3、系统基础框架搭建
  本系统是前后端分离的,下面分别介绍前后端的实现基础。
### 1、后台
  一个web后台最重要的无非那么几个部分路由权限验证数据持久化。
#### a、路由
KOA作为一个web框架其实它本身并没有提供路由功能需要配合使用koa-router来实现路由koa-router以类似下面这样的风格来进行路由
  KOA作为一个web框架其实它本身并没有提供路由功能需要配合使用koa-router来实现路由koa-router以类似下面这样的风格来进行路由
```javascript
const app = require("koa");
const router = require("koa-router");
router.get("/hello",koa=>{
koa.response="hello";
});
app.use(router.routes())
```
显然这样在项目中是很不方便的如果每个路由都要手动进行挂载很难将每个文件中的路由都挂载到一个router中。因此在参考网上的实现后我写了一个方法在启动时自动扫描某个文件夹下所有的路由文件并挂载到router中代码如下
```javascript
const fs = require('fs');
const path = require('path');
const koaBody = require('koa-body');
const config = require('../config/config.js');
function addMapping(router, filePath) {
let mapping = require(filePath);
for (let url in mapping) {
if (url.startsWith('GET ')) {
let temp = url.substring(4);
router.get(temp, mapping[url]);
console.log(`----GET${temp}`);
} else if (url.startsWith('POST ')) {
let temp = url.substring(5);
router.post(temp, mapping[url]);
console.log(`----POST${temp}`);
} else if (url.startsWith('PUT ')) {
let temp = url.substring(4);
router.put(temp, mapping[url]);
console.log(`----PUT${temp}`)
} else if (url.startsWith('DELETE ')) {
let temp = url.substring(7);
router.delete(temp, mapping[url]);
console.log(`----DELETE: ${temp}`);
} else {
console.log(`xxxxx无效路径${url}`);
}
}
}
function addControllers(router, filePath) {
let files = fs.readdirSync(filePath);
files.forEach(element => {
let temp = path.join(filePath, element);
let state = fs.statSync(temp);
if (state.isDirectory()) {
addControllers(router, temp);
} else {
if (!temp.endsWith('Helper.js')) {
console.log('\n--开始处理: ' + element + "路由");
addMapping(router, temp);
}
}
});
}
function engine(router, folder) {
addControllers(router, folder);
return router.routes();
}
module.exports = engine;
```
然后在index.js中use此方法
```
const RouterMW = require("./middleware/controllerEngine.js");
app.use(RouterMW(router,path.join(config.rootPath, 'api')));
```
然后路由文件以下面的形式编写:
```javascript
const knowledgePointDao = require('../dao/knowledgePointDao.js');
/**
* 返回某门课的全部知识点,按章节分类
*/
exports["GET /course/:c_id/knowledge_point"] = async (ctx, next) => {
let res = await knowledgePointDao.getPontsOrderBySection(ctx.params.c_id);
ctx.onSuccess(res);
}
//返回某位学生知识点答题情况
exports["GET /user/:u_id/course/:c_id/knowledge_point/condition"]=async(ctx,next)=>{
let {u_id,c_id}=ctx.params;
let res = await knowledgePointDao.getStudentCondition(u_id,c_id);
ctx.onSuccess(res);
}
```
#### b、权限验证
  权限管理是一个系统最重要的部分之一,目前主流的方式为**基于角色的权限管理** 一个用户对应多个角色每个角色对应多个权限本系统中每个用户对应一个身份每个身份对应多个角色。我们的系统如何实现的呢先从登录开始说起本系统抛弃了传统的cookiesession模式使用json web tokenJWT来做身份认证用户登录后返回一个token给客户端代码如下所示
```javascript
//生成随机盐值
let str = StringHelper.getRandomString(0, 10);
//使用该盐值生成token
let token = jwt.sign({
u_id: userInfo.u_id,
isRememberMe
}, str, {
expiresIn: isRememberMe ? config.longTokenExpiration:config.shortTokenExpiration
});
//token-盐值存入redis如想让该token过期redis中清楚该token键值对即可
await RedisHelper.setString(token, str, 30 * 24 * 60 * 60);
res.code = 1;
res.info = '登录成功';
res.data = {
u_type: userInfo.u_type,
u_id: userInfo.u_id,
token
};
```
以后每次客户端请求都要在header中设置该token然后每次服务端收到请求都先验证是否拥有权限验证代码使用`router.use(auth)`,挂载到koa-router中这样每次在进入具体的路由前都要先执行auth方法进行权限验证,主要验证代码逻辑如下:
```javascript
/**
* 1 验证成功
* 2 登录信息无效 401
* 3 已登录,无操作权限 403
* 4 token已过期
*/
let verify = async (ctx) => {
let token = ctx.headers.authorization;
if (typeof (token) != 'string') {
return 2;
}
let yan = await redisHelper.getString(token);
if (yan == null) {
return 2;
}
let data;
try {
data = jwt.verify(token, yan);
} catch (e) {
return 2;
}
if (data.exp * 1000 < Date.now()) {
return 4;
}
//判断是否需要刷新token如需要刷新将新token写入响应头
if (!data.isRememberMe && (data.exp * 1000 - Date.now()) < 30 * 60 * 1000) {
//token有效期不足半小时重新签发新token给客户端
let newYan = StringHelper.getRandomString(0, 10);
let newToken = jwt.sign({
u_id: data.u_id,
isRememberMe:false
}, newYan, {
expiresIn: config.shortTokenExpiration
});
// await redisHelper.deleteKey(token);
await redisHelper.setString(newToken, newYan,config.shortTokenExpiration);
ctx.response.set('new-token', newToken);
ctx.response.set('Access-Control-Expose-Headers','new-token');
}
//获取用户信息
let userInfoKey = data.u_id + '_userInfo';
let userInfo = await redisHelper.getString(userInfoKey);
if (userInfo == null || Object.keys(userInfo).length != 3) {
userInfo = await mysqlHelper.first(`select u_id,u_type,j_id from user where u_id=?`, data.u_id);
await redisHelper.setString(userInfoKey, JSON.stringify(userInfo), 24 * 60 * 60);
}else{
userInfo = JSON.parse(userInfo);
}
ctx.userInfo = userInfo;
//更新用户上次访问时间
mysqlHelper.execute(`update user set last_login_time=? where u_id=?`,Date.now(),userInfo.u_id);
//管理员拥有全部权限
if (userInfo.u_type == 0) {
return 1;
}
//获取该用户类型权限
let authKey = userInfo.j_id + '_authority';
let urls = await redisHelper.getObject(authKey);
// let urls = null;
if (urls == null) {
urls = await mysqlHelper.row(`
select b.r_id,b.url,b.method from jurisdiction_resource a inner join resource b on a.r_id = b.r_id where a.j_id=?
`, userInfo.j_id);
let temp = {};
urls.forEach(item => {
temp[item.url + item.method] = true;
})
await redisHelper.setObject(authKey, temp);
urls = temp;
}
//判断是否拥有权限
if (urls.hasOwnProperty(ctx._matchedRoute.replace(config.url_prefix, '') + ctx.method)) {
return 1;
} else {
return 3;
}
}
```
根据用户id获取用户身份id根据用户身份id从redis中获取拥有的权限如为null从mysql数据库中拉取并存入redis中然后判断是否拥有要访问的url权限。
#### c、数据持久化
&emsp;&emsp;本系统中使用mysql存储数据redis做缓存由于当时操作库不支持promise故对它两做了个promise封装方便代码中调用参见[MysqlHelper](https://github.com/FleyX/teach_system/tree/master/teachSystem/util/MysqlHelper.js),[RedisHelper.js](https://github.com/FleyX/teach_system/tree/master/teachSystem/util/RedisHelper.js)。
### 2、前端
&emsp;&emsp;前端使用vue-cli构建vue项目主要用到了vue-router,element-ui,axios这三个组件。
#### a、路由组织
&emsp;&emsp;单页应用需要前端自己组织路由。本系统将路由分成了三个部分公共管理端学生端。index.js如下
```javascript
export default new Router({
mode: 'history',
base: '/app/',
routes: [{
path: '',
name: 'indexPage',
component: IndexPage
},
{
path: '/about',
name: 'about',
component: About
},
Admin,
Client,
Public,
{
path: '*',
name: "NotFound",
component: NotFound
}
]
})
```
其中的AdminClientPublic分别为各部分的路由以子路由的形式一级级组织。如下所示
```javascript
export default {
path: "/client",
component: Client,
beforeEnter: (to, from, next) => {
if (getClientUserInfo() == null) {
next({
path: '/public/client_login',
replace: true,
})
} else {
next();
}
},
children: [{
//学生端主页
path: '',
name: "ClientMain",
component: ClientHome
}, {
//学生个人资料页面
path: 'person/student_info',
name: "StudentInfo",
component: StudentInfo
}, {
//公告页面
path: 'course/:c_id/announcement',
name: 'Main',
component: Announcement
}, {
//课程基本信息
path: 'course/:c_id/base',
component: ClientMain,
children: [{
path: 'course_intro',
name: "ClientCourseIntro",
component: CourseIntro
}, {
path: 'exam_type',
name: "ClientExamType",
component: ExamType
}
......
```
其中的beforEnter为钩子函数每次进入路由时执行该函数用于判断用户是否登录。这里涉及到了一个前端鉴权的概念由于前后端分离了前端也必须做鉴权以免用户进入到了无权限的页面这里我只是简单的做了登录判断更详细的url鉴权也可实现只需在对应的钩子函数中进行鉴权操作更多关于钩子函数信息[点击这里](https://router.vuejs.org/zh/guide/advanced/navigation-guards.html)。
#### b、请求封装
&emsp;&emsp;前端还有一个比较重要的部分是ajax请求的处理请求处理还保护错误处理有些错误只需要统一处理而有些又需要独立的处理这样一来就需要根据业务需求进行一下请求封装了对结果进行处理后再返回给调用者。我的实现思路是发起请求收到响应后先对错误进行一个同意弹窗提示然后再将错误继续向后传递调用者可选择性的捕获错误进行针对性处理主要代码如下
```javascript
request = (url, method, params, form, isFormData, type) => {
let token;
if (type == 'admin')
token = getToken();
else
token = getClientToken();
let headers = {
'Authorization': token
};
if (isFormData) {
headers['Content-Type'] = "multipart/form-data";
}
return new Promise((resolve, reject) => {
axios({
url,
method,
params,
data: form,
headers,
// timeout:2000
}).then(res => {
resolve(res.data);
//检查是否有更新token
// console.log(res);
if (res.headers['new-token'] != undefined) {
console.log('set new token');
if (vm.$route.path.startsWith('/admin')){
localStorage.setItem("token",res.headers['new-token']);
window.token = undefined;
}else if(vm.$route.path.startsWith('/client')){
localStorage.setItem("clientToken",res.headers['new-token']);
window.clientToken = undefined;
}
}
}).catch(err => {
reject(err);
if (err.code == 'ECONNABORTED') {
alertNotify("错误", "请求超时", "error");
return;
}
if (err.message == 'Network Error') {
alertNotify("错误", "无法连接服务器", 'error');
return;
}
if (err.response != undefined) {
switch (err.response.status) {
case 401:
if (window.isGoToLogin) {
return;
}
//使用该变量表示是否已经弹窗提示了,避免大量未登录弹窗堆积。
window.isGoToLogin = true;
vm.$alert(err.response.data, "警告", {
type: "warning",
showClose: false
}).then(res => {
window.isGoToLogin = false;
if (vm.$route.path.startsWith('/admin/')) {
clearInfo();
vm.$router.replace("/public/admin_login");
} else {
clearClientInfo();
vm.$router.replace("/public/client_login");
}
});
break;
case 403:
alertNotify("Error:403", '拒绝执行:' + err.response.data, "error");
break;
case 404:
alertNotify("Error:404", "找不到资源:" + url.substr(0, url.indexOf('?')), 'error');
break;
case 400:
alertNotify("Error:400", "请求参数错误:" + err.response.data, 'error');
break;
case 500:
alertNotify("Error:500", "服务器内部错误:" + err.response.data, 'error');
default:
console.log('存在错误未处理:' + err);
}
} else {
console.log(err);
}
})
})
}
```
[id]:2018-10-01
[type]:项目
[tag]:node,vue,element-ui,axios,koa,redis,mysql
&emsp;&emsp;毕业才刚刚两个多月而已,现在想想大学生活是那么的遥不可及,感觉已经过了好久好久,社会了两个月才明白学校的好啊。。。额,扯远了,自从毕业开始就想找个时间写下毕设的记录总结,结果找了好久好久到今天才开始动笔。
&emsp;&emsp;我的毕业设计题目是:教学辅助系统的设计与实现,,是不是很俗。。。至于为啥是这个题目呢,完全是被导师坑了。。。。。
## 1、需求分析
&emsp;&emsp;拿到这个题目想着这个可能被做了无数次了,就像着哪里能够做出点创新,,最后强行创新出了一个个性化组题(根据学生水平出题)和徽章激励(达到某个要求给予一个徽章)。最后就产生了如下需求,系统有学生端和管理端:
学生端:
- 个人资料设置
- 徽章激励机制
- 查看课程信息,下载课程资料
- 知识点检测及针对性训练
- 在线作业,考试
- 在线答疑,向老师或者学生提问
管理端:
- 课程管理,用户管理(需要管理员权限)
- 课程信息管理
- 课程公告管理
- 题库管理,支持单选,多选,填空,编程题,支持题目编组
- 发布作业,包括个性组题和手动组题
- 发布考试,包括随机出题和手动出题
- 自动判题,支持编程题判重
- 在线答疑,给学生解答
- 统计分析,包含测试统计和课程统计
洋洋洒洒需求列了一大堆,后面才发现是给自己挖坑,,答辩老师一看这类的题目就不感兴趣了,不论你做的咋样(况且我的演讲能力真的很一般),最后累死累活写了一大堆功能也没太高的分,,不过倒是让我的系统设计能力和代码能力有了不少的提高。
## 2、架构选择
&emsp;&emsp;大三的时候了解到Node.js这个比较“奇葩"的异步语言再加上在公司实习了三个月也是用的node开发对node已经比较熟悉了于是就用它做了后台前端用最近比较火的vue.js做单页应用。当时还想着负载均衡啥的就没有用传统的sessioncookie机制转而用jwt做的基于token的身份认证同时后台接口也是类Restful风格的因为纯正的Rest接口太难设计了
总的来说后台用了以下技术和框架:
&emsp;&emsp;总的来说后台用了以下技术和框架:
- 语言Node.js
- web框架kOA
- 前后台传输协议jwt
- 缓存redis
- 数据库mysql
- 编程题判题核心:[青岛大学OJ判题核心](https://github.com/QingdaoU/JudgeServer)
- 代码判重:[SIM](https://dickgrune.com/Programs/similarity_tester/)
前台技术如下:
- 框架Vue.js
- UI框架Element-UI
- 图表组件G2
## 3、系统基础框架搭建
&emsp;&emsp;本系统是前后端分离的,下面分别介绍前后端的实现基础。
### 1、后台
&emsp;&emsp;一个web后台最重要的无非那么几个部分路由权限验证数据持久化。
#### a、路由
KOA作为一个web框架其实它本身并没有提供路由功能需要配合使用koa-router来实现路由koa-router以类似下面这样的风格来进行路由
&emsp;&emsp;KOA作为一个web框架其实它本身并没有提供路由功能需要配合使用koa-router来实现路由koa-router以类似下面这样的风格来进行路由
```javascript
const app = require("koa");
const router = require("koa-router");
router.get("/hello",koa=>{
koa.response="hello";
});
app.use(router.routes())
```
显然这样在项目中是很不方便的如果每个路由都要手动进行挂载很难将每个文件中的路由都挂载到一个router中。因此在参考网上的实现后我写了一个方法在启动时自动扫描某个文件夹下所有的路由文件并挂载到router中代码如下
```javascript
const fs = require('fs');
const path = require('path');
const koaBody = require('koa-body');
const config = require('../config/config.js');
function addMapping(router, filePath) {
let mapping = require(filePath);
for (let url in mapping) {
if (url.startsWith('GET ')) {
let temp = url.substring(4);
router.get(temp, mapping[url]);
console.log(`----GET${temp}`);
} else if (url.startsWith('POST ')) {
let temp = url.substring(5);
router.post(temp, mapping[url]);
console.log(`----POST${temp}`);
} else if (url.startsWith('PUT ')) {
let temp = url.substring(4);
router.put(temp, mapping[url]);
console.log(`----PUT${temp}`)
} else if (url.startsWith('DELETE ')) {
let temp = url.substring(7);
router.delete(temp, mapping[url]);
console.log(`----DELETE: ${temp}`);
} else {
console.log(`xxxxx无效路径${url}`);
}
}
}
function addControllers(router, filePath) {
let files = fs.readdirSync(filePath);
files.forEach(element => {
let temp = path.join(filePath, element);
let state = fs.statSync(temp);
if (state.isDirectory()) {
addControllers(router, temp);
} else {
if (!temp.endsWith('Helper.js')) {
console.log('\n--开始处理: ' + element + "路由");
addMapping(router, temp);
}
}
});
}
function engine(router, folder) {
addControllers(router, folder);
return router.routes();
}
module.exports = engine;
```
然后在index.js中use此方法
```
const RouterMW = require("./middleware/controllerEngine.js");
app.use(RouterMW(router,path.join(config.rootPath, 'api')));
```
然后路由文件以下面的形式编写:
```javascript
const knowledgePointDao = require('../dao/knowledgePointDao.js');
/**
* 返回某门课的全部知识点,按章节分类
*/
exports["GET /course/:c_id/knowledge_point"] = async (ctx, next) => {
let res = await knowledgePointDao.getPontsOrderBySection(ctx.params.c_id);
ctx.onSuccess(res);
}
//返回某位学生知识点答题情况
exports["GET /user/:u_id/course/:c_id/knowledge_point/condition"]=async(ctx,next)=>{
let {u_id,c_id}=ctx.params;
let res = await knowledgePointDao.getStudentCondition(u_id,c_id);
ctx.onSuccess(res);
}
```
#### b、权限验证
&emsp;&emsp;权限管理是一个系统最重要的部分之一,目前主流的方式为**基于角色的权限管理** 一个用户对应多个角色每个角色对应多个权限本系统中每个用户对应一个身份每个身份对应多个角色。我们的系统如何实现的呢先从登录开始说起本系统抛弃了传统的cookiesession模式使用json web tokenJWT来做身份认证用户登录后返回一个token给客户端代码如下所示
```javascript
//生成随机盐值
let str = StringHelper.getRandomString(0, 10);
//使用该盐值生成token
let token = jwt.sign({
u_id: userInfo.u_id,
isRememberMe
}, str, {
expiresIn: isRememberMe ? config.longTokenExpiration:config.shortTokenExpiration
});
//token-盐值存入redis如想让该token过期redis中清楚该token键值对即可
await RedisHelper.setString(token, str, 30 * 24 * 60 * 60);
res.code = 1;
res.info = '登录成功';
res.data = {
u_type: userInfo.u_type,
u_id: userInfo.u_id,
token
};
```
以后每次客户端请求都要在header中设置该token然后每次服务端收到请求都先验证是否拥有权限验证代码使用`router.use(auth)`,挂载到koa-router中这样每次在进入具体的路由前都要先执行auth方法进行权限验证,主要验证代码逻辑如下:
```javascript
/**
* 1 验证成功
* 2 登录信息无效 401
* 3 已登录,无操作权限 403
* 4 token已过期
*/
let verify = async (ctx) => {
let token = ctx.headers.authorization;
if (typeof (token) != 'string') {
return 2;
}
let yan = await redisHelper.getString(token);
if (yan == null) {
return 2;
}
let data;
try {
data = jwt.verify(token, yan);
} catch (e) {
return 2;
}
if (data.exp * 1000 < Date.now()) {
return 4;
}
//判断是否需要刷新token如需要刷新将新token写入响应头
if (!data.isRememberMe && (data.exp * 1000 - Date.now()) < 30 * 60 * 1000) {
//token有效期不足半小时重新签发新token给客户端
let newYan = StringHelper.getRandomString(0, 10);
let newToken = jwt.sign({
u_id: data.u_id,
isRememberMe:false
}, newYan, {
expiresIn: config.shortTokenExpiration
});
// await redisHelper.deleteKey(token);
await redisHelper.setString(newToken, newYan,config.shortTokenExpiration);
ctx.response.set('new-token', newToken);
ctx.response.set('Access-Control-Expose-Headers','new-token');
}
//获取用户信息
let userInfoKey = data.u_id + '_userInfo';
let userInfo = await redisHelper.getString(userInfoKey);
if (userInfo == null || Object.keys(userInfo).length != 3) {
userInfo = await mysqlHelper.first(`select u_id,u_type,j_id from user where u_id=?`, data.u_id);
await redisHelper.setString(userInfoKey, JSON.stringify(userInfo), 24 * 60 * 60);
}else{
userInfo = JSON.parse(userInfo);
}
ctx.userInfo = userInfo;
//更新用户上次访问时间
mysqlHelper.execute(`update user set last_login_time=? where u_id=?`,Date.now(),userInfo.u_id);
//管理员拥有全部权限
if (userInfo.u_type == 0) {
return 1;
}
//获取该用户类型权限
let authKey = userInfo.j_id + '_authority';
let urls = await redisHelper.getObject(authKey);
// let urls = null;
if (urls == null) {
urls = await mysqlHelper.row(`
select b.r_id,b.url,b.method from jurisdiction_resource a inner join resource b on a.r_id = b.r_id where a.j_id=?
`, userInfo.j_id);
let temp = {};
urls.forEach(item => {
temp[item.url + item.method] = true;
})
await redisHelper.setObject(authKey, temp);
urls = temp;
}
//判断是否拥有权限
if (urls.hasOwnProperty(ctx._matchedRoute.replace(config.url_prefix, '') + ctx.method)) {
return 1;
} else {
return 3;
}
}
```
根据用户id获取用户身份id根据用户身份id从redis中获取拥有的权限如为null从mysql数据库中拉取并存入redis中然后判断是否拥有要访问的url权限。
#### c、数据持久化
&emsp;&emsp;本系统中使用mysql存储数据redis做缓存由于当时操作库不支持promise故对它两做了个promise封装方便代码中调用参见[MysqlHelper](https://github.com/FleyX/teach_system/tree/master/teachSystem/util/MysqlHelper.js),[RedisHelper.js](https://github.com/FleyX/teach_system/tree/master/teachSystem/util/RedisHelper.js)。
### 2、前端
&emsp;&emsp;前端使用vue-cli构建vue项目主要用到了vue-router,element-ui,axios这三个组件。
#### a、路由组织
&emsp;&emsp;单页应用需要前端自己组织路由。本系统将路由分成了三个部分公共管理端学生端。index.js如下
```javascript
export default new Router({
mode: 'history',
base: '/app/',
routes: [{
path: '',
name: 'indexPage',
component: IndexPage
},
{
path: '/about',
name: 'about',
component: About
},
Admin,
Client,
Public,
{
path: '*',
name: "NotFound",
component: NotFound
}
]
})
```
其中的AdminClientPublic分别为各部分的路由以子路由的形式一级级组织。如下所示
```javascript
export default {
path: "/client",
component: Client,
beforeEnter: (to, from, next) => {
if (getClientUserInfo() == null) {
next({
path: '/public/client_login',
replace: true,
})
} else {
next();
}
},
children: [{
//学生端主页
path: '',
name: "ClientMain",
component: ClientHome
}, {
//学生个人资料页面
path: 'person/student_info',
name: "StudentInfo",
component: StudentInfo
}, {
//公告页面
path: 'course/:c_id/announcement',
name: 'Main',
component: Announcement
}, {
//课程基本信息
path: 'course/:c_id/base',
component: ClientMain,
children: [{
path: 'course_intro',
name: "ClientCourseIntro",
component: CourseIntro
}, {
path: 'exam_type',
name: "ClientExamType",
component: ExamType
}
......
```
其中的beforEnter为钩子函数每次进入路由时执行该函数用于判断用户是否登录。这里涉及到了一个前端鉴权的概念由于前后端分离了前端也必须做鉴权以免用户进入到了无权限的页面这里我只是简单的做了登录判断更详细的url鉴权也可实现只需在对应的钩子函数中进行鉴权操作更多关于钩子函数信息[点击这里](https://router.vuejs.org/zh/guide/advanced/navigation-guards.html)。
#### b、请求封装
&emsp;&emsp;前端还有一个比较重要的部分是ajax请求的处理请求处理还保护错误处理有些错误只需要统一处理而有些又需要独立的处理这样一来就需要根据业务需求进行一下请求封装了对结果进行处理后再返回给调用者。我的实现思路是发起请求收到响应后先对错误进行一个同意弹窗提示然后再将错误继续向后传递调用者可选择性的捕获错误进行针对性处理主要代码如下
```javascript
request = (url, method, params, form, isFormData, type) => {
let token;
if (type == 'admin')
token = getToken();
else
token = getClientToken();
let headers = {
'Authorization': token
};
if (isFormData) {
headers['Content-Type'] = "multipart/form-data";
}
return new Promise((resolve, reject) => {
axios({
url,
method,
params,
data: form,
headers,
// timeout:2000
}).then(res => {
resolve(res.data);
//检查是否有更新token
// console.log(res);
if (res.headers['new-token'] != undefined) {
console.log('set new token');
if (vm.$route.path.startsWith('/admin')){
localStorage.setItem("token",res.headers['new-token']);
window.token = undefined;
}else if(vm.$route.path.startsWith('/client')){
localStorage.setItem("clientToken",res.headers['new-token']);
window.clientToken = undefined;
}
}
}).catch(err => {
reject(err);
if (err.code == 'ECONNABORTED') {
alertNotify("错误", "请求超时", "error");
return;
}
if (err.message == 'Network Error') {
alertNotify("错误", "无法连接服务器", 'error');
return;
}
if (err.response != undefined) {
switch (err.response.status) {
case 401:
if (window.isGoToLogin) {
return;
}
//使用该变量表示是否已经弹窗提示了,避免大量未登录弹窗堆积。
window.isGoToLogin = true;
vm.$alert(err.response.data, "警告", {
type: "warning",
showClose: false
}).then(res => {
window.isGoToLogin = false;
if (vm.$route.path.startsWith('/admin/')) {
clearInfo();
vm.$router.replace("/public/admin_login");
} else {
clearClientInfo();
vm.$router.replace("/public/client_login");
}
});
break;
case 403:
alertNotify("Error:403", '拒绝执行:' + err.response.data, "error");
break;
case 404:
alertNotify("Error:404", "找不到资源:" + url.substr(0, url.indexOf('?')), 'error');
break;
case 400:
alertNotify("Error:400", "请求参数错误:" + err.response.data, 'error');
break;
case 500:
alertNotify("Error:500", "服务器内部错误:" + err.response.data, 'error');
default:
console.log('存在错误未处理:' + err);
}
} else {
console.log(err);
}
})
})
}
```
&emsp;&emsp;到这里就算是简单介绍完了,,想要更加深入了解的可以去github查看源代码地址如下[https://github.com/FleyX/teach_system](https://github.com/FleyX/teach_system)记得star哦

View File

@ -1,182 +0,0 @@
---
id="2018-10-20-10-38-05"
title="spring基础"
headWord="spring是为了解决企业级应用开发的复杂性而创建的spring最根本的使命是简化Java开发。为降低开发复杂性有以下四种关键策略。 "
tags=["java", "spring"]
category="java"
serie="spring学习"
---
[id]:2018-08-12_1
[type]:javaee
[tag]:java,spring
&emsp;&emsp;spring是为了解决企业级应用开发的复杂性而创建的spring最根本的使命是简化Java开发。为降低开发复杂性有以下四种关键策略。
- 基于POJO的轻量级和最小侵入性编程
- 通过依赖注入和面向接口实现松耦合
- 基于切面和惯例进行声明式编程
- 通过切面和模板减少样板式代码
#### 1.依赖注入
&emsp;&emsp;假设类A依赖类B通常做法是在类A中声明类B然后使用这样一方面具有极高的耦合性将类A与类B绑定在一起另一方面也让单元测试变得很困难无法在A外部获得B的执行情况。
&emsp;&emsp;通过依赖注入,对象的依赖管理将不用对象本身来管理,将由一个第三方组件在创建对象时设定,依赖关系将被自动注入到对应的对象中去。
#### 2.创建应用上下文
- `ClassPathXmlApplicationContext()`从类路径创建
- `FileSystemXmlApplicationContext()`读取文件系统下的xml配置
- `XmlWebApplicationContext()` 读取web应用下的XML配置文件并装载上下文定义
#### 3.声明Bean
1. 最简单
`<bean id="bean1" class="com.example.Class"/>`
2. 带构造器
```xml
<bean id="bean1" class="com.example.Class">
<contructor-arg value="12"/> //基本数据类型使用value
<contructor-arg ref="bean2"/> //对象使用ref
</bean>
```
3. 通过工厂方法创建
如果想声明的Bean没有一个公开的构造函数通过factory-method属性来装配工厂生产的Bean
```xml
<bean id="bean1" class="com.example.class" factory-method="getInstance"/>//getInstance为获取实例的静态方法。
```
#### 4.Bean的作用域
所有Spring Bean默认都是单例的。通过配置scope属性为prototype可每次请求产生一个新的实例。
```xml
<bean id="bean3" class="com.example.class" scope="prototype">
```
scope可选值
- `singleton`每个容器中一个Bean对象只有一个实例。**默认**
- `prototype`:允许实例化任意次 ,每次请求都会创建新的
- `request`作用域为一次http请求
- `session`作用域为一个http session会话
- `global-session`作用域为一个全局http session仅在Protlet上下文中有效
#### 5.初始化和销毁Bean
当实例化需要执行初始化操作,或者销毁时需要执行清理工作。两种实现方式:
1. xml配置类中编写初始化方法和销毁方法在bean中定义。
```xml
<bean id="bean4" class="com.example.Class" init-method="start" destroy-method="destroy"/>
```
也可在Beans中定义默认初始化和销毁方法。
```xml
<beans . . . default-init-method="" default-destroy-method=""/>
```
2. 实现`InitializingBean ``DisposableBean`接口
#### 6.setter注入
在bean中使用`<property>`元素配置属性,使用方法类似于`<constructor-arg>`
```xml
<property name="name" value="fxg"/> //注入基本数据类型
<property name="sex" ref="sex"/> //注入类
```
可使用p简写,**-ref**后缀说明装配的是一个引用
```xml
<bean id="bean5" class="com.example.class"
p:name="fxb"
p:sex-ref="sex"/>
```
#### 7.注入内部Bean
既定义其他Bean内部的Bean避免共享问题可在属性节点或者构造器参数节点上使用。
```xml
<property name="sex">
<bean class="com.example.sex"/> //没有id属性因为不会被其他bean使用
</property>
<constructor-arg>
<bean class="com.example.sex"/>
</constructor-arg>
```
#### 8.装配集合
| 集合元素 | 用途 |
| ---------------- | ------------------------------ |
| \<list\> | 装配list类型允许重复 |
| \<set\> | set不能重复 |
| \<map\> | map类型 |
| \<props\> | properties类型键值都为String |
- list
```xml
<property name="instruments">
<list>
<ref bean="guitar"/>
<ref bean="cymbal"/>
<ref bean="harmonica"/>
</list>
</property>
<ref>用来定义上下文中的其他引用,还可使用<value>,<bean>,<null/>
```
- set
```xml
<set>
<ref bean="fasdf"/>
</set>
```
用法和list相同只是不能重复
- Map
```XML
<map>
<entry key="GUITAR" value-ref="guitar"/>
</map>
```
entry元素由一个key一个value组成分别有两种形式。
| key | 键为String |
| :-------- | -------------- |
| key-ref | 键为Bean的引用 |
| value | 值为String |
| value-ref | 值为Bean的引用 |
- props
```xml
<props>
<prop key="GUITAR">guitar</prop>
</props>
```
键值都是String
#### 9.装配空值
```xml
<property name="name"><null/></property>
```

View File

@ -1,231 +0,0 @@
---
id="2018-10-21-10-38-05"
title="spring之最小化XML配置"
headWord="spring是为了解决企业级应用开发的复杂性而创建的但是最初的Spring在随着应用程序的规模越来越大的情况下xml配置文件也随之膨胀变得不便于阅读与管理随后就有了一系列的手段来减少xml配置直到一行都没有"
tags=["java", "spring"]
category="java"
serie="spring学习"
---
## 一、自动装配
### 1、四种类型的自动装配
| 类型 | 解释 | xml配置 |
| ---------- | ------------------------------------ | ---------------------------------------------- |
| byName | 根据Bean的name或者id | \<bean id="bean" class="…" autowire="byName"/> |
| ByType | 根据Bean类型自动装配 | \<bean id="bean" class="…" autowire="byType"/> |
| contructor | 根据Bean的构造器入参具有相同类型 | 同上 |
| Autodetect | 首先使用contructor失败再尝试byType | 同上 |
&emsp;&emsp;byType在出现多个匹配项时不会自动选择一个然是报错为避免报错有两种办法1.使用\<bean>元素的primary属性设置为首选Bean但所有bean的默认primary都是true因此我们需要将所有非首选Bean设置为false2.将Bean的`autowire-candidate`熟悉设置为**false **,取消 这个Bean的候选资格这个Bean便不会自动注入了。
&emsp;&emsp;contructor自动装配和byType有一样的局限性当发现多个Bean匹配某个构造器入参时Spring不会尝试选择其中一个此外如果一个类有多个构造器都满足自动装配的条件Spring也不会猜测哪个更合适使用。
### 2、默认自动装配
&emsp;&emsp;如果需要为Spring应用上下文中的每个Bean或者其中的大多数配置相同的autowire属性可以在根元素\<beans>上增加一个default-autowire属性默认该属性设置为none。该属性只应用于指定配置文件中的所有Bean并不是Spring上下文中的所有Bean。
### 3、混合使用自动装配和显式装配
&emsp;&emsp;当我们对某个Bean使用了自动装配策略并不代表我们不能对该Bean的某些属性进行显示装配任然可以为任意一个属性配置\<property>元素,显式装配将会覆盖自动装配。**但是**当使用constructor自动装配策略时我们必须让Spring自动装配构造器所有入参不能使用\<constructor-arg>元素进行混合。
## 二、注解装配
&emsp;&emsp;从Spring2.5开始可以使用注解自动装配Bean的属性使用注解允许更细粒度的自动装配可选择性的标注某一个属性来对其应用自动装配。Spring容器默认禁用注解装配需要在Spring配置中启用最简单的启用方式是使用Spring的context命令空间配置中的`<context:annotation-config>`,如下所示:
```xml
<beans ...>
<context:annotation-config/>
<!-- bean declarations go here -->
</beans>
```
&emsp;&emsp;Spring3支持几种不同的用于自动装配的注解
- Spring自带的@Autowired注解
- JSR-330的@Inject注解
- JSR-250的@Resource注解
### 1、使用@Autowired
&emsp;&emsp;@Autowired用于对被注解对象启动ByType的自动装配,可用于以下对象:
- 类属性,即使私有属性也能注入
- set方法
- 构造器
- 任意需要装配Bean的方法
在使用@Autowired时有两种情况会出错没有匹配的Bean和存在多个匹配的Bean但是都有对应的解决方法。
- 当没有匹配Bean时自动装配会抛出NoSuchBeanDefinitionException如果不想抛出可使用required属性设置为false来配置可选的自动装配即装配失败就不进行装配不会报错。
```java
@Autowired(required=false)
```
当使用构造器配置时只有一个构造器可以将required属性设置为true其他都只能设置为false。此外当使用注解标注多个构造器时Spring会从所有满足装配条件的构造器中选择入参最多的那个。
- 当存在多个Bean满足装配条件时Spring也会抛出NoSuchBeanDefinitionException错误为了选择指定的Bean我们可以使用@Qualifier注解进行筛选:
```java
@Autowired
@Qualifier("name1")//筛选名为name1的Bean
private TestClass testClass;
```
除了通过Bean的ID来缩小选择范围我们还可以通过直接在Bean上使用qualifier来缩小范围限制Bean的类型xml如下
```xml
<bean class="com.test.xxx">
<qualifier value="stringed"/>
</bean>
```
注解如下:
```java
@Qualifier("stringed")
public class xxx{}
```
还可以创建**自定义限定器Qualifier**
&emsp;&emsp;创建自定义限定器只需要使用@Qualifier注解作为它的源注解即可如下创建了一个Stringed限定器
```java
@Target({ElementType.FIELD,ElementType.PARAMETER,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Stringed{}
```
然后使用它注解一个Bean:
```java
@Stringed
public class Guitar{}
```
然后就可以进行限定了:
```java
@Autowired
@Stringed
private Guitar guitar;
```
### 2、使用@Inject自动注入
&emsp;&emsp;为统一各种依赖注入框架的编程模型JCPJava Community Process发布的Java依赖注入规范被称为JSR-330从Spring3开始Spring已经开始兼容该依赖注入模型。
&emsp;&emsp;@Autowired一样@Inject可以用来自动装配属性、方法和构造器。但是@Inject没有required属性,因此依赖关系必须存在,如不存在将抛出异常。
&emsp;&emsp;JSR-330还提供另一种注入技巧注入一个Provider。Provider接口可以实现Bean引用的延迟注入以及注入Bean的多个实例等功能。
&emsp;&emsp;例如我们有一个KnifeJuggler类需要注入一个或多个Knife实例假设Knife Bean的作用域声明为prototype下面的KnifeJuggler的构造器将获得多个Knife Bean:
```java
private Set<Knife> knifes;
@Inject
public KnifeJuggler(Provider<Knife> knifeProvider){
knives = new HashSet<Knife>();
for(int i=0;i<5;i++){
knives.add(knifeProvider.get());
}
}
```
&emsp;&emsp;相对于@Autowired所对应的@Qualifier@Inject对应的是@Named注解。事实上JSR-330中也有@Qualifier注解不过不建议直接使用建议通过该注解来创建自定义的限定注解和Spring的@Qualifier创建过程类似
### 3、注解中使用表达式
&emsp;&emsp;Spring3中引入的`@Value`属性可用来装配String类型的值和基本类型的值。借助SpEL表达式@Value不光可以装配硬编码值还可以在运行期动态计算表达式并装配,例如下面的:
```java
@Value("#{systemProperties.name}")
private String name;
```
## 三、自动检测Bean
&emsp;&emsp;在Spring中使用上面说到的`<context:annotation-config>`可以做到自动装配但还是要在xml中申明Bean。Spring还有另一个元素`<context:component-scan>`,元素除了完成自动装配的功能还允许Spring自动检测Bean和定义Bean ,用法如下:
```xml
<beans ...>
<context:component-scan base-package="com.springtest">
</context:component-scan>
</beans>
```
开启后支持如下注解:
| 注解 | 解释 |
| ----------- | ------------------------------------ |
| @Component | 通用的构造型注解标识类为Spring组件 |
| @Controller | 标识该类定义为Spring MVC controller |
| @Repository | 标识该类定义为数据仓库 |
| @Service | 标识该类定义为服务 |
&emsp;&emsp;使用上述注解是Bean的ID默认为无限定类名。使用`@Component("name")`指定ID。
### 1、过滤组建扫描
&emsp;&emsp;通过为<context:component-scan >配置&lt;context:include-filter&gt;&lt;context:exclude-filter&gt;子元素我们可以随意调整扫描行为。下面的配置自动注册所有的TestInterface实现类
```xml
<context:component-scan base-package="com.fxb.springtest">
<context:include-filter type="assignable"
expression="com.fxb.springTest.TestInterface"/>
</context:component-scan>
```
其中的type和expression属性一起协作来定义组件扫描策略。type有以下值可选择
| 过滤器类型 | 描述 |
| ---------- | ------------------------------------------------------------ |
| annotation | 过滤器扫描使用指定注解所标注的类。通过expression属性指定要扫描的注解 |
| assignable | 过滤器扫描派生于expression属性所指定类型的那些类 |
| aspectj | 过滤器扫描于expression属性所指定的AspectJ表达式所匹配的那些类 |
| custom | 使用自定义的org.springframework.core.type.TypeFilter实现类该类由expression属性指定 |
| regex | 过滤器扫描类的名称与expression属性所指定的正则表达式所匹配的类 |
&emsp;&emsp;exclude-filter使用和include-filter类似只是效果相反。
## 四、使用Spring基于Java的配置
&emsp;&emsp;在Spring3.0中几乎可以不使用XML而使用纯粹的Java代码来配置Spring应用。
- 首先还是需要极少量的XML来启用Java配置就是上面说到的`<context:component-scan>`,该标签还会自动加载使用`@Configuration`注解所标识的类
- @Configuration注解相当于XML配置中的\<beans>元素这个注解将会告知Spring这个类包含一个或多个Spring Bean的定义这些定义是使用@Bean注解所标注的方法
- 申明一个简单的Bean代码如下
```java
@Configuration
public class TestConfig{
@Bean
public Animal duck(){
return new Ducker();
}
}
```
@Bean告知Spring这个方法将返回一个对象该对象应该被注册为Spring应用上下文中的一个Bean方法名作为该Bean的ID 。想要使用另一个Bean的引用也很简单如下
```java
@Bean
public Food duckFood(){
return new DuckFood();
}
@Bean //通过方法名引用一个Bean并不会创建一个新的实例
public Animal duck(){
return new Ducker(DuckFood());
}
```
## 五、小结
&emsp;&emsp;终于写完了spring 的最小化配置对spring的各种注解也有了一些了解再不是之前看到注解一脸莫名其妙了虽然现在Springboot已经帮我们做了零XML配置但觉得还是有必要了解下XML配置实现这样对Java的配置实现理解也会更加深刻。

View File

@ -1,249 +0,0 @@
---
id="2018-10-22-10-38-05"
title="spring之面向切面"
headWord="Spring的基础是IOC和AOP前面两节对IOC和DI做了简单总结这里再对AOP进行一个学习总结Spring基础就算有一个初步了解了。"
tags=["java", "spring"]
category="java"
serie="spring学习"
---
&emsp;&emsp;Spring的基础是IOC和AOP前面两节对IOC和DI做了简单总结这里再对AOP进行一个学习总结Spring基础就算有一个初步了解了。
## 一.面向切面编程
&emsp;&emsp;在软件开发中,我们可能需要一些跟业务无关但是又必须做的东西,比如日志,事务等,这些分布于应用中多处的功能被称为横切关注点,通常横切关注点从概念上是与应用的业务逻辑相分离的。如何将这些横切关注点与业务逻辑在代码层面进行分离,是面向切面编程(**AOP**)所要解决的。
横切关注点可以被描述为影响应用多处的功能,切面能够帮助我们模块化横切关注点。下图直观呈现了横切关注点的概念:
![横切关注点](./picFolder/切面示例.png)
途中CourseServiceStudentServiceMiscService都需要类似安全、事务这样的辅助功能这些辅助功能就被称为横切关注点。
&emsp;&emsp;**继承**和**委托**是最常见的实现重用通用功能的面向对象技术。但是如果在整个程序中使用相同的基类继承往往会导致一个脆弱的对象体系;而使用委托可能需要对委托对象进行复杂的调用。
切面提供了取代继承和委托的另一种选择,而且更加清晰简洁。在面向切面编程时,我们任然在一个地方定义通用功能,但是我们可以通过声明的方式定义这个功能以何种方式在何处应用,而无需修改受影响的类,受影响类完全感受不到切面的存在。
## 二.AOP常用术语
&emsp;&emsp;下面是AOP中常用的名词。
### 1. 通知Advice
&emsp;&emsp;通知定义了切面是什么以及何时使用。出了描述切面要完成的工作通知还解决了何时执行这个工作的问题。Sping切面可以应用以下5种类型的通知。
- **Before** 在方法被调用之前调用通知
- **After** 在方法完成之后调用通知,无论方法执行是否成功
- **After-returning** 在方法成功执行后调用通知
- **After-throwing** 在方法抛出异常后调用通知
- **Around** 通知包裹了被通知的方法,在被通知的方法调用前和调用后执行
###2.连接点Joinpoint
&emsp;&emsp;应用可能有很多个时机应用通知,这些时机被称为连接点。连接点是应用在执行过程中能够插入切面的一个点,这个点可以是调用方法时、抛出异常时、甚至是修改字段时。切面代码可以利用这些切入到应用的正常流程中,并添加新的行为。
### 3.切点Pointcut
&emsp;&emsp;切点定义了通知所要织入的一个或多个连接点。如果说通知定义了切面的“**什么**”和“**何时**”,那么切点就定义了“**何处**”。通常使用明确的类和方法名称来指定切点或者利用正则表达式定义匹配的类和方法来指定这些切点。有些AOP框架允许我们创建动态的切点可以更具运行时的策略来决定是否应用通知。
### 4.切面Aspect
&emsp;&emsp;切面是通知和切点的结合。通知和切点定义了关于切面的全部内容,**是什么**,在**何时**、**何处**完成其功能。
### 5.引入
&emsp;&emsp;引入允许我们想现有的类添加新方法或属性。即在无需修改现有类的情况下让它们具有新的行为和状态。
### 6.织入
&emsp;&emsp;织入是将切面应用到目标对象来创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点可以进行织入。
- 编译期切面在目标类编译时被织入。这种方式需要特殊的编译期比如AspectJ的织入编译期
- 类加载期切面在目标类加载到JVM时被织入。这种方式需要特殊的加载器它可以在目标类被引入应用之前增强该目标类的字节码例如AspectJ5的**LTW**load-time weaving
- 运行期切面在应用运行的某个时刻被织入。一般情况下AOP容器会为目标对象动态创建一个代理对象
##三.Spring AOP
&emsp;&emsp;Spring在运行期通知对象通过在代理类中包裹切面Spring在运行期将切面织入到Spring管理的Bean中。代理类封装了目标类并拦截被通知的方法的调用再将调用转发给真正的目标Bean。由于Spring是基于动态代理所有Spring只支持方法连接点如果需要方法拦截之外的连接点拦截我们可以利用Aspect来协助SpringAOP。
&emsp;&emsp;Spring在运行期通知对象通过在代理类中包裹切面Spring在运行期将切面织入到Spring管理的Bean中。代理类封装了目标类并拦截被通知的方法的调用再将调用转发给真正的目标Bean。由于Spring是基于动态代理所有Spring只支持方法连接点如果需要方法拦截之外的连接点拦截我们可以利用Aspect来协助SpringAOP。
### 1、定义切点
&emsp;&emsp;在SpringAOP中需要使用AspectJ的切点表达式语言来定义切点。Spring只支持AspectJ的部分切点指示器如下表所示
| AspectJ指示器 | 描述 |
| ------------- | ------------------------------------------------------------ |
| arg() | 限制连接点匹配参数为指定类型的执行方法 |
| @args() | 限制连接点匹配参数由指定注解标注的执行方法 |
| execution() | 用于匹配是连接点的执行方法 |
| this() | 限制连接点匹配AOP代理的Bean引用为指导类型的类 |
| target() | 限制连接点匹配目标对象为指定类型的类 |
| @target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解 |
| within() | 限制连接点匹配指定的类型 |
| @within() | 限制连接点匹配指定注解所标注的类型当使用SpringAOP时方法定义在由指定的注解所标注的类里 |
| @annotation | 限制匹配带有指定注解连接点 |
| bean() | 使用Bean ID或Bean名称作为参数来限制切点只匹配特定的Bean |
&emsp;其中只有execution指示器是唯一的执行匹配其他都是限制匹配。因此execution指示器是
其中只有execution指示器是唯一的执行匹配其他都是限制匹配。因此execution指示器是我们在编写切点定义时最主要使用的指示器。
### 2、编写切点
&emsp;&emsp;假设我们要使用execution()指示器选择Hello类的sayHello()方法,表达式如下:
```java
execution(* com.test.Hello.sayHello(..))
```
方法表达式以*** **号开始,说明不管方法返回值的类型。然后指定全限定类名和方法名。对于方法参数列表,我们使用(**标识切点选择任意的sayHello()方法,无论方法入参是什么。
&emsp;&emsp;同时我们可以使用&&(and)||(or)(not)来连接指示器,如下所示:
```java
execution(* com.test.Hello.sayHello(..)) and !bean(xiaobu)
```
### 3、申明切面
&emsp;&emsp;在经典Spring AOP中使用ProxyFactoryBean非常复杂因此提供了申明式切面的选择在Spring的AOP配置命名空间中有如下配置元素
| AOP配置元素 | 描述 |
| ------------------------------ | ----------------------------------------------------------- |
| &lt;aop:advisor &gt; | 定义AOP通知器 |
| &lt;aop:after &gt; | 定义AOP后置通知无论被通知方法是否执行成功 |
| &lt;aop:after-returning &gt; | 定义AOP after-returning通知 |
| &lt;aop:after-throwing &gt; | 定义after-throwing |
| &lt;aop:around &gt; | 定义AOP环绕通知 |
| &lt;aop:aspect &gt; | 定义切面 |
| &lt;aop:aspectj-autoproxy &gt; | 启用@AspectJ注解驱动的切面 |
| &lt;aop:before &gt; | 定义AOP前置通知 |
| &lt;aop:config &gt; | 顶层的AOP配置元素。大多数的&lt;aop:* &gt;元素必须包含在其中 |
| &lt;aop:declare-parents &gt; | 为被通知的对象引入额外的接口,并透明的实现 |
| &lt;aop:pointcut &gt; | 定义切点 |
### 4、实现
假设有一个演员类`Actor`,演员类中有一个表演方法`perform()`,然后还有一个观众类`Audience`,这两个类都在包`com.example.springtest`Audience类主要方法如下
```java
public class Audience{
//搬凳子
public void takeSeats(){}
//欢呼
public void applaud(){}
//计时环绕通知需要一个ProceedingJoinPoint参数
public void timing(ProceedingJoinPoint joinPoint){
joinPoint.proceed();
}
//演砸了
public void demandRefund(){}
//测试带参数
public void dealString(String word){}
}
```
#### a、xml配置实现
&emsp;&emsp;首先将Audience配置到springIOC中:
```xml
<bean id="audience" class="com.example.springtest.Audience"/>
```
然后申明通知:
```xml
<aop:config>
<aop:aspect ref="audience">
<!-- 申明切点 -->
<aop:pointcut id="performance" expression="execution(* com.example.springtest.Performer.perform(..))"/>
<!-- 声明传递参数切点 -->
<aop:pointcut id="performanceStr" expression="execution(* com.example.springtest.Performer.performArg(String) and args(word))"/>
<!-- 前置通知 -->
<aop:before pointcut-ref="performance" method="takeSeats"/>
<!-- 执行成功通知 -->
<aop:after-returning pointcout-ref="performance" method="applaud"/>
<!-- 执行异常通知 -->
<aop:after-throwing pointcut-ref="performance" method="demandRefund"/>
<!-- 环绕通知 -->
<aop:around pointcut-ref="performance" method="timing"/>
<!-- 传递参数 -->
<aop:before pointcut-ref="performanceStr" arg-names="word" method="dealString"/>
</aop:aspect>
</aop:config>
```
#### b、注解实现
直接在Audience类上加注解Aspect注解并不能被spring自动发现并注册要么写到xml中要么使用@Aspectj注解或者加一个@Component注解),如下所示:
```java
@Aspect
public class Audience{
//定义切点
@Pointcut(execution(* com.example.springtest.Performer.perform(..)))
public void perform(){}
//定义带参数切点
@Pointcut(execution(* com.example.springtest.Performer.performArg(String) and args(word)))
public void performStr(String word){}
//搬凳子
@Before("perform()")
public void takeSeats(){}
//欢呼
@AfterReturning("perform()")
public void applaud(){}
//计时环绕通知需要一个ProceedingJoinPoint参数
@Around("perform()")
public void timing(ProceedingJoinPoint joinPoint){
joinPoint.proceed();
}
//演砸了
@AfterThrowing("perform()")
public void demandRefund(){}
//带参数
@Before("performStr(word)")
public void dealString(String word){}
}
```
#### c、通过切面引入新功能
&emsp;&emsp;既然可以用AOP为对象拥有的方法添加新功能那为什么不能为对象增加新的方法呢利用被称为**引入**的AOP概念切面可以为Spring Bean添加新的方法示例图如下
![引入](./picFolder/引入新功能.png)
当引入接口的方法被调用时代理将此调用委托给实现了新接口的某个其他对象。实际上Bean的实现被拆分到了多个类。
- xml引入需要使用&lt;aop:declare-parents &gt;元素:
```xml
<aop:aspect>
<aop:declare-parents types-matching="com.fxb.springtest.Performer+"
implement-interface="com.fxb.springtest.AddTestInterface"
default-impl="com.fxb.springtest.AddTestImpl"/>
</aop:aspect>
```
顾名思义\&lt;declare-parents&gt;声明了此切面所通知的Bean在它的对象层次结构中有了新的父类型。其中types-matching指定增强的类implement-interface指定实现新方法的接口default-imple指定实现了implement-interface接口的实现类也可以用delegate-ref来指定一个Bean的引用。
- 注解引入,通过`@DeclareParents`注解
```xml
@DeclareParents(value="com.fxb.springtest.Performer+",
defaultImpl=AddTestImpl.class)
public static AddTestInterface addTestInterface;
```
同xml实现一样注解也由三部分组成1、value属性相当于tpes-matching属性标识被增强的类2、defaultImpl等同于default-imple指定接口的实现类3、有@DeclareParents注解所标注的static属性指定了将被引入的接口

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,51 +0,0 @@
[id]:2018-08-13
[type]:javaee
[tag]:java,spring,springboot
&emsp;&emsp;前面的博客有说到spring boot搭建见另一篇博文其实那篇博文还没写现在来填个坑。我们使用spring initializr来构建idea和eclipse都支持这种方式构建过程类似这里以idea为例详细记录构建过程。
###1.选择spring initializr
![1532967570728](./picFolder/1532967570728.png)
next
#### 2.设置参数
![1532967772110](./picFolder/1532967772110.png)
next
#### 3.选择依赖
&emsp;&emsp;在这里选择spring boot版本和web依赖忽略sql的依赖如有需要[点击这里](f),单独将mybatis的整合)后面也可手动编辑pom文件修改增加删除依赖
![1532967938985](./picFolder/1532967938985.png)
这里我们选择web搭建一个简单的REST风格demo。然后next。
####4.设置项目存放地址
![1532968024509](./picFolder/1532968024509.png)
这样就成功构建了一个springboot项目。
#### 5.测试
&emsp;&emsp;现在新建一个controller包包下新建一个HelloController,创建之后项目目录结构如下:
![1532969025023](./picFolder/1532969025023.png)
HelloController代码如下
```java
@RestController
@RequestMapping("/home")
public class HelloController{
@GetMapping("/hello")
public String sayHello(){
return "hello";
}
}
```
然后运行项目访问localhost:8080/home/hello即可看到hello字符串。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

@ -1,257 +0,0 @@
[id]:2018-08-20
[type]:javaee
[tag]:java,spring,springsecurity,scurity
**说明springboot版本2.0.3**
##一、 介绍
&emsp;&emsp;Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean充分利用了Spring IoCDI控制反转Inversion of Control ,DI:Dependency Injection 依赖注入和AOP面向切面编程功能为应用系统提供声明式的安全访问控制功能减少了为企业系统安全控制编写大量重复代码的工作。
##二、 环境搭建
&emsp;&emsp;建立springboot2项目,加入security依赖,mybatis依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
```
数据库为传统的用户--角色--权限权限表记录了url和methodspringboot配置文件如下
```yml
mybatis:
type-aliases-package: com.example.demo.entity
server:
port: 8081
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 123456
http:
encoding:
charset: utf-8
enabled: true
```
springboot启动类中加入如下代码,设置路由匹配规则。
```java
@Override
protected void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(false) //设置路由是否后缀匹配,譬如/user能够匹配/user.,/user.aa
.setUseTrailingSlashMatch(false); //设置是否后缀路径匹配,比如/user能够匹配/user,/user/
}
```
## 三、 security配置
&emsp;&emsp;默认情况下security是无需任何自定义配置就可使用的我们不考虑这种方式直接讲如何个性化登录过程。
#### 1、 建立security配置文件,目前配置文件中还没有任何配置。
```java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
```
#### 2、 个性化登录security中的登录如下
![登录过程](./picFolder/pic1.png)
- security需要一个user的实体类实现`UserDetails`接口,该实体类最后与系统中用户的实体类分开,代码如下:
```java
public class SecurityUser implements UserDetails{
private static final long serialVersionUID = 1L;
private String password;
private String name;
List<GrantedAuthority> authorities;
public User(string name,string password) {
this.id = id;
this.password = password;
this.name = name;
this.age = age;
}
public void setAuthorities(List<GrantedAuthority> authorities) {
this.authorities = authorities;
}
@Override
public Collection<GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override //获取校验用户名
public String getUsername() {
return String.valueOf(this.id);
}
@Override //获取校验用密码
public String getPassword() {
return password;
}
@Override //账户是否未过期
public boolean isAccountNonExpired() {
// TODO Auto-generated method stub
return true;
}
@Override //账户是否未锁定
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return true;
}
@Override //帐户密码是否未过期,一般有的密码要求性高的系统会使用到,比较每隔一段时间就要求用户重置密码
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return true;
}
@Override //账户是否可用
public boolean isEnabled() {
// TODO Auto-generated method stub
return true;
}
}
```
- 编写了实体类还需要编写一个服务类SecurityService实现`UserDetailsService`接口重写loadByUsername方法通过这个方法根据用户名获取用户信息代码如下
```java
@Component
public class SecurityUserService implements UserDetailsService {
@Autowired
private JurisdictionMapper jurisdictionMapper;
@Autowired
private UserMapper userMapper;
private Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("登录用户id为{}",username);
int id = Integer.valueOf(username);
User user = userMapper.getById(id);
if(user==null) {
//抛出错误,用户不存在
throw new UsernameNotFoundException("用户名 "+username+"不存在");
}
//获取用户权限
List<GrantedAuthority> authorities = new ArrayList<>();
List<Jurisdiction> jurisdictions = jurisdictionMapper.selectByUserId(id);
for(Jurisdiction item : jurisdictions) {
GrantedAuthority authority = new MyGrantedAuthority(item.getMethod(),item.getUrl());
authorities.add(authority);
}
SecurityUser securityUser = new SecurityUser(user.getName(),user.getPassword(),authority):
user.setAuthorities(authorities);
return securityUser;
}
}
```
- 通常我们会对密码进行加密所有还要编写一个passwordencode类实现PasswordEncoder接口代码如下
```java
@Component
public class MyPasswordEncoder implements PasswordEncoder {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Override //不清楚除了在下面方法用到还有什么用处
public String encode(CharSequence rawPassword) {
return StringUtil.StringToMD5(rawPassword.toString());
}
//判断密码是否匹配
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(this.encode(rawPassword));
}
}
```
#### 3、 编辑配置文件
- 编写config Bean以使用上面定义的验证逻辑,securityUserService、myPasswordEncoder通过@Autowired引入
```java
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(securityUserService)
.passwordEncoder(myPasswordEncoder);
}
```
- 然后编写configure Bean和上一个不一样参数不同实现security验证逻辑,代码如下:
```java
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf() //跨站
.disable() //关闭跨站检测
.authorizeRequests()//验证策略策略链
.antMatchers("/public/**").permitAll()//无需验证路径
.antMatchers("/login").permitAll()//放行登录
.antMatchers(HttpMethod.GET, "/user").hasAuthority("getAllUser")//拥有权限才可访问
.antMatchers(HttpMethod.GET, "/user").hasAnyAuthority("1","2")//拥有任一权限即可访问
//角色类似hasRole(),hasAnyRole()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/public/unlogin") //未登录跳转页面,设置了authenticationentrypoint后无需设置未登录跳转页面
.loginProcessingUrl("/public/login")//处理登录post请求接口无需自己实现
.successForwardUrl("/success")//登录成功转发接口
.failureForwardUrl("/failed")//登录失败转发接口
.usernameParameter("id") //修改用户名的表单name默认为username
.passwordParameter("password")//修改密码的表单name默认为password
.and()
.logout()//自定义登出
.logoutUrl("/public/logout") //自定义登出api无需自己实现
.logoutSuccessUrl("public/logoutSuccess")
}
```
到这里便可实现security与springboot的基本整合。
## 四、实现记住我功能
#### 1、 建表
&emsp;&emsp;记住我功能需要数据库配合实现首先要在数据库建一张表用户保存cookie和用户名数据库建表语句如下不能做修改
```sql
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
)
```
#### 2、 编写rememberMeservice Bean
&emsp;&emsp;代码如下:
```java
@Bean
public RememberMeServices rememberMeServices(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
PersistentTokenBasedRememberMeServices rememberMeServices =
new PersistentTokenBasedRememberMeServices("INTERNAL_SECRET_KEY",securityUserService,jdbcTokenRepository);
//还可设置许多其他属性
rememberMeServices.setCookieName("kkkkk"); //客户端cookie名
return rememberMeServices;
}
```
dataSource为@Autowired引入
#### 3、 配置文件设置remember
&emsp;&emsp;在config(HttpSecurity http)中加入记住我功能
```java
.rememberMe()
.rememberMeServices(rememberMeServices())
.key("INTERNAL_SECRET_KEY")
```
在登录表单中设置remember-me即可实现记住我功能。

View File

@ -1,93 +0,0 @@
[id]:2018-08-21
[type]:javaee
[tag]:java,spring,springsecurity,scurity
&emsp;&emsp;紧接着上一篇上一篇中登录验证都由security帮助我们完成了如果我们想要增加一个验证码登录或者其它的自定义校验就没办法了因此这一篇讲解如何实现这个功能。
##一、 实现自定义登录校验类
&emsp;&emsp;继承UsernamePasswordAuthenticationFilter类来拓展登录校验代码如下
```java
public class MyUsernamePasswordAuthentication extends UsernamePasswordAuthenticationFilter{
private Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
//我们可以在这里进行额外的验证如果验证失败抛出继承AuthenticationException的自定义错误。
log.info("在这里进行验证码判断");
//只要最终的验证是账号密码形式就无需修改后续过程
return super.attemptAuthentication(request, response);
}
@Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
// TODO Auto-generated method stub
super.setAuthenticationManager(authenticationManager);
}
}
```
##二、 将自定义登录配置到security中
&emsp;&emsp;编写自定义登录过滤器后configure Bean修改为如下
```java
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf() //跨站
.disable() //关闭跨站检测
//自定义鉴权过程,无需下面设置
.authorizeRequests()//验证策略
.antMatchers("/public/**").permitAll()//无需验证路径
.antMatchers("/user/**").permitAll()
.antMatchers("/login").permitAll()//放行登录
.antMatchers(HttpMethod.GET, "/user").hasAuthority("getAllUser")//拥有权限才可访问
.antMatchers(HttpMethod.GET, "/user").hasAnyAuthority("1","2")//拥有任一权限即可访问
//角色类似hasRole(),hasAnyRole()
.anyRequest().authenticated()
.and()
//自定义异常处理
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)//未登录处理
.accessDeniedHandler(myAccessDeniedHandler)//权限不足处理
.and()
//加入自定义登录校验
.addFilterBefore(myUsernamePasswordAuthentication(),UsernamePasswordAuthenticationFilter.class)
.rememberMe()//默认放在内存中
.rememberMeServices(rememberMeServices())
.key("INTERNAL_SECRET_KEY")
// 重写usernamepasswordauthenticationFilter后下面的formLogin()设置将失效,需要手动设置到个性化过滤器中
// .and()
// .formLogin()
// .loginPage("/public/unlogin") //未登录跳转页面,设置了authenticationentrypoint后无需设置未登录跳转页面
// .loginProcessingUrl("/public/login")//登录api
// .successForwardUrl("/success")
// .failureForwardUrl("/failed")
// .usernameParameter("id")
// .passwordParameter("password")
// .failureHandler(myAuthFailedHandle) //登录失败处理
// .successHandler(myAuthSuccessHandle)//登录成功处理
// .usernameParameter("id")
.and()
.logout()//自定义登出
.logoutUrl("/public/logout")
.logoutSuccessUrl("public/logoutSuccess")
.logoutSuccessHandler(myLogoutSuccessHandle);
}
```
然后再编写Bean代码如下
```java
@Bean
public MyUsernamePasswordAuthentication myUsernamePasswordAuthentication(){
MyUsernamePasswordAuthentication myUsernamePasswordAuthentication = new MyUsernamePasswordAuthentication();
myUsernamePasswordAuthentication.setAuthenticationFailureHandler(myAuthFailedHandle); //设置登录失败处理类
myUsernamePasswordAuthentication.setAuthenticationSuccessHandler(myAuthSuccessHandle);//设置登录成功处理类
myUsernamePasswordAuthentication.setFilterProcessesUrl("/public/login");
myUsernamePasswordAuthentication.setRememberMeServices(rememberMeServices()); //设置记住我
myUsernamePasswordAuthentication.setUsernameParameter("id");
myUsernamePasswordAuthentication.setPasswordParameter("password");
return myUsernamePasswordAuthentication;
}
```
完成。

View File

@ -1,220 +0,0 @@
[id]:2018-08-22
[type]:javaee
[tag]:java,spring,springsecurity,scurity
&emsp;&emsp;这篇讲解如何自定义鉴权过程实现根据数据库查询出的url和method是否匹配当前请求的url和method来决定有没有权限。security鉴权过程如下
![鉴权流程](./picFolder/pic2.png)
##一、 重写metadataSource类
1. 编写MyGranteAuthority类让权限包含url和method两个部分。
```java
public class MyGrantedAuthority implements GrantedAuthority {
private String method;
private String url;
public MyGrantedAuthority(String method, String url) {
this.method = method;
this.url = url;
}
@Override
public String getAuthority() {
return url;
}
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
@Override
public boolean equals(Object obj) {
if(this==obj) return true;
if(obj==null||getClass()!= obj.getClass()) return false;
MyGrantedAuthority grantedAuthority = (MyGrantedAuthority)obj;
if(this.method.equals(grantedAuthority.getMethod())&&this.url.equals(grantedAuthority.getUrl()))
return true;
return false;
}
}
```
2. 编写MyConfigAttribute类实现ConfigAttribute接口代码如下
```java
public class MyConfigAttribute implements ConfigAttribute {
private HttpServletRequest httpServletRequest;
private MyGrantedAuthority myGrantedAuthority;
public MyConfigAttribute(HttpServletRequest httpServletRequest) {
this.httpServletRequest = httpServletRequest;
}
public MyConfigAttribute(HttpServletRequest httpServletRequest, MyGrantedAuthority myGrantedAuthority) {
this.httpServletRequest = httpServletRequest;
this.myGrantedAuthority = myGrantedAuthority;
}
public HttpServletRequest getHttpServletRequest() {
return httpServletRequest;
}
@Override
public String getAttribute() {
return myGrantedAuthority.getUrl();
}
public MyGrantedAuthority getMyGrantedAuthority() {
return myGrantedAuthority;
}
}
```
3. 编写MySecurityMetadataSource类获取当前url所需要的权限
```java
@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Autowired
private JurisdictionMapper jurisdictionMapper;
private List<Jurisdiction> jurisdictions;
private void loadResource() {
this.jurisdictions = jurisdictionMapper.selectAllPermission();
}
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if (jurisdictions == null) this.loadResource();
HttpServletRequest request = ((FilterInvocation) object).getRequest();
Set<ConfigAttribute> allConfigAttribute = new HashSet<>();
AntPathRequestMatcher matcher;
for (Jurisdiction jurisdiction : jurisdictions) {
//使用AntPathRequestMatcher比较可让url支持ant风格,例如/user/*/a
//*匹配一个或多个字符,**匹配任意字符或目录
matcher = new AntPathRequestMatcher(jurisdiction.getUrl(), jurisdiction.getMethod());
if (matcher.matches(request)) {
ConfigAttribute configAttribute = new MyConfigAttribute(request,new MyGrantedAuthority(jurisdiction.getMethod(),jurisdiction.getUrl()));
allConfigAttribute.add(configAttribute);
//这里是获取到一个权限就返回,根据校验规则也可获取多个然后返回
return allConfigAttribute;
}
}
//未匹配到,说明无需权限验证
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
```
##二、 编写MyAccessDecisionManager类
&emsp;&emsp;实现AccessDecisionManager接口以实现权限判断,直接return说明验证通过如不通过需要抛出对应错误代码如下
```java
@Component
public class MyAccessDecisionManager implements AccessDecisionManager{
private Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
//无需验证放行
if(configAttributes==null || configAttributes.size()==0)
return;
if(!authentication.isAuthenticated()){
throw new InsufficientAuthenticationException("未登录");
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for(ConfigAttribute attribute : configAttributes){
MyConfigAttribute urlConfigAttribute = (MyConfigAttribute)attribute;
for(GrantedAuthority authority: authorities){
MyGrantedAuthority myGrantedAuthority = (MyGrantedAuthority)authority;
if(urlConfigAttribute.getMyGrantedAuthority().equals(myGrantedAuthority))
return;
}
}
throw new AccessDeniedException("无权限");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
```
##三、 编写MyFilterSecurityInterceptor类
&emsp;&emsp;该类继承AbstractSecurityInterceptor类实现Filter接口,代码如下:
```java
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
//注入上面编写的两个类
@Autowired
private MySecurityMetadataSource mySecurityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
//这里进行权限验证
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.mySecurityMetadataSource;
}
}
```
## 四、 加入到security的过滤器链中
```java
.addFilterBefore(urlFilterSecurityInterceptor,FilterSecurityInterceptor.class)
```
完成

View File

@ -1,209 +0,0 @@
[id]:2018-08-25
[type]:javaee
[tag]:java,spring,websocket
<h3 id="#一、背景">一、背景</h3>
&emsp;&emsp;我们都知道http协议只能浏览器单方面向服务器发起请求获得响应服务器不能主动向浏览器推送消息。想要实现浏览器的主动推送有两种主流实现方式
- 轮询:缺点很多,但是实现简单
- websocket在浏览器和服务器之间建立tcp连接实现全双工通信
&emsp;&emsp;springboot使用websocket有两种方式一种是实现简单的websocket另外一种是实现**STOMP**协议。这一篇实现简单的websocketSTOMP下一篇在讲。
**注意如下都是针对使用springboot内置容器**
<h3 id="二、实现">二、实现</h3>
<h4 id="1、依赖引入">1、依赖引入</h4>
&emsp;&emsp;要使用websocket关键是`@ServerEndpoint`这个注解该注解是javaee标准中的注解,tomcat7及以上已经实现了,如果使用传统方法将war包部署到tomcat中只需要引入如下javaee标准依赖即可
```xml
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
```
如使用springboot内置容器,无需引入springboot已经做了包含。我们只需引入如下依赖即可
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>1.5.3.RELEASE</version>
<type>pom</type>
</dependency>
```
<h4 id="2、注入Bean">2、注入Bean</h4>
&emsp;&emsp;首先注入一个**ServerEndpointExporter**Bean,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint。代码如下
```java
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
```
<h4 id="3、申明endpoint">3、申明endpoint</h4>
&emsp;&emsp;建立**MyWebSocket.java**类在该类中处理websocket逻辑
```java
@ServerEndpoint(value = "/websocket") //接受websocket请求路径
@Component //注册到spring容器中
public class MyWebSocket {
//保存所有在线socket连接
private static Map<String,MyWebSocket> webSocketMap = new LinkedHashMap<>();
//记录当前在线数目
private static int count=0;
//当前连接每个websocket连入都会创建一个MyWebSocket实例
private Session session;
private Logger log = LoggerFactory.getLogger(this.getClass());
//处理连接建立
@OnOpen
public void onOpen(Session session){
this.session=session;
webSocketMap.put(session.getId(),this);
addCount();
log.info("新的连接加入:{}",session.getId());
}
//接受消息
@OnMessage
public void onMessage(String message,Session session){
log.info("收到客户端{}消息:{}",session.getId(),message);
try{
this.sendMessage("收到消息:"+message);
}catch (Exception e){
e.printStackTrace();
}
}
//处理错误
@OnError
public void onError(Throwable error,Session session){
log.info("发生错误{},{}",session.getId(),error.getMessage());
}
//处理连接关闭
@OnClose
public void onClose(){
webSocketMap.remove(this.session.getId());
reduceCount();
log.info("连接关闭:{}",this.session.getId());
}
//群发消息
//发送消息
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
//广播消息
public static void broadcast(){
MyWebSocket.webSocketMap.forEach((k,v)->{
try{
v.sendMessage("这是一条测试广播");
}catch (Exception e){
}
});
}
//获取在线连接数目
public static int getCount(){
return count;
}
//操作count使用synchronized确保线程安全
public static synchronized void addCount(){
MyWebSocket.count++;
}
public static synchronized void reduceCount(){
MyWebSocket.count--;
}
}
```
<h4 id="4、客户的实现">4、客户的实现</h4>
&emsp;&emsp;客户端使用h5原生websocket部分浏览器可能不支持。代码如下
```html
<html>
<head>
<title>websocket测试</title>
<meta charset="utf-8">
</head>
<body>
<button onclick="sendMessage()">测试</button>
<script>
let socket = new WebSocket("ws://localhost:8080/websocket");
socket.onerror = err => {
console.log(err);
};
socket.onopen = event => {
console.log(event);
};
socket.onmessage = mess => {
console.log(mess);
};
socket.onclose = () => {
console.log("连接关闭");
};
function sendMessage() {
if (socket.readyState === 1)
socket.send("这是一个测试数据");
else
alert("尚未建立websocket连接");
}
</script>
</body>
</html>
```
<h3 id="三、测试">三、测试</h3>
&emsp;&emsp;建立一个controller测试群发代码如下
```java
@RestController
public class HomeController {
@GetMapping("/broadcast")
public void broadcast(){
MyWebSocket.broadcast();
}
}
```
然后打开上面的html可以看到浏览器和服务器都输出连接成功的信息
```
浏览器:
Event {isTrusted: true, type: "open", target: WebSocket, currentTarget: WebSocket, eventPhase: 2, …}
服务端:
2018-08-01 14:05:34.727 INFO 12708 --- [nio-8080-exec-1] com.fxb.h5websocket.MyWebSocket : 新的连接加入0
```
点击测试按钮,可在服务端看到如下输出:
```
2018-08-01 15:00:34.644 INFO 12708 --- [nio-8080-exec-6] com.fxb.h5websocket.MyWebSocket : 收到客户端2消息这是一个测试数据
```
再次打开html页面这样就有两个websocket客户端然后在浏览器访问[localhost:8080/broadcast](localhost:8080/broadcast)测试群发功能,每个客户端都会输出如下信息:
```
MessageEvent {isTrusted: true, data: "这是一条测试广播", origin: "ws://localhost:8080", lastEventId: "", source: null, …}
```
<br/>
&emsp;&emsp;源码可在[github]()上下载记得点赞star哦

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View File

@ -1,366 +0,0 @@
[id]:2018-09-01
[type]:javaee
[tag]:java,spring,mysql,mybatis,xml
## 写在前面
刚毕业的第一份工作是java开发项目中需要用到mybatis特此记录学习过程这只是一个简单demomybatis用法很多不可能全部写出来有更复杂的需求建议查看mybatis的官方中文文档[点击跳转](http://www.mybatis.org/mybatis-3/zh/index.html)。下面时项目环境/版本。
- 开发工具IDEA
- jdk版本1.8
- springboot版本2.03
其他依赖版本见下面pom.xml:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mybatis-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mybatis-test</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--mybatis依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--alibaba连接池依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<!--分页依赖-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
```
## 1.创建项目
使用idea中的spring initializr生成maven项目项目命令为mybatis-test选择webmysqlmybatis依赖即可成功。详细过程不赘述如有需要学习springboot创建过程可参考[这篇文章]()。
然后依照上面的pom文件补齐缺少的依赖。接着创建包entityservice和mybatis映射文件夹mapper创建。为了方便配置将application.properties改成application.yml。由于我们时REST接口故不需要static和templates目录。修改完毕后的项目结构如下
![项目结构](./picFolder/pic1.png)
修改启动类,增加`@MapperScan("com.example.mybatistest.dao") `,以自动扫描dao目录避免每个dao都手动加`@Mapper`注解。代码如下:
```java
@SpringBootApplication
@MapperScan("com.example.mybatistest.dao")
public class MybatisTestApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisTestApplication.class, args);
}
}
```
修改application.yml,配置项目,代码如下:
```yml
mybatis:
#对应实体类路径
type-aliases-package: com.example.mybatistest.entity
#对应mapper映射文件路径
mapper-locations: classpath:mapper/*.xml
#pagehelper物理分页配置
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql
returnPageInfo: check
server:
port: 8081
spring:
datasource:
name: mysqlTest
type: com.alibaba.druid.pool.DruidDataSource
#druid连接池相关配置
druid:
#监控拦截统计的filters
filters: stat
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 123456
#配置初始化大小,最小,最大
initial-size: 1
min-idle: 1
max-active: 20
#获取连接等待超时时间
max-wait: 6000
#间隔多久检测一次需要关闭的空闲连接
time-between-eviction-runs-millis: 60000
#一个连接在池中的最小生存时间
min-evictable-idle-time-millis: 300000
#打开PSCache并指定每个连接上PSCache的大小。oracle设置为truemysql设置为false。分库分表设置较多推荐设置
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
http:
encoding:
charset: utf-8
enabled: true
```
## 2.编写代码
首先创建数据表sql语句如下
```sql
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`age` tinyint(4) NOT NULL DEFAULT '0',
`password` varchar(255) NOT NULL DEFAULT '123456',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
```
然后在entity包中创建实体类User.java
```java
public class User {
private int id;
private String name;
private int age;
private String password;
public User(int id, String name, int age, String password) {
this.id = id;
this.name = name;
this.age = age;
this.password = password;
}
public User(){}
//getter setter自行添加
}
```
在dao包下创建UserDao.java
```java
public interface UserDao {
//插入用户
int insert(User user);
//根据id查询
User selectById(String id);
//查询所有
List<User> selectAll();
}
```
在mapper文件夹下创建UserMapper.xml,具体的xml编写方法查看文首的官方文档。
```xml
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.mybatistest.dao.UserDao">
<sql id="BASE_TABLE">
user
</sql>
<sql id="BASE_COLUMN">
id,name,age,password
</sql>
<insert id="insert" parameterType="com.example.mybatistest.entity.User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO <include refid="BASE_TABLE"/>
<trim prefix="(" suffix=")" suffixOverrides=",">
name,password,
<if test="age!=null">
age
</if>
</trim>
<trim prefix=" VALUE(" suffix=")" suffixOverrides=",">
#{name,jdbcType=VARCHAR},#{password},
<if test="age!=null">
#{age}
</if>
</trim>
</insert>
<select id="selectById" resultType="com.example.mybatistest.entity.User">
select
<include refid="BASE_COLUMN"/>
from
<include refid="BASE_TABLE"/>
where id=#{id}
</select>
<select id="selectAll" resultType="com.example.mybatistest.entity.User">
select
<include refid="BASE_COLUMN"/>
from
<include refid="BASE_TABLE"/>
</select>
</mapper>
```
至此使用mybatis的代码编写完了之后要用时调用dao接口中的方法即可。
## 3.测试
我们通过编写servicecontroller然后使用postman进行测试。
首先编写UserService.java,代码如下:
```java
@Component
public class UserService {
@Autowired
private UserDao userDao;
public User getByUserId(String id){
return userDao.selectById(id);
}
//获取全部用户
public List<User> getAll(){
return userDao.selectAll();
}
//测试分页
public PageInfo<User> getAll(int pageNum,int pageSize){
PageHelper.startPage(pageNum,pageSize);
List<User> users = userDao.selectAll();
System.out.println(users.size());
PageInfo<User> result = new PageInfo<>(users);
return result;
}
public int insert(User user){
return userDao.insert(user);
}
}
```
编写UserController.java
```java
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/{userId}")
public User getUser(@PathVariable String userId){
return userService.getByUserId(userId);
}
@GetMapping("/user")
public List<User> getAll(){
return userService.getAll();
}
@GetMapping("/user/page/{pageNum}")
public Object getPage(@PathVariable int pageNum,
@RequestParam(name = "pageSize",required = false,defaultValue = "10") int pageSize){
return userService.getAll(pageNum,pageSize);
}
@PostMapping("/user")
public Object addOne(User user){
userService.insert(user);
return user;
}
}
```
启动项目通过postman进行请求测试测试结果如下
- 插入数据:
![插入](./picFolder/pic2.png)
- 查询数据
![查询](./picFolder/pic3.png)
- 分页查询
![分页查询](./picFolder/pic4.png)
## 4.注解编写sql
上面使用的是xml方式编写sql代码其实mybatis也支持在注解中编写sql这样可以避免编写复杂的xml查询文件但同时也将sql语句耦合到了代码中也不易实现复杂查询因此多用于简单sql语句的编写。
要使用注解首先将applicaton.yml配置文件中的`mapper-locations: classpath:mapper/*.xml`注释掉。然后在UserDao.java中加入sql注解代码如下
```java
public interface UserDao {
//插入用户
@Insert("insert into user(name,age,password) value(#{name},#{age},#{password})")
@Options(useGeneratedKeys=true,keyColumn="id",keyProperty="id")
int insert(User user);
//根据id查询
@Select("select * from user where id=#{id}")
User selectById(String id);
//查询所有
@Select("select * from user")
List<User> selectAll();
}
```
然后重新启动项目测试,测试结果跟上面完全一样。
```
如果对你有帮助记得点赞、收藏哦!
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@ -1,117 +0,0 @@
[id]:2018-09-05
[type]:javaee
[tag]:java,spring,springboot,activemq
**说明acitveMQ版本为5.9.1springboot版本为2.0.3**<br/>
## 一. 下载安装windows
&emsp;&emsp;官方下载地址:[点我跳转](http://activemq.apache.org/download-archives.html),选择windows安装包下载,然后解压解压后运行bin目录下的**activemq.bat**启动服务,无报错即可启动成功。默认管理地址为:[localhost:8161/admin](localhost:8161/admin),默认管理员账号密码为**admin**/**admin**。
## 二. springboot整合
### &emsp;1. 创建springboot项目
&emsp;&emsp;创建springboot web项目加入spring-boot-starter-activemq依赖。
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
```
&emsp;&emsp;然后编辑配合文件加上一个配置61616为activeMQ的默认端口暂时不做其他配置使用默认值。
```yml
spring:
activemq:
broker-url: tcp://localhost:61616
```
### &emsp;2. 创建生产者消费者
&emsp;&emsp;springboot中activeMQ的默认配置为**生产-消费者模式**,还有一种模式为**发布-订阅模式**后面再讲。项目目录如下:
![项目目录](./picFolder/pic1.png)
&emsp;&emsp;首先编写配置类Config.java代码如下
```java
@Configuration
public class Config {
@Bean(name = "queue2")
public Queue queue2(){
return new ActiveMQQueue("active.queue2");
}
@Bean(name = "queue1")
public Queue queue1(){
return new ActiveMQQueue("active.queue1");
}
}
```
上面的代码建立了两个消息队列queue1queue2,分别由queue1和queue2这两个Bean注入到Spring容器中。程序运行后会在activeMQ的管理页面->queue中看到如下
![队列](./picFolder/pic2.png)
&emsp;&emsp;生产者Producer.java代码如下
```java
@RestController
public class Producer {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Autowired()
@Qualifier("queue2")
private Queue queue2;
@Autowired()
@Qualifier("queue1")
private Queue queue1;
@GetMapping("/queue2")
public void sendMessage1(String message){
jmsMessagingTemplate.convertAndSend(queue2,"I'm from queue2:"+message);
}
@GetMapping("/queue1")
public void sendMessage2(String message){
jmsMessagingTemplate.convertAndSend(queue1,"I'm from queue1:"+message);
}
}
```
上面的类创建了两个GET接口访问这两个接口分别向queue1和queue2中发送消息。
消费者Comsumer.java代码如下
```java
@Component //将该类注解到Spring 容器中
public class Comsumer {
//接受消息队列1消息
@JmsListener(destination = "active.queue1") //监听active.queue1消息队列
public void readActiveQueue11(String message){
System.out.println(1+message);
}
//接受消息队列1消息
@JmsListener(destination = "active.queue1")
public void readActiveQueue12(String message){
System.out.println(2+message);
}
//接受消息队列2消息
@JmsListener(destination = "active.queue2")
public void readActiveQueue21(String message){
System.out.println(1+message);
}
//接受消息队列2消息
@JmsListener(destination = "active.queue2")
public void readActiveQueue22(String message){
System.out.println(2+message);
}
}
```
上面的代码定义了4个消费者每两个消费一个消息队列。
## &emsp;3. 运行
&emsp;&emsp;启动项目后分别向/queue1?message=niihao,/queue2?message=nihaoa发送http请求然后我们可以在控制台中看到如下输出
```
2I'm from queue2:nihaoa
1I'm from queue2:nihaoa
2I'm from queue1:nihao
1I'm from queue1:nihao
```
消息都成功被消费者消费,从打印结果也可看出生产者消费者的一个特点:一个消息只会被一个消费者消费。同时在管理页面中可以看到:
![运行结果](./picFolder/pic3.png)
每个消息队列有两个消费者队列进入了三个消息出了三个消息说明消息都被消费掉了如果注释掉消费者代码再次运行然后发送消息就会发现MessagesEnqueued数量大于MessagesDequeued然后再让消费者上线会立即消费掉队列中的消息。

View File

@ -1,158 +0,0 @@
[id]:2018-09-06
[type]:javaee
[tag]:java,spring,activemq
&emsp;&emsp;单个MQ节点总是不可靠的一旦该节点出现故障MQ服务就不可用了势必会产生较大的损失。这里记录activeMQ如何开启主从备份一旦master主节点故障slave从节点立即提供服务实现原理是运行多个MQ使用同一个持久化数据源这里以jdbc数据源为例。同一时间只有一个节点节点A能够抢到数据库的表锁其他节点进入阻塞状态一旦A发生错误崩溃其他节点就会重新获取表锁获取到锁的节点成为master其他节点为slave如果节点A重新启动也将成为slave。
主从备份解决了单节点故障的问题但是同一时间提供服务的只有一个master显然是不能面对数据量的增长所以需要一种横向拓展的集群方式来解决面临的问题。
### 一、activeMQ设置
#### 1、平台版本说明
- 平台windows
- activeMQ版本5.9.1[下载地址](https://www.apache.org/dist/activemq/5.9.1/apache-activemq-5.9.1-bin.zip.asc)
- jdk版本1.8
#### 2、下载jdbc依赖
&emsp;&emsp;下载下面三个依赖包放到activeMQ安装目录下的lib文件夹中。
[mysql驱动](http://central.maven.org/maven2/mysql/mysql-connector-java/5.1.38/mysql-connector-java-5.1.38.jar)
[dhcp依赖](http://central.maven.org/maven2/org/apache/commons/commons-dbcp2/2.1.1/commons-dbcp2-2.1.1.jar)
[commons-pool2依赖](http://maven.aliyun.com/nexus/service/local/artifact/maven/redirect?r=jcenter&g=org.apache.commons&a=commons-pool2&v=2.6.0&e=jar)
###二、主从备份
####1、修改jettty
&emsp;&emsp;首先修改conf->jetty.xml这里是修改activemq的web管理端口管理界面账号密码默认为admin/admin
```xml
<bean id="jettyPort" class="org.apache.activemq.web.WebConsolePort" init-method="start">
<!-- the default port number for the web console -->
<property name="port" value="8161"/>
</bean>
```
####2、修改activemq.xml
&emsp;&emsp;然后修改conf->activemq.xml
- 设置连接方式
默认是下面五种连接方式都打开这里我们只要tcp把其他的都注释掉然后在这里设置activemq的服务端口可以看到每种连接方式都对应一个端口。
```xml
<transportConnectors>
<!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB -->
<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
<!-- <transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/> -->
</transportConnectors>
```
- 设置jdbc数据库
mysql数据库中创建activemq库`broker`标签的下面也就是根标签`beans`的下一级创建一个bean节点内容如下
```xml
<bean id="mysql-qs" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/activemq?relaxAutoCommit=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
```
- 设置数据源
首先修改broker节点设置name和persistent(默认为true),也可不做修改,修改后如下:
```xml
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="mq1" persistent="true" dataDirectory="${activemq.data}">
```
然后设置持久化方式,使用到我们之前设置的mysql-qs
```xml
<persistenceAdapter>
<!-- <kahaDB directory="${activemq.data}/kahadb"/> -->
<jdbcPersistenceAdapter dataDirectory="${activemq.base}/activemq-data" dataSource="#mysql-qs"/>
</persistenceAdapter>
```
#### 3、启动
&emsp;&emsp;设置完毕后启动activemq双击bin中的acitveMQ.jar),启动完成后可以看到如下日志信息:
```verilog
INFO | Using a separate dataSource for locking: org.apache.commons.dbcp2.BasicDataSource@179ece50
INFO | Attempting to acquire the exclusive lock to become the Master broker
INFO | Becoming the master on dataSource: org.apache.commons.dbcp2.BasicDataSource@179ece50
```
接着我们修改一下tcp服务端口改为61617然后重新启动日志信息如下
```verilog
INFO | Using a separate dataSource for locking: org.apache.commons.dbcp2.BasicDataSource@179ece50
INFO | Attempting to acquire the exclusive lock to become the Master broker
INFO | Failed to acquire lock. Sleeping for 10000 milli(s) before trying again...
INFO | Failed to acquire lock. Sleeping for 10000 milli(s) before trying again...
INFO | Failed to acquire lock. Sleeping for 10000 milli(s) before trying again...
INFO | Failed to acquire lock. Sleeping for 10000 milli(s) before trying again...
```
可以看到从节点一直在尝试获取表锁成为主节点,这样一旦主节点失效,从节点能够立刻取代主节点提供服务。这样我们便实现了主从备份。
### 三、负载均衡
&emsp;&emsp;activemq可以实现多个mq之间进行路由假设有两个mq分别为brokerA和brokerB当一条消息发送到brokerA的队列test中有一个消费者连上了brokerB并且想要获取test队列brokerA中的test队列就会路由到brokerB上。
&emsp;&emsp;&emsp;开启负载均衡需要设置`networkConnectors`节点,静态路由配置如下:
```xml
<networkConnectors>
<networkConnector uri="static:failover://(tcp://localhost:61616,tcp://localhost:61617)" duplex="false"/>
</networkConnectors>
```
brokerA和brokerB都要设置该配置以连上对方。
### 四、测试
####1、建立mq
&emsp;&emsp;组建两组broker每组做主从配置。
- brokerA
- 主设置web管理端口8761,设置mq名称`mq`,设置数据库地址为activemq设置tcp服务端口61616设置负载均衡静态路由`static:failover://(tcp://localhost:61618,tcp://localhost:61619)`,然后启动
- 从上面的基础上修改tcp服务端口为61617,然后启动
- brokerB:
- 主设置web管理端口8762设置mq名称`mq1`,设置数据库地址activemq1设置tcp服务端口61618设置负载均衡静态路由`static:failover://(tcp://localhost:61616,tcp://localhost:61617)`,然后启动
- 从上面的基础上修改tcp服务端口为61619然后启动
#### 2、springboot测试
&emsp;&emsp;&emsp;沿用上一篇的项目修改配置文件的broker-url为`failover:(tcp://localhost:61616,tcp://localhost:61617,tcp://localhost:61618,tcp://localhost:61619)`,然后启动项目访问会在控制台看到如下日志:
```java
2018-07-31 15:09:25.076 INFO 12780 --- [ActiveMQ Task-1] o.a.a.t.failover.FailoverTransport : Successfully connected to tcp://localhost:61618
1I'm from queue1:hello
2018-07-31 15:09:26.599 INFO 12780 --- [ActiveMQ Task-1] o.a.a.t.failover.FailoverTransport : Successfully connected to tcp://localhost:61618
2I'm from queue1:hello
2018-07-31 15:09:29.002 INFO 12780 --- [ActiveMQ Task-1] o.a.a.t.failover.FailoverTransport : Successfully connected to tcp://localhost:61616
1I'm from queue1:hello
2018-07-31 15:09:34.931 INFO 12780 --- [ActiveMQ Task-1] o.a.a.t.failover.FailoverTransport : Successfully connected to tcp://localhost:61618
2I'm from queue1:hello
```
证明负载均衡成功。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@ -1,315 +0,0 @@
[id]:2018-09-10
[type]:javaee
[tag]:java,spring,springboot,mybatis,读写分离
&emsp;&emsp;近日工作任务较轻,有空学习学习技术,遂来研究如果实现读写分离。这里用博客记录下过程,一方面可备日后查看,同时也能分享给大家(网上的资料真的大都是抄来抄去,,还不带格式的,看的真心难受)。
[完整代码](https://github.com/FleyX/demo-project/tree/master/dxfl)
## 1、背景
&emsp;&emsp;一个项目中数据库最基础同时也是最主流的是单机数据库,读写都在一个库中。当用户逐渐增多,单机数据库无法满足性能要求时,就会进行读写分离改造(适用于读多写少),写操作一个库,读操作多个库,通常会做一个数据库集群,开启主从备份,一主多从,以提高读取性能。当用户更多读写分离也无法满足时,就需要分布式数据库了(可能以后会学习怎么弄)。
&emsp;&emsp;正常情况下读写分离的实现首先要做一个一主多从的数据库集群同时还需要进行数据同步。这一篇记录如何用mysql搭建一个一主多次的配置下一篇记录代码层面如何实现读写分离。
## 2、搭建一主多从数据库集群
&emsp;&emsp;主从备份需要多台虚拟机我是用wmware完整克隆多个实例注意直接克隆的虚拟机会导致每个数据库的uuid相同需要修改为不同的uuid。修改方法参考这个[点击跳转](https://blog.csdn.net/pratise/article/details/80413198)。
- 主库配置
主数据库master中新建一个用户用于从数据库slave读取主数据库二进制日志sql语句如下
```sql
mysql> CREATE USER 'repl'@'%' IDENTIFIED BY '123456';#创建用户
mysql> GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';#分配权限
mysql>flush privileges; #刷新权限
```
同时修改mysql配置文件开启二进制日志新增部分如下
```sql
[mysqld]
server-id=1
log-bin=master-bin
log-bin-index=master-bin.index
```
然后重启数据库,使用`show master status;`语句查看主库状态,如下所示:
![主库状态](./picFolder/pic1.png)
- 从库配置
同样先新增几行配置:
```sql
[mysqld]
server-id=2
relay-log-index=slave-relay-bin.index
relay-log=slave-relay-bin
```
然后重启数据库,使用如下语句连接主库:
```sql
CHANGE MASTER TO
MASTER_HOST='192.168.226.5',
MASTER_USER='root',
MASTER_PASSWORD='123456',
MASTER_LOG_FILE='master-bin.000003',
MASTER_LOG_POS=154;
```
接着运行`start slave;`开启备份,正常情况如下图所示Slave_IO_Running和Slave_SQL_Running都为yes。
![1536223020742](./picFolder/pic2.png)
可以用这个步骤开启多个从库。
&emsp;&emsp;默认情况下备份是主库的全部操作都会备份到从库,实际可能需要忽略某些库,可以在主库中增加如下配置:
```sql
# 不同步哪些数据库
binlog-ignore-db = mysql
binlog-ignore-db = test
binlog-ignore-db = information_schema
# 只同步哪些数据库,除此之外,其他不同步
binlog-do-db = game
```
## 3、代码层面进行读写分离
&emsp;&emsp;代码环境是springboot+mybatis+druib连接池。想要读写分离就需要配置多个数据源在进行写操作是选择写的数据源读操作时选择读的数据源。其中有两个关键点
- 如何切换数据源
- 如何根据不同的方法选择正确的数据源
### 1)、如何切换数据源
&emsp;&emsp;通常用springboot时都是使用它的默认配置只需要在配置文件中定义好连接属性就行了但是现在我们需要自己来配置了spring是支持多数据源的多个datasource放在一个HashMap`TargetDataSource`中,通过`dertermineCurrentLookupKey`获取key来觉定要使用哪个数据源。因此我们的目标就很明确了建立多个datasource放到TargetDataSource中同时重写dertermineCurrentLookupKey方法来决定使用哪个key。
### 2)、如何选择数据源
&emsp;&emsp;事务一般是注解在Service层的因此在开始这个service方法调用时要确定数据源有什么通用方法能够在开始执行一个方法前做操作呢相信你已经想到了那就是**切面 **。怎么切有两种办法:
- 注解式,定义一个只读注解,被该数据标注的方法使用读库
- 方法名根据方法名写切点比如getXXX用读库setXXX用写库
### 3)、代码编写
#### a、编写配置文件配置两个数据源信息
&emsp;&emsp;只有必填信息,其他都有默认设置
```yml
mysql:
datasource:
#读库数目
num: 1
type-aliases-package: com.example.dxfl.dao
mapper-locations: classpath:/mapper/*.xml
config-location: classpath:/mybatis-config.xml
write:
url: jdbc:mysql://192.168.226.5:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
read:
url: jdbc:mysql://192.168.226.6:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
```
#### b、编写DbContextHolder类
&emsp;&emsp;这个类用来设置数据库类别其中有一个ThreadLocal用来保存每个线程的是使用读库还是写库。代码如下
```java
/**
* Description 这里切换读/写模式
* 原理是利用ThreadLocal保存当前线程是否处于读模式通过开始READ_ONLY注解在开始操作前设置模式为读模式
* 操作结束后清除该数据,避免内存泄漏,同时也为了后续在该线程进行写操作时任然为读模式
* @author fxb
* @date 2018-08-31
*/
public class DbContextHolder {
private static Logger log = LoggerFactory.getLogger(DbContextHolder.class);
public static final String WRITE = "write";
public static final String READ = "read";
private static ThreadLocal<String> contextHolder= new ThreadLocal<>();
public static void setDbType(String dbType) {
if (dbType == null) {
log.error("dbType为空");
throw new NullPointerException();
}
log.info("设置dbType为{}",dbType);
contextHolder.set(dbType);
}
public static String getDbType() {
return contextHolder.get() == null ? WRITE : contextHolder.get();
}
public static void clearDbType() {
contextHolder.remove();
}
}
```
#### c、重写determineCurrentLookupKey方法
&emsp;&emsp;spring在开始进行数据库操作时会通过这个方法来决定使用哪个数据库因此我们在这里调用上面DbContextHolder类的`getDbType()`方法获取当前操作类别,同时可进行读库的负载均衡,代码如下:
```java
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {
@Value("${mysql.datasource.num}")
private int num;
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DbContextHolder.getDbType();
if (typeKey == DbContextHolder.WRITE) {
log.info("使用了写库");
return typeKey;
}
//使用随机数决定使用哪个读库
int sum = NumberUtil.getRandom(1, num);
log.info("使用了读库{}", sum);
return DbContextHolder.READ + sum;
}
}
```
#### d、编写配置类
&emsp;&emsp;由于要进行读写分离不能再用springboot的默认配置我们需要手动来进行配置。首先生成数据源使用@ConfigurProperties自动生成数据源
```java
/**
* 写数据源
*
* @Primary 标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean 优先被考虑。
* 多数据源配置的时候注意,必须要有一个主数据源,用 @Primary 标志该 Bean
*/
@Primary
@Bean
@ConfigurationProperties(prefix = "mysql.datasource.write")
public DataSource writeDataSource() {
return new DruidDataSource();
}
```
读数据源类似注意有多少个读库就要设置多少个读数据源Bean名为read+序号。
&emsp;&emsp;然后设置数据源使用的是我们之前写的MyAbstractRoutingDataSource类
```java
/**
* 设置数据源路由通过该类中的determineCurrentLookupKey决定使用哪个数据源
*/
@Bean
public AbstractRoutingDataSource routingDataSource() {
MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>(2);
targetDataSources.put(DbContextHolder.WRITE, writeDataSource());
targetDataSources.put(DbContextHolder.READ+"1", read1());
proxy.setDefaultTargetDataSource(writeDataSource());
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
```
&emsp;&emsp;接着需要设置sqlSessionFactory
```java
/**
* 多数据源需要自己设置sqlSessionFactory
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(routingDataSource());
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// 实体类对应的位置
bean.setTypeAliasesPackage(typeAliasesPackage);
// mybatis的XML的配置
bean.setMapperLocations(resolver.getResources(mapperLocation));
bean.setConfigLocation(resolver.getResource(configLocation));
return bean.getObject();
}
```
&emsp;&emsp;最后还得配置下事务,否则事务不生效
```java
/**
* 设置事务,事务需要知道当前使用的是哪个数据源才能进行事务处理
*/
@Bean
public DataSourceTransactionManager dataSourceTransactionManager() {
return new DataSourceTransactionManager(routingDataSource());
}
```
### 4)、选择数据源
&emsp;&emsp;多数据源配置好了,但是代码层面如何选择选择数据源呢?这里介绍两种办法:
#### a、注解式
&emsp;&emsp;首先定义一个只读注解被这个注解方法使用读库其他使用写库如果项目是中途改造成读写分离可使用这个方法无需修改业务代码只要在只读的service方法上加一个注解即可。
```java
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}
```
&emsp;&emsp;然后写一个切面来切换数据使用哪种数据源重写getOrder保证本切面优先级高于事务切面优先级在启动类加上`@EnableTransactionManagement(order = 10) `,为了代码如下:
```java
@Aspect
@Component
public class ReadOnlyInterceptor implements Ordered {
private static final Logger log= LoggerFactory.getLogger(ReadOnlyInterceptor.class);
@Around("@annotation(readOnly)")
public Object setRead(ProceedingJoinPoint joinPoint,ReadOnly readOnly) throws Throwable{
try{
DbContextHolder.setDbType(DbContextHolder.READ);
return joinPoint.proceed();
}finally {
//清楚DbType一方面为了避免内存泄漏更重要的是避免对后续在本线程上执行的操作产生影响
DbContextHolder.clearDbType();
log.info("清除threadLocal");
}
}
@Override
public int getOrder() {
return 0;
}
}
```
#### b、方法名式
&emsp;&emsp;这种方法不许要注解,但是需要事务名称按一定规则编写,然后通过切面来设置数据库类别,比如`setXXX`设置为写、`getXXX`设置为读,代码我就不写了,应该都知道怎么写。
### 4、测试
&emsp;&emsp;编写好代码来试试结果如何,下面是运行截图:
![1536312274474](./picFolder/pic3.png)
&emsp;&emsp;断断续续写了好几天终于是写完了如果有帮助到你欢迎star哦这里是完整代码地址[点击跳转](https://github.com/FleyX/demo-project/tree/master/dxfl)

View File

@ -1,181 +0,0 @@
[id]:2018-09-22
[type]:java
[tag]:java,reflect,excel,hssfworksheet
## 一、背景
&emsp;&emsp;最近在java上做了一个EXCEL的导出功能写了一个通用类在这里分享分享该类支持多sheet且无需手动进行复杂的类型转换只需提供三个参数即可
- `fileName`
excel文件名
- `HasMap<String,List<?>> data`
具体的数据每个List代表一张表的数据表示可为任意的自定义对象
- `LinkedHashMap<String,String[][]> headers`
`Stirng`代表sheet名。每个`String[][] `代表一个sheet的定义举个例子如下
```java
String[][] header = {
{"field1","参数1"}
{"field2","参数2"}
{"field3","参数3"}
}
```
其中的field1field2field3为对象中的属性名参数1参数2参数3为列名实际上这个指定了列的名称和这个列用到数据对象的哪个属性。
## 二、怎么用
&emsp;&emsp;以一个例子来说明怎么用假设有两个类A和B定义如下
```java
public class A{
private String name;
private String address;
}
public class B{
private int id;
private double sum;
private String cat;
}
```
现在我们通过查询数据库获得了A和B的两个列表
```java
List<A> dataA = .....;
List<B> dataB = .....;
```
我们将这两个导出到excel中首先需要定义sheet
```java
String[][] sheetA = {
{"name","姓名"}
,{"address","住址"}
}
String[][] sheetB = {
{"id","ID"}
,{"sum","余额"}
,{"cat","猫的名字"}
}
```
然后将数据汇总构造一个ExcelUtil
```java
String fileName = "测试Excel";
HashMap<String,List<?>> data = new HashMap<>();
//ASheet为表名后面headers里的key要跟这里一致
data.put("ASheet",dataA);
data.put("BSheet",dataB);
LinkedHashMap<String,String[][]> headers = new LinkedHashMap<>();
headers.put("ASheet",sheetA);
headers.put("BSheet",sheetB);
ExcelUtil excelUtil = new ExcelUtil(fileName,data,headers);
//获取表格对象
HSSFWorkbook workbook = excelUtil.createExcel();
//这里内置了一个写到response的方法判断浏览器类型设置合适的参数如果想写到文件也是类似的
workbook.writeToResponse(workbook,request,response);
```
当然通常数据是通过数据库查询的,这里为了演示方便没有从数据库查找。
## 三、实现原理
&emsp;&emsp;这里简单说明下实现过程,从调用`createExcel()`这里开始
####1、遍历headers创建sheet
```java
public HSSFWorkbook createExcel() throws Exception {
try {
HSSFWorkbook workbook = new HSSFWorkbook();
//遍历headers创建表格
for (String key : headers.keySet()) {
this.createSheet(workbook, key, headers.get(key), this.data.get(key));
}
return workbook;
} catch (Exception e) {
log.error("创建表格失败:{}", e.getMessage());
throw e;
}
}
```
将workbooksheet名表头数据行数据传入crateSheet方法中创建sheet。
#### 2、创建表头
&emsp;&emsp;表头也就是一个表格的第一行,通常用来对列进行说明
```java
HSSFSheet sheet = workbook.createSheet(sheetName);
// 列数
int cellNum = header.length;
// 单元行,单元格
HSSFRow row;
HSSFCell cell;
// 表头单元格样式
HSSFCellStyle columnTopStyle = this.getColumnTopStyle(workbook);
// 设置表头
row = sheet.createRow(0);
for (int i = 0; i < cellNum; i++) {
cell = row.createCell(i);
cell.setCellStyle(columnTopStyle);
String str = header[i][1];
cell.setCellValue(str);
// 设置列宽为表头的文字宽度+6个半角符号宽度
sheet.setColumnWidth(i, (str.getBytes("utf-8").length + 6) * 256);
}
```
#### 3、插入行数据
&emsp;&emsp;这里是最重要的部分首先通过数据的类对象获取它的反射属性Field类然后将属性名和Field做一个hash映射避免循环查找提高插入速度接着通过一个switch语句根据属性类别设值主要代码如下
```java
/**
* 设置单元格,根据fieldName获取对应的Field类使用反射得到值
*
* @param cell 单元格实例
* @param obj 存有属性的对象实例
* @param fieldMap 属性名与Field的映射
* @param fieldName 属性名
*/
private void setCell(HSSFCell cell, Object obj, Map<String, Field> fieldMap, String fieldName) throws Exception {
//获取该属性的Field对象
Field field = fieldMap.get(fieldName);
//通过反射获取属性的值,由于不能确定该值的类型,用下面的判断语句进行合适的转型
Object value = field.get(obj);
if (value == null) {
cell.setCellValue("");
} else {
switch (field.getGenericType().getTypeName()) {
case "java.lang.String":
cell.setCellValue((String) value);
break;
case "java.lang.Integer":
case "int":
cell.setCellValue((int) value);
break;
case "java.lang.Double":
case "double":
cell.setCellValue((double) value);
break;
case "java.util.Date":
cell.setCellValue(this.dateFormat.format((Date) value));
break;
default:
cell.setCellValue(obj.toString());
}
}
}
```
完整代码可以到github上查看下载这里就不列出来了。
github地址[点击跳转]()