昨天突发奇想想统计报表的访问记录,想了一下这种公用的操作很适合用AOP实现呀~
之前看过类似的经典操作是实现Mybatis动态切换数据源,之前有写过文章,是实现多数据源的方式之一。
· 需要了解
1. 为什么使用自定义注解?
自定义注解可以配置参数,并且在切面类中可以获取到参数,使用时一行代码即可。
·步骤
1. 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UserLog {
String menuName();
String menuDesc();
}
@Target
说明了注解修饰的对象的范围;注解可以修饰packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)等;
ElementType的取值有一下几种:
· METHOD:用于描述方法 (比较常用)
· CONSTRUCTOR:用于描述构造器
· FIELD:用于描述域
· LOCAL_VARIABLE:用于描述局部变量
· PACKAGE:用于描述包
· PARAMETER:用于描述参数
· TYPE:用于描述类、接口(包括注解类型) 或enum声明
@Retantion
定义被它所修饰的注解保留多久,RetentionPolicy的值有:
· RUNTIME:保留至JVM运行时,所以我们可以通过反射去获取注解信息;(最常用)
· SOURCE:源代码级别保留,编译时就会被忽略;
· CLASS:编译时被保留,在class文件中存在,但JVM将会忽略;
@Document
表明这个注解应该被 javadoc工具记录. 默认情况下,javadoc是不包括注解的。但如果声明注解时指定了@Documented,则它会被 javadoc 之类的工具处理,,所以注解类型信息也会被包括在生成的文档中
2. 编写切面类
@Aspect
public class UserLogAdvice {
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UserLogAdvice.class.getName());
@Resource
PMUserExtMapper pmUserExtMapper;
@Pointcut("execution (* com.csot..controller.*.*(..)) && @annotation(userLog)")
public void controllerAspect(UserLog userLog) {
}
/**
* 拦截的方法执行之前就执行
* @param joinPoint 拦截的方法传入的参数
* @param userLog 自定义注解
*/
@Before("controllerAspect(userLog)")
public void addBeforeLogger(JoinPoint joinPoint, UserLog userLog) {
LocalDateTime now = LocalDateTime.now();
logger.info(now.toString()+"执行插入报表访问记录:[" + userLog.menuName() + ", " + userLog.menuDesc() + "]开始");
// 获取保存在session中的用户信息
HttpServletRequest request =((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session =request.getSession();
LoginClient loginClient = (LoginClient) session.getAttribute(LoginClient.CLIENT_SESSION_ATTR);
if (loginClient == null) {
return ;
}
Map<String, Object> params = new HashMap<String, Object>();
params.put("menuName", userLog.menuName());
params.put("menuDesc", userLog.menuDesc());
params.put("userCode", loginClient.getUser().getId());
params.put("userName", loginClient.getUser().getName());
params.put("logTime", new Date());
pmUserExtMapper.insertUserLog(params);
}
}
@PointCut
声明切入点,因为如果切面类的很多切面方法的表达式都相同并且还有与或等运算,可以统一声明一个通用的切面的点供切面方法使用;
execution (* com.csot..controller.*.*(..)) :切入点的位置描述
第一个*表示拦截方法的返回值
连续两个..表示com.csot下的所有子包,后面的*表示任意字符,*.*(..)表示controller包下的所有文件的任意方法,方法的参数任意
@annotation(userLog)
拦截方法上必须要有UserLog注解,&&表示并且的关系;
@Before("controllerAspect(userLog)")
拦截方法体执行前触发,controllerAspect() 即通过@PointCut注解的切入点;
除了@Before之外还有其他的注解可以修饰切面方法,达到在不同时间点触发操作;
· @After()
· @AfterReturning() 拦截方法返回值之后触发
` @AfterThrowing() 用于处理程序中未处理的异常,通过切面可以统一为方法增加异常处理;
` @ Around 这是一个非常强大的注解,可以控制目标操作是否执行、修改目标方法的参数、增强目标方法,主要在以下三种情况下使用:
1. 可以完全阻止目标方法执行,实际上是不写proceedindjoinpoint参数的proceed()就可以达成了,其实proceed()方法是在around注解中执行目标方法的关键词.
2.可以自己选择目标方法什么时候执行,实际上是通过proceed()方法,然后把增强方法放于proceed()前后就可以决定在proceed()之前执行还是之后执行.
3. 可以共享资源,其实际也是因为该proceed()里面塞入参数,该参数被所有目标的带参方法所用(同时也覆盖了带参方法本身的参数)
各个注解执行顺序如下:
@Before --> @Around --> @After --> @AfterReturning (@AfterThrowing视目标方法是否抛出异常执行)
3. Spring开启AOP支持
在Spring的配置文件中开启AOP,注册切面类:
<aop:aspectj-autoproxy proxy-target-class="true" />
<bean id="userLogAspect" class="com.csot.aspect.UserLogAdvice"></bean>
4. 踩坑
当我第一次配置完成后启动工程,访问想要拦截的Controller方法,发现切面并没有生效;很幸运的是,上网百度出来的第一篇博客就解决了我的问题。原因就是我的切面类被加载到容器时,controller类还没有加载,导致无法在目标Controller类里面织入切面。
因为我的Spring配置文件是分成了好几个,分别有加载Controller,Service等,配置文件目录结构如下:
我最开始的做法是在Spring-context.xml里面开启AOP,然后用@Component注释切面类,这种方式是失败的。
之后修改为:在springcontext-mvc-scan.xml中,扫描完Controller之后开启AOP,并且通过<bean>标签实例化切面类,重启工程之后就可以了。