任何Bean通过实现ProxyableBeanAccessor接口即可获得动态灵活的获取代理对象或原生对象的能力

Posted 梦在旅途

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了任何Bean通过实现ProxyableBeanAccessor接口即可获得动态灵活的获取代理对象或原生对象的能力相关的知识,希望对你有一定的参考价值。

如果一个BEAN类上加了@Transactional,则默认的该类及其子类的公开方法均会开启事务,但有时某些业务场景下某些公开的方法可能并不需要事务,那这种情况该如何做呢?

常规的做法:

针对不同的场景及事务传播特性,定义不同的公开方法【哪怕是同一种业务】,并在方法上添加@Transactional且指明不同的传播特性,示例代码如下:

@Service
@Transactional
public class DemoSerivce 
 
   //SUPPORTED 若无事务传播则默认不会有事务,若有事务传播则会开启事务
   @Transactional(propagation = Propagation.SUPPORTED)
   public int getValue()
      return 0;
   
 
 
   //默认开启事务(由类上的@Transactional决定的)
   public int getValueWithTx()
      return 0;
   
 
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
   public int getValueWithoutTx()
      return 0;
   
 

上述这样的弊端就是:若是同一个逻辑但如果要精细控制事务,则需要定义3个方法来支持,getValue、getValueWithTx、getValueWithoutTx 3个方法,分别对应3种事务场景,这种代码就显得冗余过多,那有没有简单一点的方案呢?其实是有的。

声明式事务的本质是通过AOP切面,在代理执行原始方法【即:被标注了@Transactional的公开方法】前开启DB事务,在执行后提交DB事务(若抛错则执行回滚),如果要想事务不生效,则让AOP失效即可,即:调原生的service Bean公开方法而不是代理类的公开方法,那如何获得原生的BEAN类呢,答案是:在原生BEAN方法内部通过this获取即可,如果没理解,下面写一个手写代理示例,大家就明白了:

public class DemoService
 
    public int getValue()
        return 666;
    
 
    public DemoService getReal()
        //这里的this指向的就是当前的自己,并非代理对象
        return this;
    

 
public class DemoServiceProxy
    private DemoService target;
 
    public DemoServiceProxy(DemoService target)
        this.target=target;
    
 
    public int getValue()
        //增强:开启事务
        return  target.getValue()+222;
        //增强:提交事务
    
 
    public DemoService getReal()
        //这里就会间接的把原生的对象传递返回
        return target.getReal();
    

 
    public static void main(String[] args) 
        DemoServiceProxy proxy=new DemoServiceProxy(new DemoService());
        System.out.println("proxy class:" +proxy.getClass().getName());
        System.out.println("real class:" +proxy.getReal().getClass().getName());
 
        System.out.println("proxy getValue result:" + proxy.getValue() );
        System.out.println("real getValue result:" + proxy.getReal().getValue() );
 
    

最终的输出结果为:

proxy class:...DemoServiceProxy
real class:...DemoService →原始的对象

proxy getValue result:888
real getValue result:666

通过DEMO证实了通过避开代理的方案是正确的,而且非常简单,那么有了这个基础,再应用到实际的代码中则很简单,想控制有事务则取代理对象,想控制不要事务则取原生对象即可,就是这么简单。

下面贴出核心也是全部的ProxyableBeanAccessor代码:(注意必需扩展自RawTargetAccess,否则即使返回this也会被强制返回代理)

/**
 * @author zuowenjun
 * @date 2022/12/5 22:03
 * @description 可代理BEAN访问者接口(支持获取代理的真实对象、获取代理对象)
 */
public interface ProxyableBeanAccessor<T extends ProxyableBeanAccessor> extends RawTargetAccess 
 
    String CONTEXT_KEY_REAL_GET = "proxyable_bean_accessor_real_get";
 
    /**
     * 获取代理的真实对象(即:未被代理的对象)
     * 注:若调用未被代理的bean的公开方法,则均不会再走AOP切面
     *
     * @return 未被代理的对象Bean
     */
    @SuppressWarnings("unchecked")
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    default T getReal() 
        return (T) this;
    
 
    /**
     * 获取当前类的代理对象(即:已被代理的对象)
     * 注:若调用已被代理的对象Bean的公开方法,则相关AOP切面均可正常拦截与执行
     *
     * @return 已被代理的对象Bean
     */
    @SuppressWarnings("unchecked")
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    default T getProxy() 
        return (T) SpringUtils.getBean(this.getClass());
    
 
    /**
     * 将当前BEAN转换为代理对象或真实对象
     *
     * @param realGet 是否转换获取真实对象
     * @return 未被代理的对象Bean OR 已被代理的对象Bean
     */
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    default T selfAs(Supplier<Boolean> realGet) 
        Boolean needGetReal = false;
        if (realGet == null) 
            if (ContextUtils.get() != null) 
                needGetReal = (Boolean) ContextUtils.get().getGlobalVariableMap().getOrDefault(CONTEXT_KEY_REAL_GET, false);
            
         else 
            needGetReal = realGet.get();
        
 
        return Boolean.TRUE.equals(needGetReal) ? getReal() : getProxy();
    

其中,SpringUtils是一个获取BEAN的工具类,代码如下:

public SpringUtils implements ApplicationContextAware

private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException 
        context=applicationContext;
    

  public static <T> getBean(Class<T> clazz)
     return context.getBean(clazz);
   


ContextUtils只是一个内部定义了一个ThreadLocal的静态map字段,用于存放线程上下文要传递的对象。

使用方法:只需将原来Service的子类或其它可能被切面代理的类 加上实现自ProxyableBeanAccessor即可,然后在这个类里面或外部调用均可通过getReal获得原生对象、getProxy获得代理对象、selfAs动态根据条件来判断是否需要代理或原生对象,使用示例如下:

