前言:我们项目中可能有这种需求,每个人请求了哪些接口?做了什么事情?参数是什么?重要的接口我们需要记录操作日志以便查找。操作日志和系统日志不一样,操作日志必须要做到简单易懂。所以如何让操作日志不跟业务逻辑耦合,如何让操作日志的内容易于理解,如何让操作日志的接入更加简单?我们不可能在每个接口中去一一处理,可以借助Spring提供的AOP能力+自定义注解轻松应对。
一:AOP相关术语
通知(Advice)
通知描述了切面要完成的工作以及何时执行。比如我们的日志切面需要记录每个接口调用时长,就需要在接口调用前后分别记录当前时间,再取差值。
前置通知(Before):在目标方法调用前调用通知功能;
后置通知(After):在目标方法调用之后调用通知功能,不关心方法的返回结果;
返回通知(AfterReturning):在目标方法成功执行之后调用通知功能;
异常通知(AfterThrowing):在目标方法抛出异常后调用通知功能;
环绕通知(Around):通知包裹了目标方法,在目标方法调用之前和之后执行自定义的行为。
切点(Pointcut)
切点定义了通知功能被应用的范围。比如日志切面的应用范围就是所有接口,即所有controller层的接口方法。
切面是通知和切点的结合,定义了何时、何地应用通知功能。
在无需修改现有类的情况下,向现有的类添加新方法或属性。
把切面应用到目标对象并创建新的代理对象的过程。
连接点(JoinPoint)
通知功能被应用的时机。比如接口方法被调用的时候就是日志切面的连接点。
二:AOP相关注解
Spring中使用注解创建切面
@Aspect:用于定义切面
@Before:通知方法会在目标方法调用之前执行
@After:通知方法会在目标方法返回或抛出异常后执行
@AfterReturning:通知方法会在目标方法返回后执行
@AfterThrowing:通知方法会在目标方法抛出异常后执行
@Around:通知方法会将目标方法封装起来
@Pointcut:定义切点表达式
切点表达式:指定了通知被应用的范围,表达式格式:
execution(方法修饰符 返回类型 方法所属的包.类名.方法名称(方法参数)
//com.hs.demo.controller包中所有类的public方法都应用切面里的通知
execution(public* com.hs.demo.controller.*.*(..))
//com.hs.demo.service包及其子包下所有类中的所有方法都应用切面里的通知
execution(* com.hs.demo.service..*.*(..))
//com.hs.demo.service.EmployeeService类中的所有方法都应用切面里的通知
execution(* com.hs.demo.service.EmployeeService.*(..))
(1)@POINTCUT定义切入点,有以下2种方式:
方式一:设置为注解@LogFilter标记的方法,有标记注解的方法触发该AOP,没有标记就没有。
@Aspect
@Component
publicclassLogFilter1Aspect{
@Pointcut(value = "@annotation(com.hs.aop.annotation.LogFilter)")
publicvoidpointCut(){
}
}
自定义注解LogFilter:
@Target(ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public@interfaceLogFilter1 {
}
对应的Controller方法如下,手动添加@LogFilter注解:
@RestController
publicclassAopController{
@RequestMapping("/aop")
@LogFilter
publicStringaop(){
System.out.println("这是执行方法");
return"success";
}
}
方式二:采用表达式批量添加切入点,如下方法,表示AopController下的所有public方法都添加LogFilter1切面。
@Pointcut(value = "execution(public * com.train.aop.controller.AopController.*(..))")
publicvoidpointCut(){
}
(2)@Around环绕通知
@Around集成了@Before、@AfterReturing、@AfterThrowing、@After四大通知。需要注意的是,他和其他四大通知注解最大的不同是需要手动进行接口内方法的反射后才能执行接口中的方法,换言之,@Around其实就是一个动态代理。
/**
* 环绕通知是spring框架为我们提供的一种可以在代码中手动控制增强部分什么时候执行的方式。
*
*/
publicvoidaroundPringLog(ProceedingJoinPoint pjp)
{
//拿到目标方法的方法签名
Signaturesignature=pjp.getSignature();
//获取方法名
Stringname=signature.getName();
try{
//@Before
System.out.println("【环绕前置通知】【"+name+"方法开始】");
//这句相当于method.invoke(obj,args),通过反射来执行接口中的方法
proceed = pjp.proceed();
//@AfterReturning
System.out.println("【环绕返回通知】【"+name+"方法返回,返回值:"+proceed+"】");
}catch(Exception e) {
//@AfterThrowing
System.out.println("【环绕异常通知】【"+name+"方法异常,异常信息:"+e+"】");
}
finally{
//@After
System.out.println("【环绕后置通知】【"+name+"方法结束】");
}
}
proceed = pjp.proceed(args)这条语句其实就是method.invoke,以前手写版的动态代理,也是method.invoke执行了,jdk才会利用反射进行动态代理的操作,在Spring的环绕通知里面,只有这条语句执行了,spring才会去切入到目标方法中。
为什么说环绕通知就是一个动态代理呢?
proceed = pjp.proceed(args)这条语句就是动态代理的开始,当我们把这条语句用try-catch包围起来的时候,在这条语句前面写的信息,就相当于前置通知,在它后面写的就相当于返回通知,在catch里面写的就相当于异常通知,在finally里写的就相当于后置通知。
三:实现
(注意接口地址改变RequestFilter 中urlPatterns 也要改变 ,否则无法获取前台的request)
package com.unisound.iot.smart.operlog;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Objects;
/**
* request body 作为流式只能被读取一次,如果行多次阅读,必须将其保存下来
*
* 目前暂时不做body的读取,所以暂时去掉次过滤器
*/
//@WebFilter(filterName = "requestFilter", urlPatterns = "/*")
//这个是控制接口的,如果接口有改变urlPatterns 也要改变
@WebFilter(filterName ="requestFilter", urlPatterns ="/saas/*")
public class RequestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request1 =(HttpServletRequest) request;
if (Objects.isNull(request1.getHeader("Content-Type")) ||request1.getHeader("Content-Type").contains("multipart/form-data;")) {
chain.doFilter(request, response);
} else {
chain.doFilter(new InputStreamReadRepeatableRequestWrapper(request1), response);
}
}
@Override
public void destroy() {
}
}
package com.unisound.iot.smart.operlog;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.unisound.iot.smart.dao.model.operlog.OperationLog;
import com.unisound.iot.smart.dao.model.saas.LoginUserVO;
import com.unisound.iot.smart.entity.ym.MemberVo;
import com.unisound.iot.smart.service.operlog.OperationLogService;
import com.unisound.iot.smart.utils.IPUtil;
import com.unisound.iot.smart.utils.UserUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* 切面处理类,操作日志异常日志记录处理
*
* @author wu
* @date 2019/03/21
*/
@Slf4j
@Aspect
@Component
public class OperLogAspect {
@Autowired
private OperationLogService operationLogService;
/**
* 设置操作日志切入点 记录操作日志 在注解的位置切入代码
*/
@Pointcut("@annotation(com.unisound.iot.smart.operlog.OperLog)")
public void operLogPoinCut() {
}
/**
* 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
*
* @param joinPoint 切入点
* @param keys 返回结果
*/
@AfterReturning(value ="operLogPoinCut()", returning ="keys")
public void saveOperLog(JoinPoint joinPoint, Object keys) {
// 获取RequestAttributes
RequestAttributes requestAttributes =RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request =(HttpServletRequest) requestAttributes
.resolveReference(RequestAttributes.REFERENCE_REQUEST);
OperationLog operlog =new OperationLog();
try {
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature =(MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method =signature.getMethod();
// 获取操作
OperLog opLog =method.getAnnotation(OperLog.class);
if (opLog !=null) {
String operModul =opLog.operModul();
String operType =opLog.operType();
String operDesc =opLog.operDesc();
operlog.setOperModul(operModul); // 操作模块
operlog.setOperType(operType); // 操作类型
operlog.setOperDesc(operDesc); // 操作描述
}
LoginUserVO loginUserVO =UserUtils.getCurrentUser(request);
Long userId =null;
String userName =null;
Integer custId=null;
if (loginUserVO !=null) {
userId =Long.valueOf(loginUserVO.getLoginUserId());
userName =loginUserVO.getLoginName();
//custId = loginUserVO.getCustId();
}
// 获取请求的类名
String className =joinPoint.getTarget().getClass().getName();
// 获取请求的方法名
String methodName =method.getName();
methodName =className +"." +methodName;
String RequParam =null;
// 请求的参数
Map<String, Object> rtnMap = converMap(request.getParameterMap());
// 将参数所在的数组转换成json
String params =null;
JSONObject jsonObject =new JSONObject();
if (rtnMap !=null &&rtnMap.size() >0) {
//params = JSONObject.object(rtnMap);
//RequParam += "params=" + params;
JSONObject jo =new JSONObject(rtnMap);
jsonObject.put("params", jo);
}
//添加body
/* 为了避免低效【过滤器过滤每个url,保存body,暂时去掉】
开启方式:1.打开此段注释 2.打开RequestFilter中的@WebFilter 注释即可*/
try {
//获取前台传过来的request,如果接口地址有改变 打开RequestFilter中的urlPatterns 也要改变,否则request为空
Map bodyMap =RequestHelper.getBodyString(request);
String body =null;
if (bodyMap !=null) {
//body = JSON.toJSONString(bodyMap);
//RequParam += "body=" + body;
JSONObject jo =new JSONObject(bodyMap);
jsonObject.put("body", jo);
}
} catch (Exception e) {
log.error("读取 request body 出错! 请查看RequestFilter拦截地址是否包含!{}", e, e.getMessage());
}
operlog.setRequParam(jsonObject.toJSONString()); // 请求参数
operlog.setRespParam(JSON.toJSONString(keys)); // 返回结果
operlog.setUserId(userId); // 请求用户ID
operlog.setUserName(userName); // 请求用户名称
operlog.setOperIp(IPUtil.realIP(request)); // 请求IP
operlog.setOperUri(request.getRequestURI()); // 请求URI
//operlog.setCustId(custId);
operationLogService.insert(operlog);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 转换request 请求参数
*
* @param paramMap request获取的参数数组
*/
public Map<String, Object> converMap(Map<String, String[]> paramMap) {
Map<String, Object> rtnMap =new HashMap<String, Object>();
for (String key :paramMap.keySet()) {
rtnMap.put(key, paramMap.get(key)[0]);
}
return rtnMap;
}
}
创作不易,如果这篇文章对你有用,请点个赞谢谢♪(・ω・)ノ!