Spring Cloud + Vue前后端分离-第5章 单表管理功能前后端开发
完成单表的增删改查
控台单表增删改查的前后端开发,重点学习前后端数据交互,vue ajax库axios的使用等
通用组件开发:分页、确认框、提示框、等待框等
常用的公共组件:确认框、提示框、等待框,统一日志拦截器等。使用vue自定义组件制作分页组件,mybatis分页插件pagehelper的使用等
5-1 大章列表查询功能开发1
增加maven子项目business
1.增加business模块,并增加初始启动代码
Shift+F6重命名。重命名也是一种重构,会将所有引用到的地方都一起改名,甚至是注释掉的代码也会一起改掉
application.properties
spring.application.name=business
server.servlet.context-path=/business
server.port=9002
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
大章表设计及持久层代码生成
将sql脚本和代码放一起的好处是,可以通用git提交记录来查看sql的变更记录,方便追溯
一般的表结构设计,都会有一个ID字段,作为主键,与业务无关
1.增加大章chapter表sql,生成持久层代码
小技巧:可以将常用的文件放入收藏夹,方便查找
注:每次要生成新表代码时,旧的表不要删除,但要注释掉。(同时生产多个表也可以,但没必要)
自动生成的代码
完成后端列表查询接口
同样,在business里的controller层也是一样的创建方法
启动注册中心,再启动business服务
1.增加dto层,用于controller和service层
DTO : Data Transfer Object 数据传输对象,用于数据传输
又是一个约定: domain内的实体,是mybatis generator自动生成的,不允许手动修改。一旦修改,再次生成实体类时, 所做的修改会被覆盖
domain作用于service和mapper;dto作用于controller和service
Ctrl+Alt+V为表达式生成一个变量
拓展:编写自己的for语句代码
for(int $INDEX$ = 0, l = $LIST$.size(); $INDEX$ < l; $INDEX$++) {
$END$
}
1. 点击按钮弹出变量设置窗口
2.设置这两个变量
BeanUtils是Spring提供的一个工具类,用于实体间的复制。后续我们会对BeanUtils做封闭,简化使用,提高开发效率
2.增加ChapterDto
是chapter复制的
3.修改ChapterService,将返回Chapter改成返回ChapterDto
5-2 大章列表查询功能开发2
从这个地方开始,我换mac了,嘿嘿
前端页面开发
row col-xs-12都是bootstrap栅格系统的内置样式,用于响应式页面的布局,需熟练掌握
选中全部,Shift+Tab,反向缩进
点击sidebar菜单实现页面跳转
二级菜单要显示成激活状态,只需要添加active样式
接下来完成功能:点击左侧菜单,该菜单变成激活状态,并跳到相应的路由页面
siblings,jquery的方法,获取所有兄弟节点
约定:id 的命名要和路由相关。后续我们会用到这个约定。
<router-link to="">,类似于<a href="">,用于链接跳转
为每一个路由都加上一个name属性,后续做通用的sidebar激活样式方法时会用到
通用的sidebar 点击激活样式方法
通用的功能,要尽量做个通用的方法,要学会“懒”。
1.通用的sidebar点击激活样式方法,使用watch 监听路由变化
vue 内置的watch,用来监测vue 实例上的数据变动,$route 也是一个变量。
通过name 属性值,得到菜单id 的值。前面有约定:id 的命名要和路由相关。程序开发中有一项设计范式叫:约定大于配置(按约定编程)。
此时如果从login页面点击登录跳到welcome页面,welcome并不会有激活样式。这里的watch,只在admin下面的子组件互相跳转时有效
js中有this 关键字,代表当前执行方法的对象。养成习惯,在方法开头,声明本地变量_this 代替this。后面会介绍直接用this的坑。
5-3 大章列表查询功能开发3
集成axios 完成前后端交互
vue也支持使用jquery ajax 来请求后端借口,推荐使用vue axios
注意:要先进到vue cli 项目,再安装插件
--save:在package.json添加依赖。(不加-- save的话,只是去下载插件,项目中并没有依赖插件)
1.安装axios
npm install axios --save
2.以vue属性的方式使用axios
修改main.js
import axios from 'axios'
Vue.prototype.$ajax = axios;
Vue.prototype.xxx,可以理解为Vue组件的全局变量。可以在任意Vue组件中,使用this.xxx 来获取这个值。$ 是代表Vue 全局属性的一个约定
3.chapter.vue 中使用$ajax
list() {
let _this = this;
_this.$ajax.get("http://127.0.0.1:9002/business/admin/chapter/list").then((response) => {
console.log("查询大章列表结果", response);
})
}
/admin 用于控台类的接口,/web 用于网站类的接口。接口设计中,用不同的请求前缀代表不同的入口,做接口隔离,方便做鉴权、统计、监控等
启动serve、注册中心EurekaApplication、BusinessApplication
CORS,Cross-Origin Resource Sharing 跨站点资源分享,属于跨域问题。同个IP的不同端口间访问也属于跨域。前后端分离必然有跨域问题
4.解决跨域的问题
1.集成axios 完成前后端交互
2.增加CorsConfig,解决前后端跨域的问题
增加CorsConfig.java
package com.course.server.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders(CorsConfiguration.ALL)
.allowedMethods(CorsConfiguration.ALL)
.allowCredentials(true)
.maxAge(3600); // 1小时内不需要再预检(发OPTIONS请求)
}
}
页面改造显示真实数据
1.大章页面显示真实数据
Ctrl+Shift+减号:收起所有节点,包括所有的子节点。
Ctrl+Shift+加号:展开所有的层级。
使用data定义组件内的变量,可用于做双向数据绑定,双向数据绑定是vue 的核心功能之一。
使用this.xxx来访问组件内的变量
使用gateway 路由转发
1.使用gateway 路由转发,vue页面只访问gateway的端口
spring.cloud.gateway.routes[1].id=business
spring.cloud.gateway.routes[1].uri=http://127.0.0.1:9002
spring.cloud.gateway.routes[1].predicates[0].name=Path
spring.cloud.gateway.routes[1].predicates[0].args[0]=/business/**
这里的请求地址目前是写死在代码中的,后续我们会做优化,对请求地址做多环境的配置。
扩展:1.解决gateway 跨域问题
gateway跨域配置
在gateway 启动类里增加
/**
* 配置跨域
* @return
*/
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(Boolean.TRUE);
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
把CorsConfig.java注释掉
把服务重新启动
是否直接访问gateway就不需要跨域配置里呢?需要再验证一下
单个SpringBoot 应用使用CorsConfig 解决跨域问题。使用SpringCloud Gateway的,使用CorsWebFilter解决跨域问题。
扩展:2.使用lb://+注册中心名称作路由转发
lb意思是loadbalance 负载均衡
问题:如果配置的是IP端口,那发布到生产时就可能会访问不到,就算配置了maven多环境,也需要提前知道上线后的IP和端口,提前配好。
#spring.cloud.gateway.routes[1].uri=http://127.0.0.1:9002
spring.cloud.gateway.routes[1].uri=lb://business
5-4 分页功能开发
集成分页插件pagehelper
1.集成分页插件pagehelper,注意页码从1开始
mybatis-generator 生成的代码是不带分页功能的,使用pagehelper插件来扩展分页功能。
父包
<!-- mybatis分页插件pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
</dependency>
server子包
<!-- mybatis分页插件pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
ChapterService.java
PageHelper.startPage(1,1);
PageHelper 的分页参数:pageNum是从1开始的
分页功能的关键字:limit。从日志可以看出,该sql 执行的是limit 1,相当于limit 0,1 ,即从第0行开始,查1条。
插件分页语句规则:调用startPage 方法之后,执行的第一个select 语句会进行分页。
limit 1,1 :从行号1(行号是从0开始)开始,查1条
分页查询功能需要两条sql ,一条是查总记录数(通过每页条数计算出总共有多少页),一条是查当前页的记录。
分页参数前后端交互
1.分页参数前后端交互,axios 的post 请求默认是以流的方式传递参数,所以controller 里的参数要加@RequestBody 注解
泛型需要熟练掌握,在写一些通用类,工具类时很好用。
扩展:使用泛型的地方都可以用Object 代替,但是泛型可以在编译期就发现问题,并且避免了代码中写强制类型转换。
PageDto 即用来接收入参,也用来返回结果。
当传入的分页参数不合法时,比如0,0 ,程序不会报错,而是查全部记录,分页不生效。
经验分享:在开发完代码后,需要进行测试,特别要针对一些边界值做测试。
接口请求参数传递,尽量使用post。使用get 请求在url 里拼参数的话,会使url 变得很长,有些浏览器或服务器会对url 长度做限制,导致请求失败。
private static final Logger LOG = LoggerFactory.getLogger($CLASSNAME$.class);
日志输出时,变量使用点位符,比如LOG.info("输出:id={},姓名={}",id,name),而不是LOG.info("输出:id=“+ id +”,姓名=" + name)
post请求有多种参数传递,通过header里的Content-Type来标识,常见的有两种,一种是表单的方式,另一种是json(流)的方式。
jquery 默认是以表单的方式,vue angular 默认是用json 的方式。
5-5 前端分页组件的使用
增加刷新功能
注意:<template>标签只能有一个子标签
fa 样式是 fontawesome 图标,可以百度搜“fontawesome图标”查看所有的图标样式
前端分页组件的使用
1.增加分页组件pagination.vue
2.大章管理页面使用分页组件,可自定义初始每页10条,最多显示8个按钮
问题:当数据量很大的时候,分页页码很多,这时把所有页码都显示出来,会占用页面的大部分空间,影响体验。所以需要设置显示页码数量
v-bind:list="list",前面的list,是分页组件暴露出来的一个回调方法,后面的list,是chapter组件的list方法
props,定义父组件向子组件传递的参数,可以是一个函数或数据。本组件中暴露了两个参数list 和 itemCount 给外部。
pagination.vue
<template>
<div class="pagination" role="group" aria-label="分页">
<button type="button" class="btn btn-default btn-white btn-round"
v-bind:disabled="page === 1"
v-on:click="selectPage(1)">
1
</button>
<button type="button" class="btn btn-default btn-white btn-round"
v-bind:disabled="page === 1"
v-on:click="selectPage(page - 1)">
上一页
</button>
<button v-for="p in pages" v-bind:id="'page-' + p"
type="button" class="btn btn-default btn-white btn-round"
v-bind:class="{'btn-primary active':page == p}"
v-on:click="selectPage(p)">
{{p}}
</button>
<button type="button" class="btn btn-default btn-white btn-round"
v-bind:disabled="page === pageTotal"
v-on:click="selectPage(page + 1)">
下一页
</button>
<button type="button" class="btn btn-default btn-white btn-round"
v-bind:disabled="page === pageTotal"
v-on:click="selectPage(pageTotal)">
{{pageTotal||1}}
</button>
<span class="m--padding-10">
每页
<select v-model="size">
<option value="1">1</option>
<option value="5">5</option>
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
条,共【{{total}}】条
</span>
</div>
</template>
<script>
export default {
name: 'pagination',
//props,定义父组件向子组件传递的参数,可以是一个函数或数据。本组件中暴露了两个参数list 和 itemCount 给外部。
props: {
list: {
type: Function,
default: null
},
itemCount: Number // 显示的页码数,比如总共有100页,只显示10页,其它用省略号表示
},
data: function () {
return {
total: 0, // 总行数
size: 10, // 每页条数
page: 0, // 当前页码
pageTotal: 0, // 总页数
pages: [], // 显示的页码数组
}
},
methods: {
/**
* 渲染分页组件
* @param page
* @param total
*/
render(page, total) {
let _this = this;
_this.page = page;
_this.total = total;
_this.pageTotal = Math.ceil(total / _this.size);
_this.pages = _this.getPageItems(_this.pageTotal, page, _this.itemCount || 5);
},
/**
* 查询某一页
* @param page
*/
selectPage(page) {
let _this = this;
if (page < 1) {
page = 1;
}
if (page > _this.pageTotal) {
page = _this.pageTotal;
}
if (this.page !== page) {
_this.page = page;
if (_this.list) {
_this.list(page);
}
}
},
/**
* 当前要显示在页面上的页码
* @param total
* @param current
* @param length
* @returns {Array}
*/
getPageItems(total, current, length) {
let items = [];
if (length >= total) {
for (let i = 1; i <= total; i++) {
items.push(i);
}
} else {
let base = 0;
// 前移
if (current - 0 > Math.floor((length - 1) / 2)) {
// 后移
base = Math.min(total, current - 0 + Math.ceil((length - 1) / 2)) - length;
}
for (let i = 1; i <= length; i++) {
items.push(base + i);
}
}
return items;
}
}
}
</script>
<style scoped>
.pagination {
vertical-align: middle !important;
font-size: 16px;
margin-top: 0;
margin-bottom: 10px;
}
.pagination button {
margin-right: 5px;
}
.btn-primary.active {
background-color: #2f7bba !important;
border-color: #27689d !important;
color: white !important;
font-weight: 600;
}
/*.pagination select {*/
/*vertical-align: middle !important;*/
/*font-size: 16px;*/
/*margin-top: 0;*/
/*}*/
</style>
5-6 增加新增大章功能
页面设计与前端代码开发
1.增加新增大章功能,前端代码开发
Bootstrap v3 中文文档 · Bootstrap 是最受欢迎的 HTML、CSS 和 JavaScript 框架,用于开发响应式布局、移动设备优先的 WEB 项目。 | Bootstrap 中文网
新增功能的页面如何设计,需要平时心里有些储备,可以平时浏览bootstrap 文档,看看都有哪些组件,用的时候心里有数。
模态框主要分为三大块:
modal-header 是标题;
modal-body 是主体内容,大章的表单内容就放在这里;
modal-footer 是底部按钮。
小技巧:
1.选中开头,一小部分代码
2.滚轮滚动到结尾鼠标拖动滚动条到结尾
3.按住shift并鼠标点击结尾
这种操作特别适合选中大段文本。
$(".modal")里的modal 是 css 的选择器,模态框代码里有class="modal" 样式;modal() 里的modal 是内置的方法,用于弹出或关闭模态框
启动admin、eureka、gateway、business
可以使用$(".modal").modal({backdrop:"static"});
禁止点空白的地方关闭,某些场景需求会用到这个功能。
vuecli 会将我们写的html js css 代码编译压缩,空格和换行都会被压缩掉,导致按钮间的间隔没有了
html 有很多转义字符,比如你想在界面显示文本"<text>",但是浏览器会认为<text>是一个标签,这时可以在html中用转义字符:<text>
<label for="id">有个场景会经常用到:点击复选框checkbox时选中,使用lable for 后,点击label 的文字,也能选中复制框
模态框弹出和关闭,可以用js代码,也可以用button属性:data-dismiss="css选择器"关闭;
data-toggle="css选择器"打开
短ID设计与后端代码开发
1.增加新增大章功能,后端代码开发,完成前后端联调,保存成功
面试:为什么不用自增ID?自增ID至少有三个问题:
1.id 是连续,容易被探测;
2.需要+1次查询才能得到id 的值;
3.分布式存储中,id 会出现重复
uuid 是根据机器、时间等多个维度生成的32位16进制数,有生之年不会重复。我在uuid 的基础上,封装了8位短uuid。
短ID 是根据将32位ID,转为62进制8位ID,减少存储空间。
原理是将uuid 转为10进制,再对62取余。也可以再添加两个符号,转为64进制。
xxxx.sout 用到了postfix
目前使用BeanUtil.copyProperties,需要多行代码,后续会对其做封装优化。
chapter变量用于绑定form 表单的数据。
将绑定好数据的chapter 作为前后端交互传参
增加复制工具类CopyUtil
1.增加复制工具类CopyUtil,封装BeanUtils.copyProperties,简化单实体复制和列表复制的代码
该工具类封装了BeanUtils.copyProperties,利用反射,牺牲一点性能(可忽略不计),换取开发效率。
统一返回参数ResponseDto
纯接口应用,一般会规范固定的请求参数,如版本号、请求流水等;再规范固定的返回参数,如返回码、返回描述等。方便调用方统一处理。
1.增加统一返回实体类ResponseDto,前后端代码针对ResponseDto 做修改
2.chapter 保存成功后关闭表单,并刷新列表
3.为modal增加id 属性
ResponseDto.java
ChapterController.java
response.data 就相当于responseDto
列表查询业务上一般都是成功的(查不到数据也是成功的,所以不需要判断success。保存有可能失败,所以需要判断success)
验证功能:
1.列表查询没问题;
2.保存功能没问题;
3.保存成功后关闭modal,并刷新列表。
css 选择器,可以通过id、class、标签等选择页面元素
问题:同一个页面有多个modal时,用class选择时,会出现重复,所以需要给每个modal增加id属性
需要测试modal相关的操作,点击新增,点击关闭,点击取消,点击保存,点击空白
5-7 修改删除大章功能
增加大章修改功能
1.增加修改大章功能,新增和修改用同一个保存功能,通过传入的参数id 有没有值来判断
新增和编辑功能弹出来的模态框是同一个。vue、controller、service 调用的都是同一个方法,只是到service层再根据id 是否有值来判断是新增还是删除
hidden-md:中等屏幕隐藏,其它可见;
hidden-lg:大屏幕隐藏,其它可见。
相反的有visible-xx,具体可参考https://v3.bootcss.com/css/#responsive-utilities-classes
在响应式页面中,同一个页面在大屏和小屏里显示的内容不太一样,大屏显示的内容更多,hidden-xx和visible-xx会经常用到
<div class="hidden-md hidden-lg">
<div class="inline pos-rel">
<button class="btn btn-minier btn-primary dropdown-toggle" data-toggle="dropdown" data-position="auto">
<i class="ace-icon fa fa-cog icon-only bigger-110"></i>
</button>
<ul class="dropdown-menu dropdown-only-icon dropdown-yellow dropdown-menu-right dropdown-caret dropdown-close">
<li>
<a href="#" class="tooltip-info" data-rel="tooltip" title="View">
<span class="blue">
<i class="ace-icon fa fa-search-plus bigger-120"></i>
</span>
</a>
</li>
<li>
<a href="#" class="tooltip-success" data-rel="tooltip" title="Edit">
<span class="green">
<i class="ace-icon fa fa-pencil-square-o bigger-120"></i>
</span>
</a>
</li>
<li>
<a href="#" class="tooltip-error" data-rel="tooltip" title="Delete">
<span class="red">
<i class="ace-icon fa fa-trash-o bigger-120"></i>
</span>
</a>
</li>
</ul>
</div>
</div>
1.将表格每一行数据传递到edit中做处理
2.将传递过来的一行数据chapter,赋给vue变量_this.chapter
vue变量_this.chapter会通过v-model属性和form表单做数据绑定
数据显示:将表格行数据显示到表单。反过来,数据修改:修改表单影响表格行数据。
_this.chapter = $.extend({},chapter);
发现问题:对文本框编辑后,点新增弹出文本框,会带出上一次编辑过的值。
_this.chapter = {};
增加大章删除功能
1.增加删除大章功能
delete 是js 的关键字,vue 方法里不能使用js 关键字
restful 是一种请求风格。简单的理解:通过看url 就能知道这个请求是要对什么资源做什么操作
后端的代码还没写,所以你报错404,需要熟记常用的返回码,如:200,301,400,401,403,404,500,503等
5-8 集成前端通用组件
集成sweetalert 用于界面消息确认框
1.集成sweetalert2,删除时弹出确认框
删除是一个有风险的操作,需要有确认的动作。
SweetAlert2 - a beautiful, responsive, customizable and accessible (WAI-ARIA) replacement for JavaScript's popup boxes
制作消息提示框
1.制作toast组件,内部用sweetalert2实现
通过修改timer可以设置弹出的时长,设置icon可以设置成成功、错误、警告等。
养成一种思维,将通用的代码做成组件
如果组件包含html代码,可以用vue组件;如果组件只有js代码,可以用原生的js
toast 是js 全局变量,可以在其它js 文件中使用,也可以在vue 组件中直接使用。
集成blockUI 用于界面等待框
1.制作Loading组件,内部用jquery blockui插件实现
等待框的作用:
1.让用户知道,后端正在处理,耐心等待;
2.防止用户恶意重复点击。
malsup.com/jquery/block/
BootCDN - Bootstrap 中文网开源项目免费 CDN 加速服务
本身loading功能不复杂,jquery blockUI 插件已经多年没更新了,也说明很稳定了。
一般使用压缩过的
1.修改Toastr 组件的显示效果,更大气
2.制作Confirm 组件
组件化的好处:只需要修改组件代码,就可以改变组件的样式,使用的地方完全不用动
简单理解 js 回调函数:将一个函数以参数的形式传递到另一个函数里去执行。在自定义组件中经常用到回调函数。
将变化的代码(组件无关的代码)作为回调函数传递进来
原来的代码先注释掉
5-9 代码优化
前端代码校验
1.增加工具类tool.js和校验类validator.js
2.大章保存非空和长度增加校验
validator.js
tool.js
后端代码校验
1.增加后端校验工具类ValidatorUtil
2.增加统一异常处理,ControllerExceptionHandler,关键字:@ControllerAdvice
新增什么都不填写,依旧保存成功
这种数据是不对的
前后端分离的项目,后端接口需要增加和前端一样的校验,防止被绕过前端界面,利用第三方工具如postman,直接访问后端接口
自定义异常可以继承RuntimeException 或 Exception。一般项目内部的业务异常,可以用RuntimeException,不需要try catch。如果是开发一些框架或工具类,明确告诉外部需要做异常处理的,可以用Exception。另外还需要考虑事务中的异常处理,后续介绍
测试一下
刷新
没有新增
说明我们校验生效了
现象:后端出异常,导致前后收不到结果,vue中的.then方法没有执行,等待框没有关闭,导致不能继续任何操作,只能刷新页面
选中代码,ctrl+alt+T,选择try/catch
但是这么做,如果有多个地方都用到,依然比较复杂
@ControllerAdvice 是Controller增强其,可以对Controller 做统一的处理,如异常处理,数据处理等。
前端也需要增加一下
测试
但还有一个安全问题
有时候我们的接口原本是不对外的,或者只跟特定的第三方应用做对接,这时为了内部安全,不应该把参数的校验规则暴露出去,所以需要模糊返回信息。类似登录接口应该返回“用户名或密码错误”,而不是“用户名不存在”,或“密码错误”(容易被探测)
如果开发过程中提示“请求参数异常”,说明后端有校验拦截,前端没有,此时应该把前端校验加上
使用AOP制作统一日志输出
1.增加日志AOP,统一日志输出
2.logback 增加打印日志跟踪号
问题:从打印的日志内容,看不出业务信息。日志不仅开发时有用,生产运维时查看业务日志也很重要,所以需要把日志加上业务信息。
统一日志处理,可以用AOP,也可以用Spring拦截器
package com.course.server.config;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import com.course.server.util.UuidUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
@Aspect
@Component
public class LogAspect {
private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);
/** 定义一个切点 */
@Pointcut("execution(public * com.course.*.controller..*Controller.*(..))")
public void controllerPointcut() {}
@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 日志编号
MDC.put("UUID", UuidUtil.getShortUuid());
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Signature signature = joinPoint.getSignature();
String name = signature.getName();
// 打印业务操作
String nameCn = "";
if (name.contains("list") || name.contains("query")) {
nameCn = "查询";
} else if (name.contains("save")) {
nameCn = "保存";
} else if (name.contains("delete")) {
nameCn = "删除";
} else {
nameCn = "操作";
}
// 使用反射,获取业务名称
Class clazz = signature.getDeclaringType();
Field field;
String businessName = "";
try {
field = clazz.getField("BUSINESS_NAME");
if (!StringUtils.isEmpty(field)) {
businessName = (String) field.get(clazz);
}
} catch (NoSuchFieldException e) {
LOG.error("未获取到业务名称");
} catch (SecurityException e) {
LOG.error("获取业务名称失败", e);
}
// 打印请求信息
LOG.info("------------- 【{}】{}开始 -------------", businessName, nameCn);
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
LOG.info("远程地址: {}", request.getRemoteAddr());
// 打印请求参数
Object[] args = joinPoint.getArgs();
Object[] arguments = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest
|| args[i] instanceof ServletResponse
|| args[i] instanceof MultipartFile) {
continue;
}
arguments[i] = args[i];
}
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"shard"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter)); // 为空的会不打印,但是像图片等长字段也会打印
}
@Around("controllerPointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "shard"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
return result;
}
}
约定优于配置。又一个约定:查询类接口以list或query开头,保存用save开头,删除用delete开头
敏感字段时不能明文打印或存储,比如身份证,手机号等。
后续会介绍图片上传,图片会转为base64 文本,太长,没有打印的必要,且占用空间,可以不打印。
一个日志跟踪号用来标识一次请求。生产环境中,往往同时会打印多个请求的日志,通过“grep 日志跟踪号” 可以查找出一次请求的所有日志。
1.前端增加统一日志输出
2.加上注释
添加了一些注释
删除了输出的日志
对ChapterController.java也是进行了注释和删除
ChapterService.java也是进行了注释和删除