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 能够优雅的就觉类似的问题。

如果上述解决方案对您有所帮助,记得帮忙点赞哦~~~

 

 

 

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