1、背景
Spring Boot 項目中,我們有時候希望在 Interceptor 中注入的 @Service 組件,例如本人在後管項目中使用 Spring MVC Interceptor 做權限控制,此時 Interceptor 中需要使用 Service 中的方法讀取用戶權限信息,再配合 Controller 上的註解做權限控制,大體的配置步驟如下:
- AuthInterceptor 繼承 HandlerInterceptorAdapter,標註爲 @Component, 使用 Constructor 方式注入 Service(按照官方建議不使用 @Autowire,具體原因可以百度一下,這裏不做解釋)。
- 在實現了 WebMvcConfigurer 的配置類(類上標註了 @Configuration )子類中,將 AuthInterceptor 組件通過 Constructor 方式注入此配置類。並且覆寫 addInterceptors 接口,通過 InterceptorRegistry 將 AuthInterceptor 組件添加到 Spring MVC 攔截器。
2、問題
上述的步驟看似很完美,項目也能夠正常配置並且啓動,運行基本正常,但是存在一個很嚴重的問題,就是標註了 @Service 的服務通常會添加事務配置支持,既在啓動類上添加 @EnableTransactionManagement 即可,但是我們很快會發現,在 Interceptor 中注入的 Service 事務都不可用,並且該 Service 注入的 Controller,事務依然不可用,這是怎麼回事呢?
3、問題分析
Spring源碼分析--@Autowired注入的不是代理對象,導致事務回滾失敗(@Transactional無效) 這篇博客的作者已經分析過原因,總結起來原因是 @Configuration 中直接或者間接的注入的 Service 服務 A,導致 A 過早的被初始化,此時服務 A 並沒有被 AOP 增強,但是該服務需要被 AOP 增強以實現事務,因此導致了上述的問題中出現的現象!
遺憾的是,作者並沒有給出能解決此類問題的解決方案,筆者在 Baidu,Google 上嘗試搜索了一下,也沒有搜索到相關的解決方案(Google 上沒搜到大概是因爲自己英文很爛,表述不對),在 Spring 官方文檔也未找到相關解決方案。
4、解決方案
下面給出的解決方案思路其實很簡單,既然導致事務不可用的原因是 Service 過早的被初始化,而且我們也知道該 Service 真正被調用的實際肯定是應用已經完全啓動,也就是說配置已經完成,那我們可以想一個辦法讓 Component 中注入的 Service 在真正被調用的時候再初始化,這個時候的 Service 肯定已經支持事務攔截了!
那麼問題就來了,我們在 AuthInterceptor 中注入的 Service 如何在第一次調用的時候初始化呢(懶加載)?有如下方案:
- 我們可以監聽 Spring Boot 啓動完成事件,利用 ApplicationContext 獲取 Service 注入AuthInterceptor 實例,但代碼醜陋,而且不夠優雅,不用。
- AuthInterceptor 中注入 Service 的代理實現,實際的 Service 會在 Proxy Service 第一次被使用的時候初始化。代碼簡介,優雅。
5、具體實現
下面給出方案2的具體實現:
package com.qiwen.base.util;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public final class SpringHelper {
private static ApplicationContext applicationContext;
public static void setApplicationContext(ApplicationContext context) {
if (applicationContext == null) {
applicationContext = context;
}
}
public static ApplicationContext getApplicationContext() {
if(applicationContext == null) {
throw new UnsupportedOperationException("ApplicationContext 未初始化");
}
return applicationContext;
}
public static ApplicationContext getApplicationContext(ApplicationContext applicationContext) {
if(getApplicationContext() == null) {
setApplicationContext(applicationContext);
}
return getApplicationContext();
}
private static class LazyLoadProxyBean<T> implements MethodInterceptor {
// 禁止 JVM 重排
private volatile T targetObject;
private final Class<T> clazz;
private final String beanName;
public LazyLoadProxyBean(Class<T> clazz, String beanName) {
this.clazz = clazz;
this.beanName = beanName;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 第一次初始化使用的時候初始化
if (Objects.isNull(targetObject)) {
// 代理對象懶加載部分一次只允許一個線程訪問
synchronized(this) {
if(Objects.isNull(targetObject)) {
// 首先嚐試使用 name, name 沒有則使用 class
if(StringUtils.isEmpty(beanName)) {
targetObject = (T) SpringHelper.getRealBean(clazz);
} else {
targetObject = (T) SpringHelper.getRealBean(beanName);
}
}
}
}
Object result = method.invoke(targetObject, args);
return result;
}
}
private SpringHelper() { }
public static <T> T getLazyBean(final Class<T> clazz) {
return getLazyBean(clazz, null);
}
// 創建代理傳入類型的代理類
public static <T> T getLazyBean(final Class<T> clazz, final String beanName) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback(new LazyLoadProxyBean<T>(clazz, beanName));
T proxy = (T) enhancer.create();
return proxy;
}
public static <T> T getRealBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
public static <T> T getRealBean(String name) {
return (T) getApplicationContext().getBean(name);
}
public static <T> List<T> getRealBeansByType(Class<T> clazz) {
String[] beanNames = getApplicationContext().getBeanNamesForType(clazz);
List<T> beans = Arrays.stream(beanNames)
.map(name -> (T)getRealBean(name))
.collect(Collectors.toList());
return beans;
}
}
SpringHelper 中需要持有 ApplicationContext 實例, 可以通過如下配置初始化:
@Configuration
// 啓動優先級配置到最高
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ApplicationContextHolderConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringHelper.setApplicationContext(applicationContext);
}
}
使用示例如下:
@Component
public class AuthInterceptor extends HandlerInterceptorAdapter {
private final IRoleService roleService;
public AuthInterceptor () {
// 創建 LazyBean, 既創建一個 IRoleService 的代理對象,實際 IRoleService Bean
// 會在第一次被使用的使用初始化。
this.roleService = SpringHelper.getLazyBean(IRoleService.class);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
// ...
}
}
SpringHelper 依賴 Cglib,這裏需要引入如下 Maven 依賴:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.9</version>
</dependency>
Cglib 實現動態代理相較於 Java 提供的代理使用範圍更廣,不僅可以代理接口,而且可以代理非 final 類。
6、驗證
實際項目中通過 debug 模式啓動,斷點到監聽實際被初始化的 Service,發現能夠是正常支持事務的 Service,如下:
使用過程中事務也能夠被正常支持,如果項目使用了 Spring Cache 的也會存在上述類似的問題,所以通過 SpringHelper 能夠優雅的就覺類似的問題。
如果上述解決方案對您有所幫助,記得幫忙點贊哦~~~