最近的项目中需要实现日志收集的功能,项目中使用了Spring AOP的环绕通知(Around)和消息队列实现的,本篇是基于 Spring AOP实现后台管理系统日志管理(感谢作者)来进行记录的。
1、Spring AOP
切面(Aspect):切面用于组织多个Advice,Advice放在切面中定义。
连接点(Joinpoint):程序执行过程中明确的点,如方法的调用,或者异常的抛出。在Spring AOP中,连接点总是方法的调用。
增强处理(Advice):AOP框架在特定的切入点执行的增强处理。处理有“around”、“before”和“after”等类型。
切入点(Pointcut):可以插入增强处理的连接点。
2、通知类型
1)Before:在一个切面类里使用 @Before 来修饰一个方法时,该方法将作为Before增强处理。使用@Before修饰时,通常需要指定一个value属性值,指定一个切入点表达式,用于指定该增强处理将被织入哪些切入点。
注:该增强处理在目标方法之前执行。
2)After:After增强处理不管目标方法是成功还是有异常,都会被织入。After增强处理必须准备处理正常返回和异常返回两种情况,这种增强处理通常用于释放资源。
3)AfterReturning:AfterReturning增强处理在目标方法正常完成后被织入。
注:1)AfterReturning增强处理可以访问目标方法的返回值,但不能改变。当发生异常时,不起作用。
4)AfterThrowing:AfterThrowing增强处理主要用于处理程序中未处理的异常。
5)Around:Around增强处理是功能比较强大的增强处理,它近似等于Before和AfterReturning增强处理的总和,Around增强处理既可在执行目标方法之前织入增强动作,也可在执行目标方法之后织入增强动作。
当定义一个Around增强处理方法时,该方法的第一个形参必须是roceedingJoinPoint类型。
3、自定义注解
import java.lang.annotation.*;
/**
* 定义Log收集注解
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableLog {
String desc() default "";//描述业务操作
}
1)@Target({ElementType.METHOD,ElementType.TYPE}) :
用于描述注解的使用范围(即:被描述的注解可以用在什么地方),其取值有:
CONSTRUCTOR: 用于描述构造器。
FIELD: 用于描述域。
LOCAL_VARIABLE: 用于描述局部变量。
METHOD: 用于描述方法。
PACKAGE: 用于描述包。
PARAMETER: 用于描述参数。
TYPE: 用于描述类或接口(甚至 enum )。
2)@Retention(RetentionPolicy.RUNTIME):
用于描述注解的生命周期(即:被描述的注解在什么范围内有效),其取值有:
SOURCE: 在源文件中有效(即源文件保留)。
CLASS: 在 class 文件中有效(即 class 保留)。
RUNTIME: 在运行时有效(即运行时保留)。
3)@Documented 在默认的情况下javadoc命令不会将我们的annotation生成再doc中去的,所以使用该标记就是告诉jdk让它也将annotation生成到doc中去
4、定义切面
@Component
@Aspect
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
private static final ThreadLocal<User> userThreadLocal = new NamedThreadLocal<>("threadlocal_user");
private static final ThreadLocal<MyLog> myLogThreadLocal = new NamedThreadLocal<>("threadlocal_myLog");
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Autowired
private LogService logService;
/**
* 在controller的切点
*/
@Pointcut(value = "@annotation(cn.hhm.worklogcollect.aop.EnableLog)")
public void controllerPointCut() {
}
/**
* 前置通知
*
* @param point
*/
@Before(value = "controllerPointCut()")
public void doBefore(JoinPoint point) throws Exception {
logger.error("前置通知...执行");
Date beginTime = new Date();//操作时间
MyLog myLog = new MyLog();
myLog.setTime(beginTime);
//获取用户
HttpServletRequest request = getRequest();
User user = (User) request.getSession().getAttribute("user");
myLogThreadLocal.set(myLog);
userThreadLocal.set(user);
}
/**
* 后置通知
*
* @param point
*/
@After(value = "controllerPointCut()")
public void doAfter(JoinPoint point) {
logger.error("后置通知...执行");
User user = userThreadLocal.get();
if (user != null) {
String title = "";
HttpServletRequest request = getRequest();
String ip = request.getRemoteAddr();
String uri = request.getRequestURI();
String requestType = request.getMethod();
try {
title = getDesc(point);
} catch (Exception e) {
e.printStackTrace();
}
MyLog myLog = myLogThreadLocal.get();
myLog.setType("info");
myLog.setTitle(title);
myLog.setIpAddress(ip);
myLog.setUriStr(uri);
myLog.setRequestType(requestType);
myLog.setUserId(user.getId().toString());
myLog.setCreateTime(new Date());
myLogThreadLocal.set(myLog);
//通过线程池保存日志
threadPoolTaskExecutor.execute(new InsertLogThread(myLog, logService));
userThreadLocal.remove();
}
}
/**
*在 @After之后执行,如果没有异常的话,最终执行本方法
* @param point
*/
@AfterReturning("controllerPointCut()")
public void doAfterReturning(JoinPoint point){
logger.error("AfterReturning...........执行");
myLogThreadLocal.remove();
}
/**
* 异常通知
*
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "controllerPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
logger.error("AfterThrowing通知...执行");
MyLog myLog = myLogThreadLocal.get();
if (myLog != null) {
myLog.setType("error");
myLog.setErrorInfo(e.toString());
threadPoolTaskExecutor.execute(new UpdateLogThread(myLog, logService));
myLogThreadLocal.remove();
}
}
/**
* 日志的保存
*/
private class InsertLogThread implements Runnable {
private MyLog myLog;
private LogService logService;
public InsertLogThread(MyLog myLog, LogService logService) {
this.myLog = myLog;
this.logService = logService;
}
@Override
public void run() {
logService.insert(myLog);
}
}
/**
* 日志的更新
*/
private class UpdateLogThread implements Runnable {
private MyLog myLog;
private LogService logService;
public UpdateLogThread(MyLog myLog, LogService logService) {
this.myLog = myLog;
this.logService = logService;
}
@Override
public void run() {
logService.update(myLog);
}
}
public String getDesc(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
EnableLog enableLog = method.getAnnotation(EnableLog.class);
String desc = enableLog.desc();
return desc;
}
public HttpServletRequest getRequest() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
return request;
}
}
由于在切面中使用了ThreadLocal,为了避免内存泄漏,每次使用完ThreadLocal后,都调用它的remove()方法,清除数据。
5、Spring AOP 切入点指示符
1)execution:用于匹配执行方法的连接点。
//匹配任意 public 方法的执行
@Around(value = “execution(public * * (..))”)
//匹配任何方法名以“set”开始的方法的执行
@Around(value = “execution(* set* (..))”)
//匹配service中任意方法的执行
@Around(value = “execution(* com.example.service.* (..))”)
//匹配com.exampleservice.impl包下任意类的任意方法的执行
@Around(“execution(* com.example.service.impl..(..))”)
2)within:用于限定匹配特定类型的连接点,当使用Spring AOP的时候,只能匹配方法执行的连接点。
//在com.example.service包中的任意连接点(在 spring aop 中只是方法执行的连接点)
@Around(“within(com.example.service.*)”)
//在com.example.service包或其子包中的任意连接点
@Around(“within(com.example.service..*)”)
3)this/target:用于限定AOP代理必须是指定类型的实例,匹配该对象的所有连接点。
//匹配实现了com.example.service.impl.UserServiceImpl类的AOP代理的所以连接点(方法)
@Around(“this(com.example.service.impl.UserServiceImpl)”)
6、在Controller中
@EnableLog(desc = "用户模块-查询列表")
@GetMapping(value = "/user/list")
public List<User> userList(){
List<User> userList = userService.selectUserList();
return userList;
}
演示结果: