Spring transactional package-private method

2020-07-22 16:34发布

问题:

I have a Spring MVC app with all logic related to a single business concern within a single Java package (controller, service, repository, DTO and resource). I enforce this by making all methods across presentation, service and persistence layers package-private (no interfaces are used). NB: layer separation is enforced with Maven modules with optional dependencies (presentation layer does not see persistence layer).

However, the repository shall also be @Transactional, and using Spring defaults (adding spring-tx Maven dependency + declaring @EnableTransactionManagement + creating a new DataSourceTransactionManager(dataSource) @Bean) isn't enough: the repository is no more proxified when it does not have at least one public method (I check this with AopUtils.isAopProxy() in an integration test).

What is the most straightforward way (minimal example) to solve this with Maven + annotation-based Spring + Tomcat? (I heard about AspectJ and would prefer to avoid it if another solution fits the need, because AspectJ seems complex to set up and is incompatible with Lombok --but I guess I could replace it with @AutoValue, custom aspects, Spring Roo, etc.)

EDIT: I attempted to use AspectJ and could so far add aspects (only using @Aspect i.e. without any transactions involved) to a package-private class with only package-private methods (using compile-time weaving). I'm currently stuck trying to do the same with @Transactional. When I make the class and its methods public and define @EnableTransactionalManagement, it works (getCurrentTransactionName() shows something). But as soon as I change to @EnableTransactionalManagement(mode = ASPECTJ), it does not work any more, even when the class and its methods remain public (getCurrentTransactionName() shows null). NB: proxyTargetClass is irrelevant when using AspectJ mode.

EDIT2: OK I managed to solve this with AspectJ, both with compile-time and load-time weaving. The critical information I was missing was the JavaDoc of AnnotationTransactionAspect: package-private methods do not inherit transactional information from class annotations, you must put @Transactional on the package-private method itself.

回答1:

First of all, a warning: this is a hack and a generics nightmare! Too much hassle, in my opinion, to satisfy your requirement of having only package-private methods in your repositories.

First, define an abstract entity to work with:

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
}

This is just an abstract entity with a generic key.

Then, define an abstract repository that works with abstract entities, which will be extended by all other repositories. This introduces some generics magic, so pay attention:

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
    }
}

Please read the comments. The code is ugly, since it has a lot of generics. This AbstractRepo is parameterized with 4 generic types:

  • K -> type of the key of the entity this repo will be in charged of
  • E -> type of the entity this repo will be in charged of
  • T -> type of the repo that will be exposed to Spring through an inner class, so that Spring proxying mechanism can take place while keeping your methods package-private in the enclosing class
  • U is the type of the subclass that will be extending this AbstractRepo

These generic type params are needed in order to make your concrete repos work and be type-safe, meaning they won't compile if you attempt to use a wrong type.

After that, in a private @PostConstruct method, we get the class of the third generic type param T, which is the type of the repo that will be exposed to Spring through the inner class. We need this Class<T>, so that we can ask Spring to give us a bean of this class. Then, we assign this bean to the delegate attribute, which is private and will be accessied via the protected repo() method.

At the end, there's the inner class whose descendants will be proxied by Spring. It defines some generic type constraints and some basic operations. It has a special constructor that does some generics magic in order to get the class of the entity. You'll need the class of the entity later, either to pass it to your ORM (maybe a Hibernate Session) or to create an instance of your entity by reflection and fill it with data retrieved from the database (maybe a basic JDBC approach or Spring JDBC).

Regarding basic operations, I've only sketched load(), which receives the id of the entity to load, being this id of type K, and returns the entity safely typed.

So far so good. You'd need to put these 2 classes in a package and module reachable from all other packages and modules of your application, since they will be used as the base classes for your concrete entities and repos.


Now, in one specific package of your app, define a sample entity:

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;
    }
}

This is just a sample entity with a data field, whose id is of type Long.

Finally, define a concrete SampleRepo that manages instances of 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;
        }
    }
}

Again, read carefully the comments in the code. This SampleRepo is available to Spring component scanning via the @Component annotation. It is public, though it's methods are all package-private, as per your requirement.

These package-private methods are not implemented in this concrete SampleRepo class. Instead, they're delegated via the inherited protected repo() method to the inner class that is to be scanned by Spring.

This inner class is not public. Its scope is package-private, so that it is not visible to classes outside the package. However, its methods are public, so that Spring can intercept them with proxies. This inner class is annotated with @Repository and @Transactional, as per your needs. It extends AbstractRepo.SpringAbstractRepo inner class for two reasons:

  1. All basic operations are automatically inherited (such as load()).
  2. For proxying, Spring needs this class to have a constructor that receives a bean of the enclosing class, and this argument must be @Autowired. Otherwise, Spring fails to load the application. As the AbstractRepo.SpringAbstractRepo abstract inner class has only one constructor, and this constructor accepts an argument that must be a descendant of its AbstractRepo abstract enclosing class, every descendant of the AbstractRepo.SpringAbstractRepo inner class will need to use super() in its own constructor, passing an instance of the corresponding enclosing class. This is enforced by generics, so if you attempt to pass an argument of the wrong type, you get a compilation error.

As a final comment, the abstract classes are not a must. You could perfectly avoid them, as well as all this generics stuff, though you would end up having duplicate code.