Spring AOP如何自定義注解實現(xiàn)審計或日志記錄(完整代碼)
環(huán)境準(zhǔn)備
JDK 1.8,Springboot 2.1.3.RELEASE,spring-boot-starter-aop.2.1.4.RELEASE.jar,aspectjrt.1.9.2.jar,aspectjweaver.1.9.2.jar,pom依賴如下:
<!-- 添加aspectj --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.2</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.2</version> </dependency>
項目結(jié)構(gòu)
自定義審計注解
package com.cjia.common.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(value = RetentionPolicy.RUNTIME) @Target(value = { ElementType.METHOD }) public @interface Audit { /** * 模塊代碼 */ int moudleCode() default -1; /** * 擴展信息,用戶返回操作類型及參數(shù) */ Class<?> extension(); }
定義切面類
package com.cjia.common.aspect; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; 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.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 com.cjia.common.Moudle; import com.cjia.common.Operate; import com.cjia.common.annotation.Audit; import com.cjia.common.reflect.BaseValueReturn; import com.cjia.model.AuditMessage; import com.cjia.model.ResponseContent; import com.cjia.model.User; import com.cjia.service.AuditService; import com.cjia.service.UserService; @Aspect @Component public class AuditAop { private static final Logger LOGGER = LoggerFactory.getLogger(AuditAop.class); @Autowired private AuditService auditService; @Autowired private UserService userService; // 操作發(fā)起者 ThreadLocal<User> user = new ThreadLocal<>(); // 操作應(yīng)用 ThreadLocal<Integer> appId = new ThreadLocal<>(); // 功能模塊 ThreadLocal<Integer> moudleCode = new ThreadLocal<>(); // 操作類型 ThreadLocal<Integer> operateType = new ThreadLocal<>(); // IP地址 ThreadLocal<String> ip = new ThreadLocal<>(); // 操作時間點 ThreadLocal<String> operateTime = new ThreadLocal<>(); // 操作信息實體 ThreadLocal<AuditMessage> msg = new ThreadLocal<>(); // 對CIS,額外的菜單id信息extension ThreadLocal<String> extension = new ThreadLocal<>(); // 聲明AOP切入點 @Pointcut("@annotation(com.cjia.common.annotation.Audit)") public void audit() { } @Before("audit()") public void beforeExec(JoinPoint joinPoint) { } @After("audit()") public void afterExec(JoinPoint joinPoint) { } @Around("audit()") public Object aroundExec(ProceedingJoinPoint pjp) throws Throwable { try { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); extension.set(request.getParameter("extension")); operateTime.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); ip.set(getIpAddr(request)); appId.set(Integer.valueOf(request.getParameter("appId"))); MethodSignature ms = (MethodSignature) pjp.getSignature(); Method method = ms.getMethod(); // 獲取注解的參數(shù)信息 Audit auditAnno = method.getAnnotation(Audit.class); moudleCode.set(auditAnno.moudleCode()); Class<?> external = auditAnno.extension(); Constructor<?> cons = external.getConstructor(Integer.class, HttpServletRequest.class); BaseValueReturn bvr = (BaseValueReturn) cons.newInstance(moudleCode.get(), request); Map<String, Object> reqInfo = bvr.getRequestInfo(); Integer operate_type = Integer.valueOf(reqInfo.get(BaseValueReturn.OPT) + ""); operateType.set(operate_type); Object target = reqInfo.get(BaseValueReturn.TARGET); // 獲取當(dāng)前登錄的用戶,需注意:登錄時沒有msgKey String msgKey = request.getParameter("msgKey"); User loginUser = null; if (operate_type.equals(Operate.LOGIN)) { List<User> users = userService.selectByEmail(String.valueOf(target)); if (users != null && !users.isEmpty()) { loginUser = users.get(0); } } else if (msgKey != null) { loginUser = userService.selectMsgFromRedisPurely(msgKey); } user.set(loginUser); AuditMessage am = new AuditMessage(); // am.setUserId需判空,代表過期 if (loginUser != null) { am.setUserId(loginUser.getId()); } else { am.setUserId(-1); } am.setAppId(appId.get()); am.setMoudleCode(moudleCode.get()); am.setOperateType(operateType.get()); am.setIp(ip.get()); am.setOperateTime(operateTime.get()); // TODO details=target String details = ""; if (moudleCode.get() == Moudle.DATA && loginUser != null) { details = (String) target; } else { // TODO 其他模塊 } am.setTarget(details); msg.set(am); auditService.insert(msg.get()); } catch (Exception e) { LOGGER.error("Error occured while auditing, cause by: ", e); } finally { // FATAL: remove threadLocal and set threadLocal = null } Object rtn = pjp.proceed(); return rtn; } /** * 帶參返回 */ @AfterReturning(pointcut = "audit()", returning = "rc") public void doAfterReturning(ResponseContent rc) { LOGGER.debug("afterReturning with returnType.."); } /** * 不帶參返回 */ @AfterReturning(pointcut = "audit()") public void doAfterReturning(JoinPoint joinPoint) { LOGGER.debug("afterReturning without returnType.."); } @AfterThrowing(pointcut = "audit()", throwing = "e") public void doAfterThrowing(JoinPoint joinPoint, Throwable e) { LOGGER.error("Error occured, cause by {}", e.getMessage()); } private String getRemoteHost(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(); } return ip.equals("0:0:0:0:0:0:0:1") ? "127.0.0.1" : ip; } /** * 獲取用戶真實IP地址,不使用request.getRemoteAddr()的原因是有可能用戶使用了代理軟件方式避免真實IP地址, * 可是,如果通過了多級反向代理的話,X-Forwarded-For的值并不止一個,而是一串IP值 * * @return ip */ private String getIpAddr(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) { // 多次反向代理后會有多個ip值,第一個ip才是真實ip if( ip.indexOf(",")!=-1 ){ ip = ip.split(",")[0]; } } 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.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("X-Real-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } }
注意事項
第81行的HttpServletRequest request最好設(shè)置為局部變量,或ThreadLocal修飾的成員變量,而非普通成員變量,防止異步請求較多,導(dǎo)致有的request丟失參數(shù)的離奇問題。
定義返回值處理基類
package com.cjia.common.reflect; import java.util.Map; import javax.servlet.http.HttpServletRequest; public abstract class BaseValueReturn { public static final String OPT = "operate"; public static final String TARGET = "target"; protected Integer moudleCode; protected HttpServletRequest request; public BaseValueReturn(Integer moudleCode, HttpServletRequest request) { super(); this.moudleCode = moudleCode; this.request = request; } /** * 返回操作動作operate和操作目標(biāo)target */ public abstract Map<String, Object> getRequestInfo(); }
定義返回值處理子類
package com.cjia.common.reflect; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletRequest; import com.cjia.common.Operate; public class DataValueReturn extends BaseValueReturn { public DataValueReturn(Integer moudleCode, HttpServletRequest request) { super(moudleCode, request); } @Override public Map<String, Object> getRequestInfo() { Map<String, Object> info = new HashMap<>(); info.put(OPT, Operate.FETCH_DATA); String menuId = request.getParameter("extension"); info.put(TARGET, menuId); return info; } }
package com.cjia.common.reflect; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletRequest; import com.cjia.common.Operate; public class LoginValueReturn extends BaseValueReturn { public LoginValueReturn(Integer moudleCode, HttpServletRequest request) { super(moudleCode, request); } @Override public Map<String, Object> getRequestInfo() { Map<String, Object> info = new HashMap<>(); info.put(OPT, Operate.LOGIN); String email = request.getParameter("email"); info.put(TARGET, email); return info; } }
定義功能模塊類
package com.cjia.common; import java.util.HashMap; import java.util.Map; public class Moudle { public static final int LOGIN = 1; public static final int DATA = 2; public static final int USER = 3; // TODO more moudles private static final Map<Integer, String> moudleMap = new HashMap<>(); static { moudleMap.put(Moudle.LOGIN, "登錄"); moudleMap.put(Moudle.DATA, "業(yè)務(wù)數(shù)據(jù)"); moudleMap.put(Moudle.USER, "用戶"); } public static String getNameByCode(int code) { return moudleMap.get(code); } }
定義操作類
package com.cjia.common; import java.util.HashMap; import java.util.Map; public class Operate { public static final int LOGIN = 1; // 內(nèi)部系統(tǒng)獲取數(shù)據(jù) public static final int FETCH_DATA = 2; public static final int INSERT = 3; public static final int DELETE = 4; public static final int UPDATE = 5; public static final int SEARCH = 6; // TODO more opts private static final Map<Integer, String> optMap = new HashMap<>(); static { optMap.put(Operate.LOGIN, "登錄"); optMap.put(Operate.FETCH_DATA, "獲取業(yè)務(wù)數(shù)據(jù)"); optMap.put(Operate.INSERT, "新增"); optMap.put(Operate.DELETE, "刪除"); optMap.put(Operate.UPDATE, "修改"); optMap.put(Operate.SEARCH, "查詢"); } public static String getNameByCode(int code) { return optMap.get(code); } }
定義審計信息實體類
package com.cjia.model; public class AuditMessage { private int id; private int userId; private int appId; private int moudleCode; private int operateType; private String ip; private String operateTime; private String target; public int getId() { return id; } public void setId(int id) { this.id = id; } public int getUserId() { return userId; } public void setUserId(int userId) { this.userId = userId; } public int getAppId() { return appId; } public void setAppId(int appId) { this.appId = appId; } public int getMoudleCode() { return moudleCode; } public void setMoudleCode(int moudleCode) { this.moudleCode = moudleCode; } public int getOperateType() { return operateType; } public void setOperateType(int operateType) { this.operateType = operateType; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public String getOperateTime() { return operateTime; } public void setOperateTime(String operateTime) { this.operateTime = operateTime; } public String getTarget() { return target; } public void setTarget(String target) { this.target = target; } @Override public String toString() { return "AuditMessage [id=" + id + ", userId=" + userId + ", appId=" + appId + ", moudleCode=" + moudleCode + ", operateType=" + operateType + ", ip=" + ip + ", operateTime=" + operateTime + ", target=" + target + "]"; } }
書寫mapper文件
<?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.cjia.dao.AuditMessageMapper"> <resultMap id="BaseResultMap" type="com.cjia.model.AuditMessage"> <result column="id" jdbcType="INTEGER" property="id" /> <result column="user_id" jdbcType="VARCHAR" property="userId" /> <result column="app_id" jdbcType="VARCHAR" property="appId" /> <result column="moudle_code" jdbcType="VARCHAR" property="moudleCode" /> <result column="operate_type" jdbcType="INTEGER" property="operateType" /> <result column="ip" jdbcType="VARCHAR" property="ip" /> <result column="operate_time" jdbcType="INTEGER" property="operateTime" /> <result column="target" jdbcType="VARCHAR" property="target" /> </resultMap> <!-- 對應(yīng)的插入字段的名字 --> <sql id="keys"> <trim suffixOverrides=","> <if test="id!=null and id!=''"> id, </if> <if test="userId!=null and userId!=''"> user_id, </if> <if test="appId!=null and appId!=''"> app_id, </if> <if test="moudleCode!=null and moudleCode!=''"> moudle_code, </if> <if test="operateType!=null and operateType!=''"> operate_type, </if> <if test="ip!=null and ip!=''"> ip, </if> <if test="operateTime!=null and operateTime!=''"> operate_time, </if> <if test="target!=null and target!=''"> target, </if> </trim> </sql> <!-- 對應(yīng)的插入字段的值 --> <sql id="values"> <trim suffixOverrides=","> <if test="id!=null and id!=''"> #{id}, </if> <if test="userId!=null and userId!=''"> #{userId}, </if> <if test="appId!=null and appId!=''"> #{appId}, </if> <if test="moudleCode!=null and moudleCode!=''"> #{moudleCode}, </if> <if test="operateType!=null and operateType!=''"> #{operateType}, </if> <if test="ip!=null and ip!=''"> #{ip}, </if> <if test="operateTime!=null and operateTime!=''"> #{operateTime}, </if> <if test="target!=null and target!=''"> #{target}, </if> </trim> </sql> <insert id="insert" parameterType="com.cjia.model.AuditMessage"> insert into audit_message( <include refid="keys" /> ) values( <include refid="values" /> ) </insert> <select id="selectAll" resultMap="BaseResultMap"> select * from audit_message </select> </mapper>
開啟AOP攔截
package com.cjia; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.EnableAspectJAutoProxy; @SpringBootApplication @MapperScan("com.cjia.dao") @EnableAspectJAutoProxy(exposeProxy=true) public class DataserviceApplication { public static void main(String[] args) { SpringApplication.run(DataserviceApplication.class, args); } }
或在配置文件中:
<!-- 開啟AOP攔截 --> <aop:aspectj-autoproxy proxy-target-class="true" /> <mvc:annotation-driven /> <!-- 定義Spring描述Bean的范圍,使切面類AuditAop可以被掃描到 --> <context:component-scan base-package="**.common" > <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>
注解配置
package com.cjia.controller; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import com.alibaba.fastjson.JSON; import com.cjia.common.InnerApp; import com.cjia.common.Moudle; import com.cjia.common.ResConstance.UserGroup; import com.cjia.common.annotation.Audit; import com.cjia.common.reflect.LoginValueReturn; import com.cjia.model.Menu; import com.cjia.model.ResponseContent; import com.cjia.model.User; import com.cjia.service.MenuService; import com.cjia.service.UserService; import com.cjia.utils.DigestUtil; /** * 處理內(nèi)部系統(tǒng)登錄信息驗證 */ @Controller public class LoginController { private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class); @Autowired private UserService userService; @Autowired private MenuService menuService; /** * 處理登錄頁表單提交信息 */ @ResponseBody @PostMapping("/doLogin") @Audit(moudleCode = Moudle.LOGIN, extension = LoginValueReturn.class) public ResponseContent doLogin(HttpServletRequest req) throws Exception { String email = req.getParameter("email"); String password = req.getParameter("password"); int appId = Integer.valueOf(req.getParameter("appId")); // 獲取到的這么多user(同一賬戶)只是role或門店不同,密碼等其他都一致 List<User> users = new ArrayList<>(); // 對SRP報表系統(tǒng)不允許管家登錄 if (appId == InnerApp.CIS) { users = userService.selectByEmail4CIS(email); } else { users = userService.selectByEmail(email); } ResponseContent responseContent = new ResponseContent(); if (users != null && !users.isEmpty() && users.get(0).getPassword().equals(DigestUtil.threeHash(password, users.get(0).getSecurityKey()))) { User user = users.get(0); LOGGER.debug("登陸驗證成功{}", user); // 設(shè)置roleIdLst和branchMap List<Integer> roleIdLst = users.stream().map(User::getRoleId).collect(Collectors.toList()); Map<String, String> branchMap = new HashMap<>(); Map<Integer, List<Menu>> menuMap = getAllMenus(users); for (User u : users) { branchMap.put(u.getBranchId() + "", u.getBranchName()); } user.setRoleIdLst(roleIdLst); user.setBranchMap(branchMap); // 給user設(shè)置菜單列表 user.setMenuMap(menuMap); // 登陸成功后,需要看此用戶的信息是否存在于redis,存在則刷新過期時間,不存在則插入記錄 String msgKey = userService.insertIntoRedis(user); responseContent.setRespCode(ResponseContent.RESPCODE_SUCCESS); responseContent.setMessage("登陸成功"); responseContent.setData(msgKey); } else { LOGGER.debug("登陸驗證失敗"); responseContent.setRespCode(ResponseContent.RESPCODE_FAILURE); responseContent.setMessage("登陸失敗,請檢查用戶賬號密碼是否正確"); } String jsonString = JSON.toJSONString(responseContent); LOGGER.debug("登錄返回信息:{}", jsonString); return responseContent; } }
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java實戰(zhàn)之小米交易商城系統(tǒng)的實現(xiàn)
這篇文章將利用Java實現(xiàn)小米交易商城系統(tǒng),文中采用的技術(shù)有:JSP?、Spring、SpringMVC、MyBatis等,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-04-04javaweb用戶注銷后點擊瀏覽器返回刷新頁面重復(fù)登錄問題的解決方法
這篇文章主要為大家詳細介紹了javaweb用戶注銷后點擊瀏覽器返回刷新頁面重復(fù)登錄問題的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-09-09詳解Java8函數(shù)式編程之收集器的應(yīng)用
這篇文章主要介紹了詳解Java8函數(shù)式編程之收集器的應(yīng)用,收集器是一種通用的、從流生成復(fù)雜值的結(jié)構(gòu)。可以使用它從流中生成List、Set、Map等集合,需要的朋友可以參考下2023-04-04SpringBoot實現(xiàn)JWT token自動續(xù)期的示例代碼
本文主要介紹了SpringBoot實現(xiàn)JWT token自動續(xù)期的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01