Spring Boot 在 @Configuration 中注入 @Service(需要被代理的 Component )導致 @Service 事務不可用的解決方案

1、背景

Spring Boot 項目中,我們有時候希望在 Interceptor 中注入的 @Service 組件,例如本人在後管項目中使用 Spring MVC Interceptor 做權限控制,此時 Interceptor 中需要使用 Service 中的方法讀取用戶權限信息,再配合 Controller 上的註解做權限控制,大體的配置步驟如下:

  1. AuthInterceptor 繼承 HandlerInterceptorAdapter,標註爲 @Component, 使用 Constructor 方式注入 Service(按照官方建議不使用 @Autowire,具體原因可以百度一下,這裏不做解釋)。
  2. 在實現了 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 如何在第一次調用的時候初始化呢(懶加載)?有如下方案:

  1. 我們可以監聽 Spring Boot 啓動完成事件,利用 ApplicationContext 獲取 Service 注入AuthInterceptor 實例,但代碼醜陋,而且不夠優雅,不用。
  2. 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 能夠優雅的就覺類似的問題。

如果上述解決方案對您有所幫助,記得幫忙點贊哦~~~

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章