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的內容
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xgp</groupId>
<artifactId>mylog</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mylog</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
其中注意,與一般的SpringBoot項目中,此處多引入了一個依賴(Spring框架的apo依賴),因為本次演示需要使用到Spring框架的切面。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
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<String,String> 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中,工作量大且繁瑣。