//serive BEAN定义
@Service
@Transactional
public class DemoService implements ProxyableBeanAccessor<DemoService> 
 
   ... ...

    public Demo selectByMergerParam(Demo demo)
       return getMapper().selectByMergerParam(demo);
    
 
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public Demo selectByMergerParam2(Demo demo)
        //通过getProxy获取当前类的代理BEAN,以便可以执行事务切面
        return getProxy().doSelectByMergerParam(demo);
    
 
    public Demo doSelectByMergerParam(Demo demo)
        return getMapper().selectByMergerParam(demo);
    
 
 

 
 
//具体使用:
 
 
    @Autowired
    private DemoService demoService;
 
 
            Demo query = new Demo ();
            query.setWaybillNumber("123455667");
 
            //示例一:获取原生对象查询,由于没有代理,则无事务
            demoService.getReal().selectByMergerParam(query);
 
            //示例二:根据LAMBDA表达式的布尔值来决定是否获取原生或代理(这里演示:如果是TIDB,则获取原生对象,即:不开事务)
            demoService.selfAs(()-> TidbDataSourceSwitcher.isUsingTidbDataSource()).selectByMergerParam(query);
 
            //示例三:根据线程上下文设置的布尔值来决定是否获取原生或代理(这里演示:如果是TIDB,则获取原生对象,即:不开事务),这种方式主要是为了简化大批量的动态逻辑判断的场景,
            // 一次设置同线程的所有ProxyableBeanAccessor的子类的selfAs(null)均可自动判断
            ContextUtils.get().addGlobalVariable(ProxyableBeanAccessor.CONTEXT_KEY_REAL_GET,TidbDataSourceSwitcher.isUsingTidbDataSource());
            demoService.selfAs(null).selectByMergerParam(query);
 
            //示例四:获取代理对象,一般用于BEAN内部方法之间调用,外部调用其实本身就是代理无意义
            demoService.getProxy().selectByMergerParam(query);

通过上述示例代码可以看到,借助于ProxyableBeanAccessor接口默认实现的getReal、getProxy、selfAs方法,可以很灵活的实现按需获取代理或非代理对象。

Spring Boot:ReactiveCrudRepository 没有被任何 bean 实现

【中文标题】Spring Boot:ReactiveCrudRepository 没有被任何 bean 实现【英文标题】:Spring Boot: ReactiveCrudRepository is not being implemented by any bean 【发布时间】:2018-05-28 22:54:25 【问题描述】:

我计划使用 Cassandra 响应式地保存数据。为此,我编写了以下界面:

@Repository
public interface ContributorStatRepository extends ReactiveCrudRepository<ContributorStat, Long> 
    Flux<ContributorStat> findByAkonId(String akonId);

上面的异常被抛出:

com.example.sample.controller.ContributorStatControllerTest > shouldReturnBadRequestWithBlankContributorStat FAILED
    java.lang.IllegalStateException
        Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException
            Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException
                Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException

你知道为什么没有为ContributorStatRepository 创建合适的bean吗?

我正在使用 Spring boot 2.0.0.M7 和这些依赖项:

dependencies 
    compile('org.springframework.boot:spring-boot-starter-actuator')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('javax.xml.bind:jaxb-api:2.3.0')
    compile('org.springframework.boot:spring-boot-starter-webflux')
    compile('org.springframework.boot:spring-boot-starter-data-cassandra-reactive')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('io.projectreactor:reactor-test')

更新: 运行测试:

@Test
public void shouldReturnBadRequestWithBlankContributorStat() throws Exception 
    requestPayload = mapper.writeValueAsString(new ContributorStatDTO());

    this.mockMvc.perform(post(CONTRIBUTOR_STATS_ROUTE)
            .contentType(MediaType.APPLICATION_JSON)
            .accept(MediaType.APPLICATION_JSON)
            .content(requestPayload)).andDo(print())
            .andExpect(status().isBadRequest());

【问题讨论】:

您能提供更多信息吗?您的存储库接口看起来很奇怪(声明了两倍的扩展接口?)并且您没有显示您正在运行的测试类。另外,为什么要在组合中添加 spring-data-jpa? @BrianClozel 哦,我忘了删除注释行。刚刚更新了。感谢您的观察。包含 spring-data-jpa 以创建另一个非反应性存储库。我包含了测试,但实际上所有测试都失败了,因为没有找到该存储库的 bean。 @Jovanny 你有没有在任何地方添加@Enable*Repositories?例如。喜欢this example? @Brian,是的,我有一个标记为@Configuration@EnableReactiveCassandraRepositories 的配置类 【参考方案1】:

您似乎正在使用 @WebMvcTest 带注释的类来测试此案例(不过我不确定,您的问题中缺少该部分)。

要测试 Spring WebFlux 应用程序,您应该使用@WebFluxTest(请参阅reference documentation)。即使你这样做了,ContributorStatRepository bean 也不会存在,因为 web 测试切片只会考虑你应用程序的 web 部分,你通常需要用 @MockBean 模拟这个。

【讨论】:

以上是关于任何Bean通过实现ProxyableBeanAccessor接口即可获得动态灵活的获取代理对象或原生对象的能力的主要内容,如果未能解决你的问题,请参考以下文章

JAVA bean为何要实现序列化

spring的annotation

spring服务定位器,可在任何地方获取bean

通过实现ApplicationContextAware接口动态获取bean

springmvc请求处理方法中有多个Bean类型参数解决方法(使用封装类将多个Bean设为属性,通过对封装类的注入实现多个Bean注入)

Spring 通过注解定义Bean以及自动扫描注解定义的bean ComponentScan 自动扫描组件&指定扫描规则