目錄
- 問題背景
- 問題現(xiàn)場(aop代碼)
- 源碼
- 初步解決方案
- 重寫事務(wù)攔截器
- 設(shè)置攔截器
- 通用解決方案(自定義的方法攔截器)
- demo 乞丐版
- Pro版
- 注意細(xì)節(jié)
問題背景
最近在spring-boot項目中做mysql讀寫分離時遇到了一些奇葩問題预伺,問題現(xiàn)象:通過常規(guī)的spring aop去攔截帶有自定義注解的方法時雕拼,發(fā)現(xiàn)只有注解寫在實現(xiàn)類上面時才有效愁憔,寫在接口上時卻不生效焰手。所用的spring-boot版本為1.x版本
問題現(xiàn)場(aop代碼)
@Aspect
@Component
@EnableAspectJAutoProxy
public class DataSourceAspect {
@Around("@annotation(com.xxx.DataSource)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 業(yè)務(wù)方法執(zhí)行之前設(shè)置數(shù)據(jù)源...
doingSomthingBefore();
// 執(zhí)行業(yè)務(wù)方法
Object result = joinPoint.proceed();
// 業(yè)務(wù)方法執(zhí)行之后清除數(shù)據(jù)源設(shè)置...
doingSomthingAfter();
return result;
}
}
這是一段非常普通的spring aop攔截器代碼,由于項目中使用的事務(wù)注解全部都是寫在接口的方法上的荠藤,所以我也就習(xí)慣性的把注解@DataSource
寫在接口的方法上伙单,一調(diào)試代碼,這時候發(fā)現(xiàn)spring aop根本就不鳥你哈肖,攔截器沒生效吻育。網(wǎng)上一通搜索后,發(fā)現(xiàn)遇到這個問題的人非常多淤井,答案也是五花八門布疼,有的說是spring-boot 1.x版本的bug,升級到2.x版本就可以了币狠。然后就屁顛屁顛的把spring-boot版本換成最新的2.3.0.RELEASE
版本游两,根本就沒用;也有人分析說aop代理的是spring的bean實例漩绵,然而接口很顯然是不能實例化的贱案,所以aop無法生效。查了很多止吐,都是分析為什么不起作用的轰坊,可能是我搜索的關(guān)鍵字不對的原因,就沒怎么看到有解決方案的帖子祟印。
同樣的寫在接口方法上的@Transactional
為什么就能生效呢(至于spring事務(wù)原理的解析這里就不講了,網(wǎng)上一大把)粟害?
源碼
通過@EnableTransactionManagement
進(jìn)去看了下spring事務(wù)的源碼蕴忆,
上圖中看到@EnableTransactionManagement
注解上導(dǎo)入了一個類,不知道干什么的悲幅,點進(jìn)去看看
TransactionManagementConfigurationSelector
繼承了AdviceModeImportSelector
套鹅,就是想加載別的類,在selectImports
方法返回的內(nèi)容就是要加載的類汰具,這里可以看到分別加載了AutoProxyRegistrar
卓鹿,ProxyTransactionManagementConfiguration
這兩個類,通過名字能猜出ProxyTransactionManagementConfiguration
這個類應(yīng)該是一個事務(wù)相關(guān)的配置類留荔,繼續(xù)點進(jìn)去看下
點開ProxyTransactionManagementConfiguration
類后吟孙,果然是一個配置類,在這個類中其實它主要是干了一件事,配置spring的advisor
(增強(qiáng)器)杰妓。這里的TransactionAttributeSource
表示事務(wù)屬性源藻治,它是用來生成事務(wù)相關(guān)的屬性的,比如什么事務(wù)是否為只讀啊巷挥,傳播特性啊等等桩卵,都是通過這個接口來獲取的,那這個接口有很多實現(xiàn)類倍宾,如圖:
這里默認(rèn)是用的AnnotationTransactionAttributeSource
注解事務(wù)屬性源雏节,換句話說,這個類就是用來處理@Transactional
注解的高职。
剛剛的ProxyTransactionManagementConfiguration
配置類中還有一個bean钩乍,TransactionInterceptor
事務(wù)攔截器产禾,這個類才是真正的處理事務(wù)相關(guān)的一切邏輯的,可以看下一它的類圖結(jié)構(gòu),
可以看到TransactionInterceptor
繼承了TransactionAspectSupport
類和實現(xiàn)了MethodInterceptor
接口黄伊,其中TransactionAspectSupport
是提供事務(wù)支持的,MethodInterceptor
是用來攔截加了@Transactional
注解的方法的,職責(zé)分明帕膜。那這里知道了這個方法攔截器后我們就可以做一些騷操作了危纫。
這里我們先回到我們的需求點上螃征,我們要做的是實現(xiàn)程序自動讀寫分離酗电,那么讀寫分離的本質(zhì)是啥,不就是切換數(shù)據(jù)源么划滋,我不會告訴你怎么實現(xiàn)多數(shù)據(jù)源切換的(我也不知道处坪,動態(tài)數(shù)據(jù)源方案網(wǎng)上又是一大把的根资,但是有的是有坑的,比如為什么你配了動態(tài)數(shù)據(jù)源加上事務(wù)注解之后就無效了呢稻薇,去掉事務(wù)注解又可以了,是不是很蛋疼胶征。動態(tài)切換數(shù)據(jù)源的關(guān)鍵點在于:在適當(dāng)?shù)臅r機(jī)切換數(shù)據(jù)源
)塞椎。那我這里的遇到的問題是無法攔截接口上的注解
(其實你把注解放到實現(xiàn)類的方法上,啥事兒都沒了睛低。但我這個人就是喜歡杠案狠,非要放到接口方法上)
那怎么搞定這個問題呢服傍,其實通過上面對事務(wù)源碼的簡單分析之后大致可以得出以下結(jié)論:
重寫事務(wù)攔截器,在事務(wù)處理的前后加上自己的邏輯骂铁,切換數(shù)據(jù)源吹零。然后將自己重寫的事務(wù)攔截器設(shè)置到剛開始的 advisor 中就可以了
初步解決方案
重寫事務(wù)攔截器
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 {
before(invocation.getMethod());
Object invoke = null;
try {
invoke = super.invoke(invocation);
} finally {
after();
}
return invoke;
}
public void before(Method method) {
//這里都拿到method對象了霍骄,那通過反射可以做的事情就很多了千康,
//能到這里來的杨蛋,那方法上面肯定是有Transactional注解的有缆,拿到它并獲取相關(guān)屬性的榛,
//如果事務(wù)屬性為只讀的督弓,那毫無疑問可以把它對數(shù)據(jù)的請求打到從庫
Transactional transactional = method.getAnnotation(Transactional.class);
boolean readOnly = transactional.readOnly();
if (readOnly) {
// 只讀事務(wù)猖吴,切換到mysql的從庫
changeDatasource(DatasourceType.SLAVE);
} else {
// 非只讀事務(wù)囊扳,切換到mysql主庫
changeDatasource(DatasourceType.MASTER);
}
}
public void after() {
// 清除數(shù)據(jù)源設(shè)置
changeDatasource(DatasourceType.CLEAN);
}
private void changeDatasource(DatasourceType type) {
//模擬數(shù)據(jù)源切換
System.out.println("\n\n\n===========================================================");
System.out.println("Datasource = " + type);
System.out.println("===========================================================\n\n\n");
}
}
enum DatasourceType {
MASTER, SLAVE, CLEAN
}
設(shè)置攔截器
將自己重寫后的事務(wù)攔截器設(shè)置到advisor
中烁挟,將它默認(rèn)的覆蓋掉
@Configuration
public class TransactionConfig implements InitializingBean, BeanFactoryAware {
@Override
public void afterPropertiesSet() throws Exception {
// 獲取增強(qiáng)器
BeanFactoryTransactionAttributeSourceAdvisor advisor = factory.getBean(BeanFactoryTransactionAttributeSourceAdvisor.class);
PlatformTransactionManager platformTransactionManager = factory.getBean(PlatformTransactionManager.class);
// spring原有的事務(wù)攔截器用的就是注解類型的事務(wù)屬性源婴洼,那我們也用這個,不然你的事務(wù)注解就失效了撼嗓,那不就白忙活了么
TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource();
// 實例化自己的事務(wù)攔截器
CustomInterceptor advice = new CustomInterceptor(platformTransactionManager, attributeSource);
// 把它原有的事務(wù)攔截器替換成自己的柬采,因為你重寫的事務(wù)攔截是繼承它原有的,所以可以這么搞
advisor.setAdvice(advice);
}
private DefaultListableBeanFactory factory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof DefaultListableBeanFactory) {
this.factory = (DefaultListableBeanFactory) beanFactory;
}
}
}
到這里且警,對于接口上有事務(wù)注解的方法粉捻,我們已經(jīng)可以動態(tài)的切換它的數(shù)據(jù)源了,而且還可以不用自定注解振湾,直接用spring自帶的注解就好杀迹。
那經(jīng)過上面的一頓操作后,終于可以在事務(wù)的前后做自己的事情了押搪。
從某種意義上來將树酪,這個方案確實解決了接口方法上的注解問題,但也只是僅限于spring的事務(wù)注解大州。那對于本文標(biāo)題所述的問題续语,在本質(zhì)上并沒有得到解決,因為事務(wù)這里是spring-transaction模塊實現(xiàn)的注解處理厦画,我們這里只是用了一種投機(jī)取巧的方法達(dá)到了目的而已疮茄。
通用解決方案(自定義的方法攔截器)
所謂通用解決方案就是模仿spring-transaction寫一個自己的方法攔截器,那這里就不限于注解了根暑,通過注解也是可以的力试,只不過除了接口方法上的注解無法直接通過spring aop攔截外,其他的方式好像都可以通過spring aop直接實現(xiàn)排嫌。
實現(xiàn)一個自定義的方法攔截器:
- 你的bean需要是一個被
ProxyFactoryBean
創(chuàng)建的bean - 需要有一個
Advisor
對象(AbstractBeanFactoryPointcutAdvisor
)畸裳,然后把這個advisor對象設(shè)置到ProxyFactoryBean
中 - 需要有一個
PointCut
對象(StaticMethodMatcherPointcut
),將其設(shè)置到 advisor 對象中 - 需要有一個
Advice
對象(MethodInterceptor
)淳地,將其設(shè)置到 advisor 對象中
demo 乞丐版
/**
* 業(yè)務(wù)接口
*/
interface Service {
void test1();
/**
* 打上標(biāo)記怖糊,需要被攔截的方法
*/
@DataSource
void test2();
}
/**
* 業(yè)務(wù)實現(xiàn)
*/
class ServiceImpl implements Service {
@Override
public void test1() {
System.out.println("hello world");
}
@Override
public void test2() {
System.out.println("I'm doing something in DB");
}
}
/**
* 方法攔截標(biāo)記
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@interface DataSource {
}
class DataSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {
private DataSourcePointCut pointCut;
public void setPointCut(DataSourcePointCut pointCut) {
this.pointCut = pointCut;
}
@Override
public Pointcut getPointcut() {
return pointCut;
}
}
class DataSourceInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String name = invocation.getMethod().getName();
System.out.println("==============" + name + " before ================");
Object result = invocation.proceed();
System.out.println("==============" + name + " after ================");
return result;
}
}
class DataSourcePointCut extends StaticMethodMatcherPointcut {
/**
* 方法匹配器帅容,這個才是真正起作用的主
*
* @param method
* @param targetClass
* @return
*/
@Override
public boolean matches(Method method, Class<?> targetClass) {
return method.isAnnotationPresent(DataSource.class);
}
}
/**
* 單元測試
*/
public class MethodInterceptorTest {
private ProxyFactoryBean proxyFactoryBean;
@BeforeEach
public void before() {
// 0. 通過某種手段拿到一個bean實例,這里簡單點new一個
Service service = new ServiceImpl();
// 1. 創(chuàng)建一個代理工廠bean
ProxyFactoryBean pfb = new ProxyFactoryBean();
// 2. 設(shè)置哪個對象需要被代理
pfb.setTarget(service);
// 3. 初始化 advisor
DataSourceAdvisor advisor = new DataSourceAdvisor();
// 4. 設(shè)置pointcut
advisor.setPointCut(new DataSourcePointCut());
// 5. 設(shè)置方法攔截器
advisor.setAdvice(new DataSourceInterceptor());
// 6. 將advisor添加到代理中
pfb.addAdvisor(advisor);
proxyFactoryBean = pfb;
}
@Test
public void test() {
// 通過代理生成 service 實例
Service proxy = (Service) proxyFactoryBean.getObject();
proxy.test1();
System.out.println("\n\n");
proxy.test2();
}
}
測試結(jié)果如下:
可以看到成功的攔截到了service#test2
方法伍伤。實現(xiàn)方法攔截就這么幾個步驟并徘。
這是一個bean的情況,但是在實際的企業(yè)級開發(fā)中扰魂,這么寫很顯然不現(xiàn)實麦乞,在實際開發(fā)中要是這么寫,那就離拎盒飯不遠(yuǎn)了...
Pro版
在這個版本中我們只需要解決一件事情阅爽,那就是讓spring能夠自動為我們創(chuàng)建ProxyFactoryBean
@SpringBootApplication
public class DefaultProxyCreatorApplication {
public static void main(String[] args) {
SpringApplication.run(DefaultProxyCreatorApplication.class, args);
}
}
public interface Test2Service {
/**
* 被標(biāo)記的方法
*/
@Tx
void a();
void b();
void c();
}
@Service
public class Test2ServiceImpl implements Test2Service {
@Override
public void a() {
System.out.println("test2 method a");
}
/**
* 被標(biāo)記的方法
*/
@Tx
@Override
public void b() {
System.out.println("test2 method b");
}
@Override
public void c() {
System.out.println("test2 method c");
}
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Tx {
}
public class TxInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String name = invocation.getMethod().getName();
System.out.println(String.format("------------%s: before----------", name));
Object object = invocation.proceed();
System.out.println(String.format("------------%s: after----------", name));
return object;
}
}
public class TxMethodPointcutAdvisor extends StaticMethodMatcherPointcutAdvisor {
/**
* 攔截規(guī)則:
* 1: 接口類名上有 @Tx 注解
* 2: 接口方法名上有 @Tx 注解
*
* @param method
* @param targetClass
* @return
*/
@Override
public boolean matches(Method method, Class<?> targetClass) {
return methodCanPass(method) || classCanPass(method.getDeclaringClass());
}
private boolean methodCanPass(Method method) {
return method.isAnnotationPresent(Tx.class);
}
private boolean classCanPass(Class<?> clazz) {
return clazz.isAnnotationPresent(Tx.class);
}
}
@Configuration
public class AopConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
// 這個類就是自動代理創(chuàng)建器路幸,能夠自動的為每個bean生成代理
return new DefaultAdvisorAutoProxyCreator();
}
@Bean
public TxMethodPointcutAdvisor methodPointcutAdvisor(TxInterceptor txInterceptor) {
TxMethodPointcutAdvisor advisor = new TxMethodPointcutAdvisor();
advisor.setAdvice(txInterceptor);
return advisor;
}
@Bean
public TxInterceptor methodInterceptor() {
return new TxInterceptor();
}
}
創(chuàng)建一個單元測試驗證功能
@SpringBootTest
class TxInterceptorTest {
@Autowired
private Test2Service test2Service;
@Test
void test1() {
test2Service.a();
System.out.println("\n");
test2Service.b();
System.out.println("\n");
test2Service.c();
}
}
單元測試結(jié)果如下:
可以看到service#a
和service#b
這兩個方法都被攔截到了。其中方法 a 的注解在接口上付翁,方法b的注解在實現(xiàn)類上简肴,可見這已經(jīng)達(dá)到了我的目的,成功的攔截到了接口方法上的注解百侧。
注意細(xì)節(jié)
那如果說僅僅將上面的配置代碼復(fù)制到項目中去用的話砰识,是可以攔截接口方法注解的,但是如果要和spring的事務(wù)注解一起用的話佣渴,那么你可能要失望了辫狼,因為它會先經(jīng)過事務(wù)的攔截,然后才到你的自定義攔截器辛润,要解決這個問題很簡單膨处,將advisor設(shè)置一個執(zhí)行順序就可以了
@Bean
public TxMethodPointcutAdvisor methodPointcutAdvisor(TxInterceptor txInterceptor) {
TxMethodPointcutAdvisor advisor = new TxMethodPointcutAdvisor();
advisor.setAdvice(txInterceptor);
advisor.setOrder(1);//設(shè)置順序,值越小砂竖,優(yōu)先級越高真椿,也就是越被先執(zhí)行
return advisor;
}
那這個值是怎么取的呢,難道設(shè)置成1就一定會被先執(zhí)行么乎澄,
從這里可以看到spring事務(wù)的advisor執(zhí)行順序值為Integer的最大值突硝,所以也就是你隨便設(shè)置一個值(只要它不是Integer.MAX_VALUE),它都會比spring事務(wù)攔截器先執(zhí)行置济。