【问题标题】:Spring transactional package-private methodSpring事务包私有方法
【发布时间】:2015-03-10 11:39:23
【问题描述】:

我有一个 Spring MVC 应用程序,其中包含与单个 Java 包(控制器、服务、存储库、DTO 和资源)中的单个业务关注点相关的所有逻辑。我通过将表示层、服务层和持久层中的所有方法设为包私有(不使用接口)来强制执行这一点。注意:层分离是由具有可选依赖项的 Maven 模块强制执行的(表示层看不到持久层)。

然而,存储库也应该是@Transactional,并且使用 Spring 默认值(添加 spring-tx Maven 依赖项 + 声明 @EnableTransactionManagement + 创建一个 new DataSourceTransactionManager(dataSource) @Bean)是不够的:存储库已不复存在当它没有至少一个公共方法时被代理(我在集成测试中使用AopUtils.isAopProxy() 进行检查)。

使用 Maven + 基于注释的 Spring + Tomcat 解决此问题的最直接方法(最小示例)是什么? (我听说过 AspectJ,如果需要另一种解决方案,我宁愿避免使用它,因为 AspectJ 似乎设置起来很复杂并且与 Lombok 不兼容——但我想我可以用 @AutoValue、自定义方面、Spring Roo 等替换它.)

编辑:我尝试使用 AspectJ 并且到目前为止可以将方面(仅使用 @Aspect 即不涉及任何事务)添加到仅具有包私有方法(使用编译时编织)的包私有类中。我目前正试图对@Transactional 做同样的事情。当我公开该类及其方法并定义@EnableTransactionalManagement 时,它可以工作(getCurrentTransactionName() 显示了一些东西)。但是一旦我更改为@EnableTransactionalManagement(mode = ASPECTJ),它就不再起作用,即使类及其方法仍然是公开的(getCurrentTransactionName() 显示null)。注意:proxyTargetClass 在使用 AspectJ 模式时无关紧要。

EDIT2:好的,我设法用 AspectJ 解决了这个问题,包括编译时和加载时编织。我缺少的关键信息是 AnnotationTransactionAspect 的 JavaDoc:包私有方法不会从类注释中继承事务信息,您必须将 @Transactional 放在包私有方法本身上。

【问题讨论】:

  • 我自己没有尝试过,因为我更喜欢使用接口,但是您可以尝试探索<tx:annotation-driven>proxy-target-class属性
  • 使用注释,@Serge 所说的转换为@EnableTransactionManagement(mode = AdviceMode.PROXY, proxyTargetClass = true)。我也不确定这是否足以使 cglib 代理在您的情况下正常工作。
  • 不幸的是,这还不够,症状保持不变。
  • 恐怕没有直接的方法来完成你想要的(尽管这并不意味着根本没有办法)。你用的是java8吗?
  • 是的,Java 8 + Spring 4.1.5(最新可用)。如果没有直截了当的方法,我仍然希望找到最简单的方法。

标签: java spring maven spring-transactions


【解决方案1】:

首先,警告:这是一个黑客和泛型的噩梦!在我看来,要满足您在存储库中只有包私有方法的要求太麻烦了。

首先,定义一个要使用的抽象实体:

package reachable.from.everywhere;

import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

@MappedSuperclass
public abstract class AbstractEntity<K> {

    @Id
    private K id;

    // TODO other attributes common to all entities & JPA annotations

    public K getId() {
        return this.id;
    }

    // TODO hashCode() and equals() based on id
}

这只是一个带有通用键的抽象实体。

然后,定义一个与抽象实体一起工作的抽象存储库,它将被所有其他存储库扩展。这引入了一些泛型魔法,所以要注意:

package reachable.from.everywhere;

import java.lang.reflect.ParameterizedType;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;

public abstract class AbstractRepo<
    K, // key
    E extends AbstractEntity<K>, // entity
    T extends AbstractRepo.SpringAbstractRepo<K, E, U>, // Spring repo
    U extends AbstractRepo<K, E, T, U>> { // self type

    @Autowired
    private ApplicationContext context;

    private T delegate;

    @SuppressWarnings("unchecked")
    @PostConstruct
    private void init() {
        ParameterizedType type = 
            (ParameterizedType) this.getClass().getGenericSuperclass();
        // Spring repo is inferred from 3rd param type
        Class<T> delegateClass = (Class<T>) type.getActualTypeArguments()[2];
        // get an instance of the matching Spring repo
        this.delegate = this.context.getBean(delegateClass);
    }

    protected T repo() {
        return this.delegate;
    }

    protected static abstract class SpringAbstractRepo<K, E, U> {

        protected final Class<E> entityClass;

        // force subclasses to invoke this constructor
        // receives an instance of the enclosing class
        // this is just for type inference and also
        // because Spring needs subclasses to have
        // a constructor that receives the enclosing class
        @SuppressWarnings("unchecked")
        protected SpringAbstractRepo(U outerRepo) {
            ParameterizedType type = 
                (ParameterizedType) this.getClass().getGenericSuperclass();
            // Spring repo is inferred from 3rd param type
            this.entityClass = (Class<E>) type.getActualTypeArguments()[1];
        }

        public E load(K id) {
            // this method will be forced to be transactional!
            E entity = ...; 
            // TODO load entity with key = id from database
            return entity;
        }

        // TODO other basic operations
    }
}

