草庐IT

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

梦在旅途 2023-04-02 原文

如果一个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方法,可以很灵活的实现按需获取代理或非代理对象。

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

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby-on-rails - 按天对 Mongoid 对象进行分组 - 2

    在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev

  3. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  4. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  5. ruby - 通过 erb 模板输出 ruby​​ 数组 - 2

    我正在使用puppet为ruby​​程序提供一组常量。我需要提供一组主机名,我的程序将对其进行迭代。在我之前使用的bash脚本中,我只是将它作为一个puppet变量hosts=>"host1,host2"我将其提供给bash脚本作为HOSTS=显然这对ruby​​不太适用——我需要它的格式hosts=["host1","host2"]自从phosts和putsmy_array.inspect提供输出["host1","host2"]我希望使用其中之一。不幸的是,我终其一生都无法弄清楚如何让它发挥作用。我尝试了以下各项:我发现某处他们指出我需要在函数调用前放置“function_”……这

  6. ruby-on-rails - 如何验证非模型(甚至非对象)字段 - 2

    我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss

  7. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  8. ruby - 通过 ruby​​ 进程共享变量 - 2

    我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是

  9. ruby - 通过 RVM (OSX Mountain Lion) 安装 Ruby 2.0.0-p247 时遇到问题 - 2

    我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search

  10. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

随机推荐