java基础复习-自定义注解3(自定义注解在SpringBoot中的使用)
写在前面:
1、本节内容源于前些日子工作的真实业务情况,为了方便本节叙述,特地将公司的项目单独宅出来作为讲解。
2、当时做该项目的开发时,有一个记录日志的需求,当时的第一想法是利用拦截器去完成,但是却也有着一些不方便的地方,因此使用了自定义注解技术进行了改进。
3、本节涉及的知识有自定义注解、SpringBoot框架、mybatis技术,spring切面的知识,如果未曾了解过该知识,可以先行进行学习。
1、搭建演示环境
1.1、数据库日志表的搭建:
建日志表的SQL语句:
DROP TABLE IF EXISTS `t_log`;
CREATE TABLE `t_log` (
`logID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID 主键自增长',
`operator` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '操作人',
`opDescribe` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '操作描述',
`operMethod` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '操作方法',
`opAddress` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '操作地址',
`operTime` datetime(0) DEFAULT NULL COMMENT '操作时间',
`delFlag` int(11) NOT NULL DEFAULT 0 COMMENT '置废标识',
UNIQUE INDEX `logID`(`logID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 337176 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
日志表的截图:
该表t_log各个字段的含义见上述sql语句中的备注字段
1.2、项目的目录结构
1.3、pom.xml的内容
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.7.RELEASE
com.xgp
mylog
0.0.1-SNAPSHOT
mylog
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-aop
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.0
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-maven-plugin
其中注意,与一般的SpringBoot项目中,此处多引入了一个依赖(Spring框架的apo依赖),因为本次演示需要使用到Spring框架的切面。
org.springframework.boot
spring-boot-starter-aop
1.4、配置文件(application.yml)
spring:
datasource:
# 数据源基本配置
username: ****
password: ****
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3307/smartclassroom?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=round
server:
port: 80
#开启mybatis的sql语句显示功能
logging:
level:
com:
xgp:
mylog:
mapper: debug
1.5、准备日志表的实体类
package com.xgp.mylog.model;
import java.util.Date;
public class Log {
private Long logid;
private String operator;
private String opdescribe;
private String opermethod;
private String opaddress;
private Date opertime;
private Integer delflag;
public Log() {
}
public Log(String operator, String opdescribe, String opermethod, String opaddress, Date opertime) {
this.operator = operator;
this.opdescribe = opdescribe;
this.opermethod = opermethod;
this.opaddress = opaddress;
this.opertime = opertime;
}
public Long getLogid() {
return logid;
}
public void setLogid(Long logid) {
this.logid = logid;
}
public String getOperator() {
return operator;
}
public void setOperator(String operator) {
this.operator = operator == null ? null : operator.trim();
}
public String getOpdescribe() {
return opdescribe;
}
public void setOpdescribe(String opdescribe) {
this.opdescribe = opdescribe == null ? null : opdescribe.trim();
}
public String getOpermethod() {
return opermethod;
}
public void setOpermethod(String opermethod) {
this.opermethod = opermethod == null ? null : opermethod.trim();
}
public String getOpaddress() {
return opaddress;
}
public void setOpaddress(String opaddress) {
this.opaddress = opaddress == null ? null : opaddress.trim();
}
public Date getOpertime() {
return opertime;
}
public void setOpertime(Date opertime) {
this.opertime = opertime;
}
public Integer getDelflag() {
return delflag;
}
public void setDelflag(Integer delflag) {
this.delflag = delflag;
}
@Override
public String toString() {
return "Log{" +
"logid=" + logid +
", operator='" + operator + '\'' +
", opdescribe='" + opdescribe + '\'' +
", opermethod='" + opermethod + '\'' +
", opaddress='" + opaddress + '\'' +
", opertime=" + opertime +
", delflag=" + delflag +
'}';
}
}
2、编写自定义注解
2.1、明确注解的作用访问:因为是记录请求日志的注解,所以应该作用在方法上
@Target(ElementType.METHOD)
2.2、明确使用阶段:运行时
@Retention(RetentionPolicy.RUNTIME)
2.3、明确注解的参数:根据t_log数据表,其中的请求方法的中文名称是需要在编写各个请求方法时进行写入的,而不能系统自动生成,因此注解的请求参数只有一个。完整的自定义注解的代码为:
package com.xgp.mylog.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {
String value();
}
3、编写注解解析器
3.1、Spring框架的Aop切面的介绍使用
如果要使用Spring框架的Aop切面,需要在类级别上标注@Aspect注解
如果该注解标红,是你前面的依赖没导
使用Spring的Aop切面,有三个关键的注解
切点注解:
@Pointcut("@annotation(com.xgp.mylog.annotation.MyLog)")
表示在切点前进行解析自定义注解的注解:
@Before("annotationPointcut()")
表示在切点后进行解析自定义注解的注解:
@After("annotationPointcut()")
本次的日志因为需要写入数据库,而与数据库的交互的方法较慢,为了不影响用户的体验速度,因此是在操作完成后进行日志的写入。
3.2、定义注解解析器类,并注册成为Spring的组件
@Aspect
@Component
public class MyLogAspect {
3.3、因为设计与数据库的交互,因此需要注入Mapper
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Autowired
LogMapper logMapper;
3.4、编写切点函数
@Pointcut("@annotation(com.xgp.mylog.annotation.MyLog)")
public void annotationPointcut() {
}
3.5、编写在切点后进行解析注解的函数
@After("annotationPointcut()")
public void afterPointcut(JoinPoint joinPoint) throws IOException {
其中该方法的JoinPoint参数为切点对象类,可以通过该类的实例获得被切方法的一些信息。
3.6、拿到注解上的value的值,即为请求方法的中文解释
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
MyLog annotation = method.getAnnotation(MyLog.class);
String opdescribe = annotation.value();
3.7、请求方法的中文值为由方法的编写者写入的,想要获取到其他信息,还需要获取到请求的request对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
3.8、通过请求的request对象获取请求的方法名
String[] uri = request.getRequestURI().split("/");
String requestName = uri[uri.length - 1];
3.9、判断改用户是否进行了登陆,如果没有进行登陆,则为登陆方法,从传递过来的参数中来获取操作人。
String operator = null;
if(request.getSession().getAttribute("token") == null) {
operator = (String) joinPoint.getArgs()[0];
}
3.10、编写获取请求的ip地址的函数,并获取请求方法来源的ip地址
String ip = getIpAddr(request);
该函数如下:
public String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if ("0:0:0:0:0:0:0:1".equals(ip)) {
ip = "127.0.0.1";
}
if (ip.split(",").length > 1) {
ip = ip.split(",")[0];
}
return ip;
}
3.11、自动生成日志时间,封装对象
Date requestTime = new Date();
Log log = new Log();
log.setOperator(operator);
log.setOpdescribe(opdescribe);
log.setOpermethod(requestName);
log.setOpaddress(ip);
log.setOpertime(requestTime);
3.12、判断该用户是否登陆成功,若登陆成功,则写入数据库。防治恶意登陆进行数据库爆库。
//获取返回结果
int result = (int) request.getAttribute("flag");
if(result == 1) {
//登陆成功写入数据库
logMapper.insert(log);
}
3.13、该注解解析器的网整的代码如下:
package com.xgp.mylog.annotation.aspect;
import com.xgp.mylog.annotation.MyLog;
import com.xgp.mylog.mapper.LogMapper;
import com.xgp.mylog.model.Log;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Date;
@Aspect
@Component
public class MyLogAspect {
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Autowired
LogMapper logMapper;
@Pointcut("@annotation(com.xgp.mylog.annotation.MyLog)")
public void annotationPointcut() {
}
@After("annotationPointcut()")
public void afterPointcut(JoinPoint joinPoint) throws IOException {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
MyLog annotation = method.getAnnotation(MyLog.class);
String opdescribe = annotation.value();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String[] uri = request.getRequestURI().split("/");
String requestName = uri[uri.length - 1];
String operator = null;
if(request.getSession().getAttribute("token") == null) {
operator = (String) joinPoint.getArgs()[0];
}
String ip = getIpAddr(request);
Date requestTime = new Date();
Log log = new Log();
log.setOperator(operator);
log.setOpdescribe(opdescribe);
log.setOpermethod(requestName);
log.setOpaddress(ip);
log.setOpertime(requestTime);
//获取返回结果
int result = (int) request.getAttribute("flag");
if(result == 1) {
//登陆成功写入数据库
logMapper.insert(log);
}
}
public String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if ("0:0:0:0:0:0:0:1".equals(ip)) {
ip = "127.0.0.1";
}
if (ip.split(",").length > 1) {
ip = ip.split(",")[0];
}
return ip;
}
}
4、编写插入一条日志的Mapper
注解解析器中设计到了日志与数据库的交互,因此在该类中编写与数据库交互的方法:
package com.xgp.mylog.mapper;
import com.xgp.mylog.model.Log;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LogMapper {
@Insert("insert into t_log (operator,opDescribe,operMethod,opAddress,operTime) values (#{operator,jdbcType=VARCHAR}, #{opdescribe,jdbcType=VARCHAR}, #{opermethod,jdbcType=VARCHAR}, #{opaddress,jdbcType=VARCHAR}, #{opertime,jdbcType=TIMESTAMP})")
void insert(Log log);
}
这里需注意一点,mybatis在取方法的参数值时,最好以#{}d的方式进行取值,可以有效的防治SQL注入的问题。
5、编写测试的Controller,这里以登陆日志作为演示
package com.xgp.mylog.Controller;
import com.xgp.mylog.annotation.MyLog;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
public class UserController {
/**
* 登陆方法
*/
@MyLog("登陆方法")
@GetMapping("/login")
public String login(@RequestParam String username, @RequestParam String password, HttpServletRequest request) {
if("xgp".equals(username) && "123".equals(password)) {
request.setAttribute("flag",1);
return "登陆成功";
}
request.setAttribute("flag",0);
return "登陆失败";
}
}
这里使用自己的自定义注解上的参数为"登陆方法",并且如果登陆成功,则像request域中写入1,反之写入0。
6、进行测试
因为方便测试,这里的登陆方法采用的是GET请求,测试者可以在浏览器中的地址栏中方便的进行测试:
看到前端返回了登陆成功,表示我们的数据已经成功的插如到了数据库中,打开数据库中进行验证,也确实插入成功了。
7、与用拦截器写日志的方法进行对比
之前使用拦截器的做法:
package com.c611.smartclassroom.component;
import com.c611.smartclassroom.mapper.LogMapper;
import com.c611.smartclassroom.model.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class LogHandlerInterceptor implements HandlerInterceptor {
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Autowired
LogMapper logMapper;
private static Map requestMap = new HashMap<>();
public LogHandlerInterceptor() {
// requestMap.put("querySchByClassSeme","查询课表");
requestMap.put("exportSch","导出课表");
requestMap.put("importSch","导入课表");
requestMap.put("addSchool","添加学校");
requestMap.put("addZone","添加区域");
requestMap.put("addBuild","添加教学楼");
requestMap.put("addClassRoom","添加教室");
requestMap.put("setCourseTime","设置课时");
requestMap.put("setSemesterDate","设置学期时间");
requestMap.put("editSemesterDate","编辑学期时间");
requestMap.put("addRole","增加角色");
requestMap.put("delRole","删除角色");
requestMap.put("editAuth","编辑授权码");
//工单管理模块
requestMap.put("queryWorkOrder","按时间排序分页查询所有工单");
requestMap.put("queryWorkOrderResult","按工单编号查询处理结果");
requestMap.put("saveWorkOrderResult","填写处理结果");
requestMap.put("delWorkOrder","删除工单");
requestMap.put("addWorkOrder","增加工单");
//网关管理
requestMap.put("addGateWay","添加网关");
requestMap.put("saveGateWay","编辑网关");
requestMap.put("delGateWay","删除网关");
//设备管理
requestMap.put("addDevice","添加设备");
requestMap.put("saveDevice","编辑设备信息");
requestMap.put("delDevice","删除设备");
}
/* private static final String USER_AGENT = "user-agent";
private Logger logger = LoggerFactory.getLogger(this.getClass());*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
/* HttpSession sesson = request.getSession();
String userId = (String) sesson.getAttribute("userId");
//说明未登陆,不能放行
if(userId == null) return false;
//查数据库,根据userId查找用户,或者从session中取出*/
String userName = "薛国鹏";
String[] uri = request.getRequestURI().split("/");
String requestName = uri[uri.length - 1];
// System.out.println(requestName);
String chaineseName = requestMap.get(requestName);
// System.out.println(chaineseName);
if(chaineseName != null) {
String ip = getIpAddr(request);
Date requestTime = new Date();
Log log = new Log();
log.setOperator(userName);
log.setOpdescribe(chaineseName);
log.setOpermethod(requestName);
log.setOpaddress(ip);
log.setOpertime(requestTime);
logMapper.insertSelective(log);
return true;
}
return false;
}
public String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if ("0:0:0:0:0:0:0:1".equals(ip)) {
ip = "127.0.0.1";
}
if (ip.split(",").length > 1) {
ip = ip.split(",")[0];
}
return ip;
}
}
分析代码可以知道,有如下弊端:
其中有一个Map集合数据量可能会较大,占用较大内存
拦截器的编写者需要手动的将其他开发者编写的Controller层的方法一一翻译放入map中,工作量大且繁琐。