springaop⽆法拦截接⼝上的注解
⽬录
问题背景
最近在spring-boog项⽬中做mysql读写分离时遇到了⼀些奇葩问题,问题现象:通过常规的spring aop去拦截带有⾃定义注解的⽅法时,发现只有注解写在实现类上⾯时才有效,写在接⼝上时却不⽣效。所⽤的spring-boot版本为1.x版本
问题现场(aop代码)
@Aspect
@Component
@EnableAspectJAutoProxy
public class DataSourceAspect {
@Around("@DataSource)")
public Object around(ProceedingJoinPoint joinPoint)throws Throwable {
// 业务⽅法执⾏之前设置数据源...
doingSomthingBefore();
// 执⾏业务⽅法
Object result = joinPoint.proceed();
// 业务⽅法执⾏之后清除数据源设置...
doingSomthingAfter();
return result;
}
}
这是⼀段⾮常普通的spring aop代码,由于项⽬中使⽤的事务注解全部都是写在接⼝的⽅法上的,所以我也就习惯性的把注
解@DataSource写在接⼝的⽅法上,⼀调试代码,这时候发现spring aop根本就不鸟你,没⽣效。⽹上⼀通搜索后,发现遇到这个问题的⼈⾮常多,答案也是五花⼋门,有的说是spring-boot 1.x版本的bug,升级到2.x版本就可以了。然后就屁颠屁颠的把spring-boot 版本换成最新的2.3.0.RELEASE版本,根本就没⽤;也有⼈分析说aop代理的是spring的bean实例,然⽽接⼝很显然是不能实例化的,所以aop⽆法⽣效。查了很多,都是分析为什么不起作⽤的,可能是我搜索的关键字不对的原因,就没怎么看到有解决⽅案的帖⼦。
同样的写在接⼝⽅法上的@Transactional为什么就能⽣效呢(⾄于spring事务原理的解析这⾥就不讲了,⽹上⼀⼤把)?
源码
通过@EnableTransactionManagement进去看了下spring事务的源码,
上图中看到@EnableTransactionManagement注解上导⼊了⼀个类,不知道⼲什么的,点进去看看
TransactionManagementConfigurationSelector继承了AdviceModeImportSelector,就是想加载别的类,在selectImports⽅法返回的内容就是要加载的类,这⾥可以看到分别加载了AutoProxyRegistrar,ProxyTransactionManagementConfiguration这两个类,通过名字能猜
出ProxyTransactionManagementConfiguration这个类应该是⼀个事务相关的配置类,继续点进去看下
点开ProxyTransactionManagementConfiguration类后,果然是⼀个配置类,在这个类中其实它主要是⼲了⼀件事,配置spring的advisor(增强器)。这⾥的TransactionAttributeSource表⽰事务属性源,
它是⽤来⽣成事务相关的属性的,⽐如什么事务是否为只读啊,传播特性啊等等,都是通过这个接⼝来获取的,那这个接⼝有很多实现类,如图:
这⾥默认是⽤的AnnotationTransactionAttributeSource注解事务属性源,换句话说,这个类就是⽤来处理@Transactional注解的。
刚刚的ProxyTransactionManagementConfiguration配置类中还有⼀个bean,TransactionInterceptor事务,这个类才是真正的处理事务相关的⼀切逻辑的,可以看下⼀它的类图结构,
可以看到TransactionInterceptor继承了TransactionAspectSupport类和实现了MethodInterceptor接⼝,其中TransactionAspectSupport是提供事务⽀持的,MethodInterceptor是⽤来拦截加了@Transactional注解的⽅法的,职责分明。那这⾥知道了这个⽅法后我们就可以做⼀些骚操作了。
这⾥我们先回到我们的需求点上,我们要做的是实现程序⾃动读写分离,那么读写分离的本质是啥,不就是切换数据源么,我不会告诉你怎么实现多数据源切换的(我也不知道,动态数据源⽅案⽹上⼜是⼀⼤把的,但是有的是有坑的,⽐如为什么你配了动态数据源加上事务注解之后就⽆效了呢,去掉
事务注解⼜可以了,是不是很蛋疼。动态切换数据源的关键点在于:在适当的时机切换数据源)。那我这⾥的遇到的问题是⽆法拦截接⼝上的注解(其实你把注解放到实现类的⽅法上,啥事⼉都没了。但我这个⼈就是喜欢杠,⾮要放到接⼝⽅法上)
那怎么搞定这个问题呢,其实通过上⾯对事务源码的简单分析之后⼤致可以得出以下结论:
重写事务,在事务处理的前后加上⾃⼰的逻辑,切换数据源。然后将⾃⼰重写的事务设置到刚开始的 advisor 中就可以了
初步解决⽅案
重写事务
public class CustomInterceptor extends TransactionInterceptor {
private static final long serialVersionUID =1154144110124764905L;
public CustomInterceptor(PlatformTransactionManager ptm, TransactionAttributeSource tas){
super(ptm, tas);
}
@Override
public Object invoke(MethodInvocation invocation)throws Throwable {
Method());
Object invoke = null;
try{
invoke =super.invoke(invocation);
}finally{
after();
}
return invoke;
}
public void before(Method method){
//这⾥都拿到method对象了,那通过反射可以做的事情就很多了,
//能到这⾥来的,那⽅法上⾯肯定是有Transactional注解的,拿到它并获取相关属性,
//如果事务属性为只读的,那毫⽆疑问可以把它对数据的请求打到从库
Transactional transactional = Annotation(Transactional.class);
boolean readOnly = adOnly();
if(readOnly){
// 只读事务,切换到mysql的从库
changeDatasource(DatasourceType.SLAVE);
}else{
/
/ ⾮只读事务,切换到mysql主库
changeDatasource(DatasourceType.MASTER);
}
}
public void after(){
// 清除数据源设置
changeDatasource(DatasourceType.CLEAN);
}
private void changeDatasource(DatasourceType type){
//模拟数据源切换
System.out.println("\n\n\n==========================================================="); System.out.println("Datasource = "+ type);
System.out.println("===========================================================\n\n\n"); }
}
enum DatasourceType {
MASTER, SLAVE, CLEAN
}
设置
将⾃⼰重写后的事务设置到advisor中,将它默认的覆盖掉
@Configuration
public class TransactionConfig implements InitializingBean, BeanFactoryAware {
@Override
public void afterPropertiesSet()throws Exception {
// 获取增强器
BeanFactoryTransactionAttributeSourceAdvisor advisor = Bean(BeanFactoryTransactionAttributeSourceAdvisor.class);
PlatformTransactionManager platformTransactionManager = Bean(PlatformTransactionManager.class);
// spring原有的事务⽤的就是注解类型的事务属性源,那我们也⽤这个,不然你的事务注解就失效了,那不就⽩忙活了么
TransactionAttributeSource attributeSource =new AnnotationTransactionAttributeSource();
springboot aop// 实例化⾃⼰的事务
CustomInterceptor advice =new CustomInterceptor(platformTransactionManager, attributeSource);
// 把它原有的事务替换成⾃⼰的,因为你重写的事务拦截是继承它原有的,所以可以这么搞
advisor.setAdvice(advice);
}
private DefaultListableBeanFactory factory;
@Override
public void setBeanFactory(BeanFactory beanFactory)throws BeansException {
if(beanFactory instanceof DefaultListableBeanFactory){
this.factory =(DefaultListableBeanFactory) beanFactory;
}
}
}
到这⾥,对于接⼝上有事务注解的⽅法,我们已经可以动态的切换它的数据源了,⽽且还可以不⽤⾃定注解,直接⽤spring⾃带的注解就好。
那经过上⾯的⼀顿操作后,终于可以在事务的前后做⾃⼰的事情了。
从某种意义上来将,这个⽅案确实解决了接⼝⽅法上的注解问题,但也只是仅限于spring的事务注解。那对于本⽂标题所述的问题,在本质上并没有得到解决,因为事务这⾥是spring-transaction模块实现的注解处理,我们这⾥只是⽤了⼀种投机取巧的⽅法达到了⽬的⽽已。
通⽤解决⽅案(⾃定义的⽅法)
所谓通⽤解决⽅案就是模仿spring-transaction写⼀个⾃⼰的⽅法,那这⾥就不限于注解了,通过注解也是可以的,只不过除了接⼝⽅法上的注解⽆法直接通过spring aop拦截外,其他的⽅式好像都可以通过spring aop直接实现。
实现⼀个⾃定义的⽅法:
1. 你的bean需要是⼀个被ProxyFactoryBean创建的bean
2. 需要有⼀个Advisor对象(AbstractBeanFactoryPointcutAdvisor),然后把这个advisor对象设置到ProxyFactoryBean中
3. 需要有⼀个PointCut对象(StaticMethodMatcherPointcut),将其设置到 advisor 对象中
4. 需要有⼀个Advice对象(MethodInterceptor),将其设置到 advisor 对象中
demo 乞丐版
/**
* 业务接⼝
*/
interface Service {
void test1();
/**
* 打上标记,需要被拦截的⽅法
*/
@DataSource
void test2();
}
/**
* 业务实现
*/
class ServiceImpl implements Service {
@Override
public void test1(){
System.out.println("hello world");
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论