Error when adding custom revision in Hibernate envers

9.7k views Asked by At

When I add custom revision entity, I start getting error:

2020-12-13 00:22:29.418 ERROR 80983 --- [ost-startStop-1] o.s.b.web.embedded.tomcat.TomcatStarter  : Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException. Message: Error creating bean with name 'webSecurityConfig': Unsatisfied dependency expressed through field 'userDetailsService'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userDetailsServiceImpl': Unsatisfied dependency expressed through field 'userRepository'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRepository': Cannot create inner bean '(inner bean)#4384acd' of type [org.springframework.orm.jpa.SharedEntityManagerCreator] while setting bean property 'entityManager'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#4384acd': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is java.lang.NoClassDefFoundError: org/hibernate/resource/beans/spi/ManagedBeanRegistry

MyRevision:

package ...;

import org.hibernate.envers.DefaultRevisionEntity;
import org.hibernate.envers.RevisionEntity;

import javax.persistence.Entity;

@Entity
@RevisionEntity(MyRevisionListener.class)
public class MyRevision extends DefaultRevisionEntity {

    private String username;

    public String getUsername() { return username; }

    public void setUsername(String username) { this.username = username; }
}

MyRevisionListener:

package ...;

// import de.xxxxx.carorderprocess.models.User;
import org.hibernate.envers.RevisionListener;
// import org.springframework.security.core.Authentication;
// import org.springframework.security.core.context.SecurityContext;
// import org.springframework.security.core.context.SecurityContextHolder;

// import java.util.Optional;

public class MyRevisionListener implements RevisionListener {

    @Override
    public void newRevision(Object revisionEntity) {

        /* String currentUser = Optional.ofNullable(SecurityContextHolder.getContext())
                .map(SecurityContext::getAuthentication)
                .filter(Authentication::isAuthenticated)
                .map(Authentication::getPrincipal)
                .map(User.class::cast)
                .map(User::getUsername)
                .orElse("Unknown-User"); */

        MyRevision audit = (MyRevision) revisionEntity;
        audit.setUsername("dd");

    }
}

WebSecurityConfig:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserDetailsServiceImpl userDetailsService;

UserDetailsServiceImpl:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    UserRepository userRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));

        return UserDetailsImpl.build(user);
    }

}

UserRepository:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);

    Boolean existsByUsername(String username);
}

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>de.xxxxxxx</groupId>
    <artifactId>carorderprocess</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>carorderprocess</name>
    <description>Demo project for Spring Boot</description>

    <dependencyManagement>
        <dependencies>

        </dependencies>
    </dependencyManagement>


    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.17</version>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.16</version>
            <scope>provided</scope>
        </dependency>


        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>persistence-api</artifactId>
            <version>1.0.2</version>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.5.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-envers</artifactId>
            <version>2.4.1</version>
        </dependency>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-envers</artifactId>
            <version>5.4.25.Final</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <executions>
                    <execution>
                        <id>compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>
3

There are 3 answers

22
jccampanero On BEST ANSWER

I think your problem could be related with the different dependencies in your pom.xml.

Please, first, remove the spring-data-envers dependency, unless you are querying your audit tables you do not need it. Even in that case, you can use Envers on its own to obtain that information if required.

Be aware that, as indicated in the comments of the answer from Sunit, you will need to remove the attribute repositoryFactoryBeanClass, it could not longer take the value EnversRevisionRepositoryFactoryBean. But you probably still need to include the @EnableJpaRepositories annotation.

Although I initially indicated that you can let Spring Boot manage your versions, due to the one of spring-boot-starter-parent, the framework is providing you versions of hibernate-xxx similar to 5.2.17.Final.

But, as you indicated, you need to use the method forRevisionsOfEntityWithChanges for querying your audit entities. As you can see in the java docs, that method was introduced in AuditQueryCreator in version 5.3.

As a consequence, you need to provide the following dependency:

<dependency> 
  <groupId>org.hibernate</groupId> 
  <artifactId>hibernate-envers</artifactId> 
  <version>5.3.20.Final</version> 
</dependency>

But in addition you also need to provide a compatible version of both hibernate-entitymanager and hibernate-core:

<dependency> 
  <groupId>org.hibernate</groupId> 
  <artifactId>hibernate-entitymanager</artifactId> 
  <version>5.3.20.Final</version> 
</dependency>

<dependency> 
  <groupId>org.hibernate</groupId> 
  <artifactId>hibernate-core</artifactId> 
  <version>5.3.20.Final</version> 
</dependency>
8
Sunit Chatterjee On

Your code looks fine. But it may not be sufficient to identify the root cause.

Looking at the exception it is clear that application is failing since it is not able to find bean dependency

Could you try following

  1. Check your library imports first in your build.gradle or pom.xml. Generally you should not require any other Hibernate library other than Spring Boot Data JPA and Hibernate Envers

  2. Try removing/disabling the Hibernate Envers audit code and library dependencies and see if can you get your application up and running. This will help you identify if error is due to Hibernate Envers or if your application code has other issues.

If above does not works, then please provide more information

  1. Which version of Spring Boot are you on
  2. What libraries have you imported (build.gradle or maven pom file)
  3. What other Configurations you have in your project - do you have any other JPA configuration file or any other custom configuration related to Hibernate or JPA
  4. What annotations are on the main application class
  5. Directory structure of your Repository class, and the directory on which you do component scan (in case you have overridden it)
4
Sunit Chatterjee On

From what I understood from all the comments above, your requirement is

  • to use Envers Auditing
  • and use method forRevisionsOfEntityWithChanges to get list of all revisions with what changed in them

Please start by doing these

  • Remove dependency of spring-data-envers library.
  • Just keep library hibernate-envers - version 5.4.23.Final also worked for me
  • Remove repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class from @EnableJpaRepositories annotation
  • All Repository classes should only extend from JpaRespository and NOT from RevisionRepository. You dont need RevisionRepository

You should be able to get your application up and running now.

Now coming back to the question, how to get all revisions with changes using forRevisionsOfEntityWithChanges method.

  • Create an AuditConfiguration class like this, to create the AuditReader bean

     @Configuration
     public class AuditConfiguration {
    
     private final EntityManagerFactory entityManagerFactory;
    
     AuditConfiguration(EntityManagerFactory entityManagerFactory) {
         this.entityManagerFactory = entityManagerFactory;
     }
    
     @Bean
     AuditReader auditReader() {
         return AuditReaderFactory.get(entityManagerFactory.createEntityManager());
     }
    

    }

  • In your AuditRevisionEntity class, add following annotation. Without this the serialization of this class wont work. e.g

    @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
    public class AuditRevisionEntity extends DefaultRevisionEntity {
    
  • In your entity class add option withModifiedFlag = true to @Audited annotation. Without this you cannot get entity revisions with all changes. e.g

     @Audited(withModifiedFlag = true)
     public class Customer {
    
  • Modify your database table for this entity audit table and fields *_mod. e.g if you have a customer table with fields name, age, address columns, then add columns name_mod, age_mod, address_mod to the customer_audit table

  • Last, add following code in your service method to get audit revisions with changes

     @Autowired
     private AuditReader auditReader;
    
     public List<?> getRevisions(Long id) {
     AuditQuery auditQuery = auditReader.createQuery()
                 .forRevisionsOfEntityWithChanges(Customer.class, true)
             .add(AuditEntity.id().eq(id));
     return auditQuery.getResultList();
    

    }

I will try to post the same code in Github sometime today, so that you can take a look at working code.