我有一个Spring MVC应用程序,其中包含与单个Java包(控制器,服务,存储库,DTO和资源)中的单个业务问题相关的所有逻辑。我通过使表示,服务和持久层的所有方法都使用package-private(不使用接口)来强制执行此操作。注意:使用具有可选依赖项的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:package-private方法不从类注释继承事务信息,你必须将@Transactional
放在package-private方法本身上。
答案 0 :(得分: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
}
}
请阅读评论。代码很难看,因为它有很多泛型。此AbstractRepo
参数化为4种泛型:
AbstractRepo
为了使您的具体存储库工作并且类型安全,需要这些通用类型参数,这意味着如果您尝试使用错误的类型,它们将无法编译。
之后,在private
@PostConstruct
方法中,我们得到第三个泛型类型param T
的类,它是将通过Spring公开的repo的类型内在的阶级。我们需要这个Class<T>
,以便我们可以让Spring给我们这个类的bean。然后,我们将此bean分配给delegate
属性private
,并通过protected
repo()
方法进行访问。
最后,有一个内部类,其后代将由Spring代理。它定义了一些泛型类型约束和一些基本操作。它有一个特殊的构造函数,可以执行一些泛型魔法以获取实体的类。稍后你需要实体的类,要么将它传递给你的ORM(可能是一个Hibernate Session
),要么通过反射创建你的实体实例,并用从数据库中检索的数据填充它(也许是基本的JDBC方法或Spring JDBC)。
关于基本操作,我只绘制了load()
,它接收要加载的实体的id,是K
类型的id,并返回实体安全输入。
到目前为止一切顺利。您需要将这两个类放在一个包中,并且可以从应用程序的所有其他包和模块中访问它们,因为它们将被用作具体实体和repos的基类。
现在,在您应用的一个特定包中,定义一个示例实体:
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;
}
}
}
再次仔细阅读代码中的注释。此SampleRepo
可通过@Component
注释进行Spring组件扫描。它是public
,但根据您的要求,它的方法都是包私有的。
这个package-private方法没有在这个具体的SampleRepo
类中实现。相反,它们通过继承的protected
repo()
方法委托给要由Spring扫描的内部类。
此内部类不是public
。它的范围是包私有的,因此它对包外的类是不可见的。但是,它的方法是public
,因此Spring可以使用代理拦截它们。根据您的需要,此内部类使用@Repository
和@Transactional
进行注释。它扩展了AbstractRepo.SpringAbstractRepo
内部类,原因有两个:
load()
)。@Autowired
。否则,Spring无法加载应用程序。由于AbstractRepo.SpringAbstractRepo
abstract
内部类只有一个构造函数,并且此构造函数接受的参数必须是其AbstractRepo
abstract
封闭类的后代,因此AbstractRepo.SpringAbstractRepo
super()
封闭类的后代{1}}内部类需要在自己的构造函数中使用abstract
,传递相应封闭类的实例。这是由泛型强制执行的,因此如果您尝试传递错误类型的参数,则会出现编译错误。作为最终评论,{{1}}类不是必须的。你可以完全避免它们,以及所有这些泛型的东西,尽管你最终会有重复的代码。