使用Spring?AOP實(shí)現(xiàn)用戶(hù)操作日志功能
我使用Spring AOP實(shí)現(xiàn)了用戶(hù)操作日志功能
今天答辯完了,復(fù)盤(pán)了一下系統(tǒng),發(fā)現(xiàn)還是有一些東西值得拿出來(lái)和大家分享一下。
需求分析
系統(tǒng)需要對(duì)用戶(hù)的操作進(jìn)行記錄,方便未來(lái)溯源
首先想到的就是在每個(gè)方法中,去實(shí)現(xiàn)記錄的邏輯,但是這樣做肯定是不現(xiàn)實(shí)的,首先工作量大,其次違背了軟件工程設(shè)計(jì)原則(開(kāi)閉原則)
這種需求顯然是對(duì)代碼進(jìn)行增強(qiáng),首先想到的是使用 SpringBoot 提供的 AOP 結(jié)合注解的方式來(lái)實(shí)現(xiàn)
功能實(shí)現(xiàn)
1、 需要一張記錄日志的 Log 表
導(dǎo)出的 sql 如下:
-- mcams.t_log definition CREATE TABLE `t_log` ( `log_id` int NOT NULL AUTO_INCREMENT COMMENT '日志編號(hào)', `user_id` int NOT NULL COMMENT '操作人id', `operation` varchar(128) NOT NULL COMMENT '用戶(hù)操作', `method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作的方法', `params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '方法的參數(shù)', `ip` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用戶(hù)的ip', `create_time` timestamp NULL DEFAULT NULL COMMENT '操作時(shí)間', `cost_time` int DEFAULT NULL COMMENT '花費(fèi)時(shí)間', PRIMARY KEY (`log_id`) ) ENGINE=InnoDB AUTO_INCREMENT=189 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
2、我使用的是 Spring Boot 所以需要引入 spring aop 的 starter
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
如果是使用 spring 框架的,引入 spring-aop 即可
3、Log 實(shí)體類(lèi)
package com.xiaofengstu.mcams.web.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.io.Serializable; import java.time.LocalDateTime; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Getter; import lombok.Setter; /** * <p> * * </p> * * @author fengzeng * @since 2022-05-21 */ @Getter @Setter @TableName("t_log") public class TLog implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "log_id", type = IdType.AUTO) private Integer logId; /** * 操作人id */ @TableField("user_id") private Integer userId; /** * 用戶(hù)操作 */ @TableField("operation") private String operation; @TableField("method") private String method; @TableField("params") private String params; @TableField("ip") private String ip; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @TableField("create_time") private LocalDateTime createTime; @TableField("cost_time") private Long costTime; }
需要 lombok 插件(@getter && @setter 注解)
4、ILog 注解
package com.xiaofengstu.mcams.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @Author FengZeng * @Date 2022-05-21 00:48 * @Description TODO */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ILog { String value() default ""; }
5、切面類(lèi) LogAspect
package com.xiaofengstu.mcams.aspect; import com.xiaofengstu.mcams.annotation.ILog; import com.xiaofengstu.mcams.util.ThreadLocalUtils; import com.xiaofengstu.mcams.web.entity.TLog; import com.xiaofengstu.mcams.web.service.TLogService; import lombok.RequiredArgsConstructor; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; 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 java.lang.reflect.Method; import java.time.LocalDateTime; /** * @Author FengZeng * @Date 2022-05-21 00:42 * @Description TODO */ @Aspect @Component @RequiredArgsConstructor public class LogAspect { private final TLogService logService; @Pointcut("@annotation(com.xiaofengstu.mcams.annotation.ILog)") public void pointcut() { } @Around("pointcut()") public Object around(ProceedingJoinPoint point) { Object result = null; long beginTime = System.currentTimeMillis(); try { result = point.proceed(); } catch (Throwable e) { e.printStackTrace(); } long costTime = System.currentTimeMillis() - beginTime; saveLog(point, costTime); return result; } private void saveLog(ProceedingJoinPoint point, long costTime) { // 通過(guò) point 拿到方法簽名 MethodSignature methodSignature = (MethodSignature) point.getSignature(); // 通過(guò)方法簽名拿到被調(diào)用的方法 Method method = methodSignature.getMethod(); TLog log = new TLog(); // 通過(guò)方法區(qū)獲取方法上的 ILog 注解 ILog logAnnotation = method.getAnnotation(ILog.class); if (logAnnotation != null) { log.setOperation(logAnnotation.value()); } String className = point.getTarget().getClass().getName(); String methodName = methodSignature.getName(); log.setMethod(className + "." + methodName + "()"); // 獲取方法的參數(shù) Object[] args = point.getArgs(); LocalVariableTableParameterNameDiscoverer l = new LocalVariableTableParameterNameDiscoverer(); String[] parameterNames = l.getParameterNames(method); if (args != null && parameterNames != null) { StringBuilder param = new StringBuilder(); for (int i = 0; i < args.length; i++) { param.append(" ").append(parameterNames[i]).append(":").append(args[i]); } log.setParams(param.toString()); } ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); log.setIp(request.getRemoteAddr()); log.setUserId(ThreadLocalUtils.get()); log.setCostTime(costTime); log.setCreateTime(LocalDateTime.now()); logService.save(log); } }
因?yàn)槲沂褂玫氖?Mybatis-plus,所以 logService.save(log);
是 mybatis-plus 原生的 save operation
這步其實(shí)就是把 log 插入到數(shù)據(jù)庫(kù)
6、使用
只需要在方法上加上 @ILog 注解,并設(shè)置它的 value 即可(value 就是描述當(dāng)前 method 作用)
package com.xiaofengstu.mcams.web.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.xiaofengstu.mcams.annotation.ILog; import com.xiaofengstu.mcams.dto.BasicResultDTO; import com.xiaofengstu.mcams.enums.RespStatusEnum; import com.xiaofengstu.mcams.web.entity.TDept; import com.xiaofengstu.mcams.web.service.TDeptService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * <p> * 前端控制器 * </p> * * @author fengzeng * @since 2022-05-07 */ @RestController @RequestMapping("/web/dept") @RequiredArgsConstructor public class TDeptController { private final TDeptService deptService; @ILog("獲取部門(mén)列表") @GetMapping("/list") public BasicResultDTO<List<TDept>> getDeptListByCampId(@RequestParam("campusId") Integer campusId) { return new BasicResultDTO(RespStatusEnum.SUCCESS, deptService.list(new QueryWrapper<TDept>().eq("campus_id", campusId))); } @ILog("通過(guò)角色獲取部門(mén)列表") @GetMapping("/listByRole") public BasicResultDTO<List<TDept>> getDeptListByRole() { return new BasicResultDTO<>(RespStatusEnum.SUCCESS, deptService.listByRole()); } }
數(shù)據(jù)庫(kù):
總結(jié)
如果要對(duì)現(xiàn)有代碼進(jìn)行功能擴(kuò)展,使用 AOP + 注解不妨為一種優(yōu)雅的方式
對(duì) AOP 不熟悉的小伙伴,可以深入了解一下,畢竟是 spring 最重要的特性之一。
到此這篇關(guān)于使用Spring AOP實(shí)現(xiàn)了用戶(hù)操作日志功能的文章就介紹到這了,更多相關(guān)Spring AOP用戶(hù)操作日志內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解使用Java原生代理實(shí)現(xiàn)AOP實(shí)例
本篇文章主要介紹了詳解使用Java原生代理實(shí)現(xiàn)AOP實(shí)例,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-01-01SpingMvc復(fù)雜參數(shù)傳收總結(jié)
這篇文章主要為大家介紹了SpingMvc復(fù)雜參數(shù)傳收總結(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08SpringMVC4+MyBatis+SQL Server2014實(shí)現(xiàn)數(shù)據(jù)庫(kù)讀寫(xiě)分離
這篇文章主要介紹了SpringMVC4+MyBatis+SQL Server2014實(shí)現(xiàn)讀寫(xiě)分離,需要的朋友可以參考下2017-04-04SpringBoot處理form-data表單接收對(duì)象數(shù)組的方法
form-data則是一種更加靈活的編碼方式,它可以處理二進(jìn)制數(shù)據(jù)(如圖片、文件等)以及文本數(shù)據(jù),這篇文章主要介紹了SpringBoot處理form-data表單接收對(duì)象數(shù)組,需要的朋友可以參考下2023-11-11Java開(kāi)發(fā)崗位面試被問(wèn)到泛型怎么辦
泛型在java中有很重要的地位,在面向?qū)ο缶幊碳案鞣N設(shè)計(jì)模式中有非常廣泛的應(yīng)用。java泛型知識(shí)點(diǎn)也是Java開(kāi)發(fā)崗位必問(wèn)的一個(gè)話(huà)題,今天小編就給大家普及下Java泛型常見(jiàn)面試題,感興趣的朋友一起看看吧2021-07-07Java HTTP協(xié)議收發(fā)MQ 消息代碼實(shí)例詳解
這篇文章主要通過(guò)實(shí)例代碼為大家詳細(xì)介紹了如何在Java 環(huán)境下使用 HTTP 協(xié)議收發(fā) MQ 消息,需要的朋友可以參考下2017-04-04新手場(chǎng)景Java線(xiàn)程相關(guān)問(wèn)題及解決方案
這篇文章主要介紹了新手場(chǎng)景Java線(xiàn)程相關(guān)問(wèn)題及解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07詳解Spring Security中權(quán)限注解的使用
這篇文章主要為大家詳細(xì)介紹一下Spring Security中權(quán)限注解的使用方法,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)或工作有一定參考價(jià)值,需要的可以參考一下2022-05-05java線(xiàn)程并發(fā)countdownlatch類(lèi)使用示例
javar的CountDownLatch是個(gè)計(jì)數(shù)器,它有一個(gè)初始數(shù),等待這個(gè)計(jì)數(shù)器的線(xiàn)程必須等到計(jì)數(shù)器倒數(shù)到零時(shí)才可繼續(xù)。2014-01-01