请阅读 cmets。代码很丑,因为它有很多泛型。这个AbstractRepo 用 4 种泛型类型参数化:

  • K -> 此 repo 将负责的实体的密钥类型
  • E -> 此 repo 将负责的实体的类型
  • T -> 将通过内部类向 Spring 公开的 repo 类型,以便 Spring 代理机制可以发生,同时将您的方法包私有在封闭类中
  • U 是要扩展此AbstractRepo 的子类的类型

需要这些泛型类型参数才能使您的具体存储库正常工作并保证类型安全,这意味着如果您尝试使用错误的类型,它们将无法编译。

之后,在private@PostConstruct 方法中,我们得到第三个泛型类型参数T 的类,这是将通过内部类公开给Spring 的repo 的类型。我们需要这个Class&lt;T&gt;,这样我们就可以要求Spring给我们一个这个类的bean。然后,我们将此 bean 分配给 delegate 属性,即 private 并将通过 protected repo() 方法访问。

最后,内部类的后代将被 Spring 代理。它定义了一些泛型类型约束和一些基本操作。它有一个特殊的构造函数,可以执行一些泛型魔术以获取实体的类。稍后您将需要实体的类,将其传递给您的 ORM(可能是 Hibernate Session)或通过反射创建实体的实例并用从数据库检索的数据填充它(可能是基本的 JDBC方法或 Spring JDBC)。

关于基本操作,我只画了load(),它接收要加载的实体的id,即K类型的id,并返回安全输入的实体。

到目前为止一切顺利。您需要将这 2 个类放在一个包和模块中,该包和模块可从应用程序的所有其他包和模块访问,因为它们将用作具体实体和存储库的基类。


现在,在您应用的一个特定包中,定义一个示例实体:

package sample;

import reachable.from.everywhere.AbstractEntity;

public class SampleEntity
    extends AbstractEntity<Long> {

    private String data;

    public String getData() {
        return this.data;
    }

    public void setData(String data) {
        this.data = data;
    }
}

这只是一个带有data 字段的示例实体,其id 类型为Long

最后,定义一个具体的SampleRepo 来管理SampleEntity 的实例:

package sample;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;

import reachable.from.everywhere.AbstractRepo;

// annotation needed to detect inner class bean
@Component
public class SampleRepo
    extends AbstractRepo<
        Long, // key
        SampleEntity, // entity with id of type Long 
        SampleRepo.SampleSpringRepo,  // Spring concrete repo
        SampleRepo> { // self type

    // here's your package-private method
    String method(String s) {
        return this.repo().method(s);
    }

    // here's another package-private method
    String anotherMethod(String s) {
        return this.repo().anotherMethod(s);
    }

    // can't be public
    // otherwise would be visible from other packages
    @Repository
    @Transactional
    class SampleSpringRepo
        extends AbstractRepo.SpringAbstractRepo<
            Long, // same as enclosing class 1st param
            SampleEntity, // same as enclosing class 2nd param
            SampleRepo> { // same as enclosing class 4th param

        // constructor and annotation needed for proxying
        @Autowired
        public SampleSpringRepo(SampleRepo myRepo) {
            super(myRepo);
        }

        public String method(String arg) {
            // transactional method
            return "method - within transaction - " + arg;
        }

        public String anotherMethod(String arg) {
            // transactional method
            return "anotherMethod - within transaction - " + arg;
        }
    }
}

再次,仔细阅读代码中的 cmets。这个SampleRepo 可通过@Component 注解用于Spring 组件扫描。它是public,尽管它的方法都是包私有的,根据您的要求。

这些包私有方法没有在这个具体的SampleRepo 类中实现。相反,它们通过继承的 protected repo() 方法委托给 Spring 将扫描的内部类。

这个内部类不是public。它的范围是包私有的,因此它对包外的类不可见。但是,它的方法是public,这样Spring就可以用代理拦截它们。这个内部类根据您的需要使用@Repository@Transactional 进行注释。它扩展 AbstractRepo.SpringAbstractRepo 内部类有两个原因:

  1. 所有基本操作都是自动继承的(如load())。
  2. 对于代理,Spring 需要这个类有一个构造函数来接收封闭类的一个 bean,并且这个参数必须是@Autowired。否则,Spring 无法加载应用程序。由于 AbstractRepo.SpringAbstractRepo abstract 内部类只有一个构造函数,并且此构造函数接受的参数必须是其 AbstractRepo abstract 封闭类的后代,因此 AbstractRepo.SpringAbstractRepo 内部类的每个后代都需要使用super() 在其自己的构造函数中,传递相应封闭类的实例。这是由泛型强制执行的,因此如果您尝试传递错误类型的参数,则会出现编译错误。

作为最后的评论,abstract 类不是必须的。你可以完全避免它们,以及所有这些泛型的东西,尽管你最终会得到重复的代码。

【讨论】:

  • 这是一个非常有趣的方法(谢谢),但确实非常做作并且几乎无法使用。在那种情况下,我认为 AspectJ 会更符合要求。
  • @SébastienDubois 同意。试试 AspectJ。不过,我不知道最终的解决方案是否会变得更容易。特别注意当事务已经启动时,拦截器中要做什么,以及回滚行为、异常处理、方法链等。
猜你喜欢
  • 1970-01-01
  • 2021-10-31
  • 2010-09-25
  • 2018-03-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-10
相关资源
最近更新 更多