springBoot使用aop+注解方式实现可由切点(Poincut)向增强(Advice)传递参数的日志管理
老规矩先抛出需求:
在原有的业务上新增日志管理,要求日志记录指定的信息(包含业务所属模块;客户端传入的参数;业务处理时产生的数据,如结算后的余额等;业务执行的结果)
1.添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.日志表 略
3.自定义日志注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Description:日志注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAnnotation {
String module() default "未指定";
}
4.日志增强类
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Map;
/**
* @Description:日志增强类
*/
@Component
@Aspect
@Lazy(value = false)
public class LogAspect {
@Pointcut("@annotation(com.****.LogAnnotation)")
private void cutMethod() {}
@Around(value = "cutMethod()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//从注解中获取业务模块
String module = getModule(joinPoint);
//从切点的方法参数中获取传入业务的参数
JSONObject jsonParams = getBusinessParams(joinPoint);
//执行切点方法
Object proceed = joinPoint.proceed();
//从固定的getLogParams()方法中获取需要打印出来的数据
Map logParamsMap = getParams(joinPoint);
//输出日志 实际应用将控制台输出改为插入日志表即可
System.out.println("打印业务日志----业务模块:" + module + ",传入参数:" + jsonParams.toJSONString() +
",指定记录参数:" + JSON.toJSONString(logParamsMap) + ",业务调用结果:" + JSON.toJSONString(proceed));
return proceed;
}
/**
* 从注解中获取业务模块
*
* @param joinPoint
* @return
*/
private String getModule(ProceedingJoinPoint joinPoint) throws Exception{
Object target = joinPoint.getTarget();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
LogAnnotation annotation = method.getAnnotation(LogAnnotation.class);
return annotation.module();
}
/**
* 从切点的方法参数中获取传入业务的参数
*
* @param joinPoint
* @return
*/
private JSONObject getBusinessParams(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
JSONObject jsonParams = new JSONObject();
for (int i = 0; i < args.length; i++) {
if (null != parameterNames[i]) {
jsonParams.put(parameterNames[i], null == args[i] ? "" : args[i].toString());
}
}
return jsonParams;
}
/**
* 从固定的getLogParams()方法中获取需要打印出来的数据
*
* @param joinPoint
* @return
* @throws Throwable
*/
private Map getParams(ProceedingJoinPoint joinPoint) throws Throwable {
Object target = joinPoint.getTarget();
Method logParams = target.getClass().getDeclaredMethod("getLogParams");
logParams.setAccessible(true);//注意:由于getLogParams方法为private方法,此处必须关闭安全检查,否则报异常
Map logParamsMap = (Map) logParams.invoke(target);
return logParamsMap;
}
}
5.写个controller类测试一下
import com.alibaba.fastjson.JSONObject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 测试controller
*/
@Controller
@RequestMapping("/test1")
public class Test1 {
private static ThreadLocal<Map> threadLocal = new ThreadLocal<>();
private Map getThreadLocalMap() {
if (null == threadLocal.get()) {
threadLocal.set(new HashMap(2));
}
return threadLocal.get();
}
@RequestMapping("/add")
@ResponseBody
@LogAnnotation(module = "用户管理")
public Object addUser(String name, String email) {
Map map = getThreadLocalMap();
map.put("切点中经过业务处理产生的参数", "新增用户的名字和邮箱是:" + name + "," + email + ",分配的id是:" + 123456);
JSONObject jsonObject = new JSONObject();
jsonObject.put("status", "200");
jsonObject.put("message", "操作成功");
return jsonObject;
}
private Map getLogParams() {
return this.threadLocal.get();
}
}
6.输入地址测试一下
打印业务日志----业务模块:用户管理,传入参数:{"name":"tonny","email":"tonny@qq.com"},指定记录参数:{"切点中经过业务处理产生的参数":"新增用户的名字和邮箱是:tonny,tonny@qq.com,分配的id是:123456"},业务调用结果:{"message":"操作成功","status":"200"}
测试OK!
7.相关知识点:
- 通知(有的地方叫增强)(Advice),需要完成的工作叫做通知,就是你写的业务逻辑中需要比如事务、日志等先定义好,然后需要的地方再去用
- 连接点(Join point),就是spring中允许使用通知的地方,基本上每个方法前后抛异常时都可以是连接点
- 切点(Poincut),其实就是筛选出的连接点,一个类中的所有方法都是连接点,但又不全需要,会筛选出某些作为连接点做为切点。如果说通知定义了切面的动作或者执行时机的话,切点则定义了执行的地点
- 切面(Aspect),其实就是通知和切点的结合,通知和切点共同定义了切面的全部内容,它是干什么的,什么时候在哪执行
- 引入(Introduction),在不改变一个现有类代码的情况下,为该类添加属性和方法,可以在无需修改现有类的前提下,让它们具有新的行为和状态。其实就是把切面(也就是新方法属性:通知定义的)用到目标类中去
- 目标(target),被通知的对象。也就是需要加入额外代码的对象,也就是真正的业务逻辑被组织织入切面。
- 织入(Weaving),把切面加入程序代码的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点可以进行织入:
- 编译期:切面在目标类编译时被织入,这种方式需要特殊的编译器
- 类加载期:切面在目标类加载到JVM时被织入,这种方式需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码
- 运行期:切面在应用运行的某个时刻被织入,一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象,Spring AOP就是以这种方式织入切面的。
常用AOP通知(增强)类型
- before(前置通知): 在方法开始执行前执行
- after(后置通知): 在方法执行后执行
- afterReturning(返回后通知): 在方法返回后执行
- afterThrowing(异常通知): 在抛出异常时执行
- around(环绕通知): 在方法执行前和执行后都会执行
执行顺序:around > before > around > after > afterReturning
版权声明:本文为a455363168原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。