前言
上一篇文章在SelectProvider获取需要操作的bean对象时,出现了点问题,导致泛型对象<T>需要传入到dao层的每个方法,十分不合理。后面参考了下网上的教程,跟着用上了AOP进行切面编程,切面方法将当前dao的内容获取到ThreadLocal中,数据操作方法执行时获取dao内容。
AOP工作原理图
正文
各模块一览
name | description |
---|---|
nott-mybatis-curd | mybaits-基础 |
nott-mybatis-dynamic-datasource | mybatis-动态数据源 |
nott-web-test | web测试模块 |
完整项目文件见GITHUB代码仓库,如果对你有用请点个star。
引入依赖
在根目录中引入需要的ASPECT包。
pom
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.21.1</version>
<scope>runtime</scope>
</dependency>
Pointcut配置
这里我们需要'拦截'这个基础CommonMapper下的方法,包路径为org.nott.mybatis.mapper,所以aop表达式应为execution(* org.nott.mybatis.mapper.*.*(..)) ,表示Pointcut 包括org.nott.mybatis.mapper 下的所有方法,它下面的某个方法就是JoinPoint 。然后在执行方法前将它的当前泛型放入到需要的地方,BaseSelectProvider执行时获取泛型内容并实例化bean。 CommonMapper
创建在curd模块包下aop相关的创建配置类,用于读取aop的表达式。
less
@Data
@Component
@ConfigurationProperties("nott.mybatis.aop")
public class MybatisAopConfig {
/**
* 拦截基础的CommonMapper aop表达式
*/
private String baseAopPackageExpression = "execution(* org.nott.mybatis.mapper.*.*(..))";
/**
* 自定义加上的aop表达式
*/
private String[] appendAopPackageExpression;
}
创建信息储存对象
我们aop的PointCut已经定义完成,需要定义每一个方法执行前都存储的信息对象,存放当前mapper的类、执行方法、参数等。
java
@Data
public class ExecuteMapperContextBean {
/**
* 当前mapper的class
*/
private Class<?> currentMapperClass;
/**
* 执行的方法
*/
private Method executeMethod;
/**
* 参数
*/
private Parameter[] parameters;
}
Advice
至此,已经定义好需要获取信息的方法和用来存储的对象,接下来就定义怎么获取方法的内容。
创建继承至MethodInterceptor的MybatisAopInterceptor,获取当前执行的方法,在把它转成ExecuteMapperContextBean存储到ThreadLocal<ExecuteMapperContextBean>里面。
java
@Component
public class MybatisAopInterceptor implements MethodInterceptor {
private static final ThreadLocal<ExecuteMapperContextBean> mapperContextThreadLocal = new ThreadLocal<>();
public void set(MethodInvocation invocation){
ExecuteMapperContextBean bean = new ExecuteMapperContextBean();
Class<?> aClass = Objects.requireNonNull(invocation.getThis()).getClass();
Class<?>[] interfacesForClass = ClassUtils.getAllInterfacesForClass(aClass, aClass.getClassLoader());
Class<?>[] interfaces = interfacesForClass[0].getInterfaces();
bean.setExtendMapperClass(interfaces[0]);
bean.setCurrentMapperClass(interfacesForClass[0]);
bean.setParameters(invocation.getMethod().getParameters());
bean.setExecuteMethod(invocation.getMethod());
mapperContextThreadLocal.set(bean);
}
@Nullable
@Override
public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable {
// 存储信息
this.set(invocation);
Object result = null;
try {
// JoinPoint方法执行
result = invocation.proceed();
return result;
} catch (Exception e) {
throw e;
} finally {
// 最后,释放mapperContextThreadLocal中信息
mapperContextThreadLocal.remove();
}
}
public static ThreadLocal<ExecuteMapperContextBean> getContext(){
return mapperContextThreadLocal;
}
}
这样,已经定义好存储dao层方法执行时的信息的行为动作,和它执行的时机,就剩下把整套动作行为拼在一起了。
PointcutAdvisor
Advisor是aop管理Advice和PointCut的顶级接口,它的部分继承关系如图所示。
这里我们需要设置Advice和PointCut的属性,因此需要定义AspectJExpressionPointcutAdvisor的bean。
java
@Configuration
@EnableConfigurationProperties(MybatisAopConfig.class)
@RequiredArgsConstructor
public class MapperContextAutoConfiguration {
private final MybatisAopConfig mybatisAopConfig;
@Bean
public Interceptor setMybatisAopInterceptor(){
return new MybatisAopInterceptor();
}
@Bean
public PointcutAdvisor setMyBatisPointcutAdvisor(){
// 设置拦截表达式(PointCut)
AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
String baseAopPackage = mybatisAopConfig.getBaseAopPackageExpression();
advisor.setExpression(baseAopPackage);
String[] appendAopPackage = mybatisAopConfig.getAppendAopPackageExpression();
if (appendAopPackage != null && appendAopPackage.length > 0) {
for (String packageExpression : appendAopPackage) {
advisor.setExpression(packageExpression);
}
}
// 执行动作
advisor.setAdvice(setMybatisAopInterceptor());
return advisor;
}
}
可以注意到MybatisAopInterceptor作为setAdvice(Advice advice)方法的参数,而它是实现MethodInterceptor接口的。关于Interceptor为什么能作为setAdvice的参数,这个关系图可以说明。
验证
在web模块,创建UserMapper继承作为JoinPoint的CommonMapper,CommonMapper定义了一个selectList的通用查询方法
java
public interface UserMapper extends CommonMapper<User> {
java
public interface CommonMapper<T> {
@SelectProvider(type = BaseSelectProvider.class,method = "selectList")
public List<T> selectList();
}
在MybatisAopInterceptor中打上断点,如果正确执行,程序会在此卡住,此时set方法已经把信息放入ThreadLocal中,debug追踪可以看到目前执行方法的信息。
编写测试类
断点
小结
纸上得来终觉浅 绝知此事要躬行
之前都是在网上看有关AOP的教程,看得一头雾水,内容很是抽象。等到实际运用的时候,才感觉里面设计的十分巧妙,有很多门道可以学习使用,实际运用其中我也看了很多网上的相关教程,学习了很多意想不到的东西。