Hibernate Envers: disable auditing temporarily - java

I've created an entities copier in a project where envers is enable but for this copier I don't need the auditing: is there a way to disable temporarily the envers auditing?
I know that there are listener that works as interceptors (before the audit trigger) but I need also know where auditing is triggered from (for example the controller that call the service where auditing is triggered).
I don't know if it's possible.
Thanks

Envers registers a set of event listeners that are triggered by Hibernates Event system.
In order to implement conditional auditing (see envers documentation), you need to replace these listeners with your own implementation. The following event types trigger enver's listeners:
EventType.POST_INSERT
EventType.PRE_UPDATE
EventType.POST_UPDATE
EventType.POST_DELETE
EventType.POST_COLLECTION_RECREATE
EventType.PRE_COLLECTION_REMOVE
EventType.PRE_COLLECTION_UPDATE
Since you want to skip auditing only on copied entities you only need to replace org.hibernate.envers.event.spi.EnversPostInsertEventListenerImpl.
In your entity you add a transient field that acts as a flag. This way you can determine within your listener implementation if the entity was copied.
public class YourEntity {
#Transient
private boolean copy;
public boolean isCopy() {
return copy;
}
public void setCopy(boolean copy) {
this.copy = copy;
}
}
Then you implement your listener by extending the default one:
public class CustomPostInsertListener extends EnversPostInsertEventListenerImpl {
public CustomPostInsertListener(EnversService enversService) {
super(enversService);
}
#Override public void onPostInsert(PostInsertEvent event) {
if (event.getEntity() instanceof YourEntity
&& ((YourEntity) event.getEntity()).isCopy()) {
// Ignore this entity
return;
}
super.onPostInsert(event);
}
}
In your copier you need to set the copy flag for your entities.
public YourEntity copyEntity(YourEntity entityToCopy){
YourEntity newEntity;
//... your copy logic
newEntity.setCopy(true);
return newEntity;
}
Then you need to create your own implementation of org.hibernate.integrator.spi.Integrator. The Easiest is to extend org.hibernate.envers.event.spi.EnversIntegrator:
package com.your.project.audit;
public class EnversCustomIntegrator extends EnversIntegrator {
public static final String AUTO_REGISTER = "hibernate.listeners.envers.autoRegister";
private AuditConfiguration enversConfiguration;
#Override
public void integrate(org.hibernate.cfg.Configuration configuration, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
final EventListenerRegistry listenerRegistry = serviceRegistry.getService(EventListenerRegistry.class);
listenerRegistry.addDuplicationStrategy(EnversListenerDuplicationStrategy.INSTANCE);
enversConfiguration = AuditConfiguration.getFor(configuration, serviceRegistry.getService(ClassLoaderService.class));
if (enversConfiguration.getEntCfg().hasAuditedEntities()) {
listenerRegistry.appendListeners(EventType.POST_DELETE, new EnversPostDeleteEventListenerImpl(enversConfiguration));
listenerRegistry.appendListeners(EventType.POST_INSERT, new CustomPostInsertListener(enversConfiguration));
listenerRegistry.appendListeners(EventType.POST_UPDATE, new EnversPostUpdateEventListenerImpl(enversConfiguration));
listenerRegistry.appendListeners(EventType.POST_COLLECTION_RECREATE, new EnversPostCollectionRecreateEventListenerImpl(enversConfiguration));
listenerRegistry.appendListeners(EventType.PRE_COLLECTION_REMOVE, new EnversPreCollectionRemoveEventListenerImpl(enversConfiguration));
listenerRegistry.appendListeners(EventType.PRE_COLLECTION_UPDATE, new EnversPreCollectionUpdateEventListenerImpl(enversConfiguration));
}
}
}
Lastly you need to add your Integrator implementation in the META-INF/services/org.hibernate.integrator.spi.Integrator file.
com.your.project.audit.EnversCustomIntegrator

Related

Sending Spring domain events using a custom ApplicationEventPublisher

I'm trying to implement an application using Spring Boot/Spring Data, following DDD architecture guidelines. I have an Aggregate Root which publish domain events using the method AbstractAggregateRoot::registerEvent() . Furthermore, I need to intercept those events for Logging/Tracing purposes so I decided to make an experiment:
First, implement a custom ApplicationEvent Publisher
public class CustomEventPublisher implements ApplicationEventPublisher {
private final ApplicationEventPublisher publisher;
private final Logger logger = getLogger(CustomEventPublisher.class);
public CustomEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
#Override
public void publishEvent(ApplicationEvent event) {
logger.info("sending an event...");
publisher.publishEvent(event);
}
//.....
}
And then registering as bean
#Configuration
public class CustomEventPublisherConfig {
#Bean
#Primary
public ApplicationEventPublisher getCustomEventPublisher(ApplicationEventPublisher publisher , RabbitTemplate rabbitTemplate) {
return new CustomEventPublisher(publisher, rabbitTemplate);
}
}
this works fine once I explicitly publish events from some sample object with an injected ApplicationEventPublisher
public void pub() {
publisher.publishEvent(new Event(this , 1));
}
#EventListener
public void sub(Event e) {
this.value = e.getValue();
}
and I got the "sending an event..." log entry
then I've tried to define the aggregate root
#Entity
public class AggregateRoot extends AbstractAggregateRoot {
#Id
#GeneratedValue
private Long id;
private int value = 0;
public AggregateRoot setValue(int value) {
registerEvent(new Event(this , value));
return this;
}
}
and
public void pub() {
repository.save(new AggregateRoot().setValue(1));
}
Test pass again but I can clearly see that Spring Data is not using the CustomEventPublisher. I've tried to understand if there is some way to intercept repository.save() call and override the default behaviour, this approach could work even if needs to reinvent the wheel (I don't think that the domain event publishing code is so complicated though) but the only thing I've found is about Spring Data REST that is out of my scope
Any suggestion to overcome this problem?
Thanks in advance
As far as I know, Spring does not provide a way to replace the publisher used by EventPublishingRepositoryProxyPostProcessor. And it seems to me that you have chosen not quite the right path to get what you want to achieve, so I am not answering your direct question, but your requirements described in the beginning.
I would advise you to register a listener using #EventListener and handle your event there:
#EventListener
public void handleEvent(Event event) {
System.out.println(event);
}
Or you can use #TransactionalEventListener to bind the listener to a transaction phase:
#TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleEvent(Event event) {
System.out.println(event);
}

Skipping creation of a specific table [Spring] [Hibernate]

I'm creating some simple spring project (spring security) with a configuration file determining some simple values like table names etc. I defined there a boolean field REQUIRED_ACTIVATION for determining if new user have to activate his account with activation link sent for example via mail.
public static final boolean REQUIRED_ACTIVATION = true;
If I set REQUIRED_ACTIVATION value to false user is active immediately after registration. I've got defined entity which contains data for activation links:
#Entity
#Table(name = 'user_activation_link')
public class UserActivationLink {
[...]
}
When REQUIRED_ACTIVATION is set to false I'm not using this class anywhere but the table is being created in database. Is there any solution for determining if table will be created dependent on value of REQUIRED_ACTIVATION?
You need to do something like this to exclude the tables you don't want to create in the database.
Implement the SchemaFilterProvider and the SchemaFilter interfaces
In the SchemaFilter implementation, add an if condition to includeTable so that it returns false for the table that you don’t
want to create
Add hibernate.properties to the classpath and define hibernate.hbm2ddl.schema_filter_provider to point to the
SchemaFilterProvider implementation
hibernate.hbm2ddl.schema_filter_provider=com.your.package.Provider
And the also:
package com.your.package;
import org.hibernate.boot.model.relational.Namespace;
import org.hibernate.boot.model.relational.Sequence;
import org.hibernate.mapping.Table;
import org.hibernate.tool.schema.spi.SchemaFilter;
import org.hibernate.tool.schema.spi.SchemaFilterProvider;
public class Provider implements SchemaFilterProvider {
#Override
public SchemaFilter getCreateFilter() {
return MySchemaFilter.INSTANCE;
}
#Override
public SchemaFilter getDropFilter() {
return MySchemaFilter.INSTANCE;
}
#Override
public SchemaFilter getMigrateFilter() {
return MySchemaFilter.INSTANCE;
}
#Override
public SchemaFilter getValidateFilter() {
return MySchemaFilter.INSTANCE;
}
}
class MySchemaFilter implements SchemaFilter {
public static final MySchemaFilter INSTANCE = new MySchemaFilter();
#Override
public boolean includeNamespace(Namespace namespace) {
return true;
}
#Override
public boolean includeTable(Table table) {
if (//REQUIRED_ACTIVATION==true && table.getName() is the table you want to exclude){
return false;
}
return true;
}
#Override
public boolean includeSequence(Sequence sequence) {
return true;
}
}
If you use Hibernate with spring.jpa.hibernate.ddl-auto = create / create-drop, so it would try to create a table if not exists on each startup, if you use update it create once if not exists, and after only update table on each startup.
As an alternative try to use Application Listener in Spring:
#Value("${required.activation}")
private Boolean isActivationRequired;
#PersistenceContext
private EntityManager em;
#EventListener
public void handleContextRefreshEvent(ContextRefreshedEvent ctxRefreshedEvent) {
if (!isActivationRequired) {
em.createNativeQuery("drop table user_activation_link").executeUpdate();
}
}
Note here is using #Value instead of public static final field, you can add the field called required.activation in application.properties. It will be automatically injected into private field.

Why does my Spring #EventListener show different transactional behavior on event submission than when being called directly?

When using the #EventListener functionality with Spring Data's repositories the behavior is different than when calling the same code procedural.
My persistent objects publish events using the following base class:
public abstract class Aggregate {
#Transient
private transient final Set<Object> events = new LinkedHashSet<>();
protected <T> T registerEvent(T event) {
this.events.add(event);
return event;
}
#DomainEvents
Collection<Object> events() {
return Collections.unmodifiableSet(events);
}
#AfterDomainEventPublication
void clearEvents() {
this.events.clear();
}
}
My event listening class is implemented as follows:
class Service {
#EventListener
public void listener(SomeEvent event) {
someOtherRepository.save(someOtherPersistentObject);
someOtherCode();
}
}
When the listener is triggered and someOtherRepository's save(…) method fails a rollback will be issued. But someOtherCode() is executed regardless of the rollback.
But when I remove all #EventListening functionality and call the listener(…) method directly after the point where the originating repository is responsible for firing the event. Then I get a different behavior. Then someOtherCode() is never executed and the someOtherRepository.save(…) method fails immediately.
The original service responsible for publishing the event looks like this
public OriginatingService {
#Transactional
public void someMethod() {
originatingRepoDifferentFromSomeOtherRepo.save(something);
Why is this happening and is there a way to force the same behavior onto my event listening implementation?
Because writes to the database may be delayed until transaction commit i.e. when the transactional method returns.
Update as below to explicitly trigger an immediate flush:
#EventListener
public void listener(SomeEvent event) {
someOtherRepository.saveAndFlush(someOtherPersistentObject);
someOtherCode();
}

Database driven feature toggle

I would want to enable my new feature based on a database value . If the database value is set for the feature , the new code should be enabled. Else it should toggle and flow to the old code. is there a way to accomplish this in Java/Spring ? I do not want to hit the database frequently.I am thinking of one call at the start of the request . Are there any examples to this ? If so please let me know. Thanks
Create Feature class implementation:
public enum CustomFeatures implements Feature {
#Label("Activates customFeature")
MY_CUSTOM_FEATURE;
public boolean isActive() {
return FeatureContext.getFeatureManager().isActive(this);
}
}
Create feature provider:
#Bean
public FeatureProvider featureProvider() {
return new EnumBasedFeatureProvider(CustomFeatures.class);
}
Create entity and repository:
#Entity
public class Feature {
private String name;
private Boolean active;
// getters-setters
}
Create #Component which will query database and sets new feature sate
#Component
public class FeatureToggler {
private final FeatureRepository featureRepository;
private final FeatureManager featureManager;
private FeatureToggler(FeatureRepository featureRepository, FeatureManager featureManager) {
this.featureRepository = featureRepository;
this.featureManager = featureManager;
}
#Schedule(fixedRate=60000)
public void refreshFeatureToggles() {
featureManager.setFeatureState(new FeatureState(CustomFeatures.MY_CUSTOM_FEATURE, featureRepository.findByName().isActive);
}
}
Now you could use check if feature enabled like this:
if(CustomFeatures.MY_CUSTOM_FEATURE.isActive()) {
}
Or use Spring aspect..

How can I ignore spring #Transactional annotation for a specific method when #ActiveProfiles("test")

I need to ignore the following #Transactional annotation during my integration tests.
#Service
public class MyClass {
#Transactional(propagation = Propagation.NEVER)
public void doSomething() {
// do something that once in production can not be inside a transaction (reasons are omitted)
}
}
The problem is that all my tests are executed inside a transaction that is rolled back by default. How could I ignore the #Transactional(propagation = Propagation.NEVER) annotation for this method when it is running in the scope of a test (#ActiveProfiles("test")) allowing it to be executed inside a transaction?
First of all, you need to exclude your current #EnableTransactionManagement annotation to be active in your test profile. You can do this by isolating the #EnableTransactionManagement annotation to a separate configuration class which excludes the profile test so it only gets activated whenever the test profile is not active.
#EnableTransactionManagement(mode=AdviceMode.PROXY)
#Profile("!test")
public class TransactionManagementConfig {}
With that in place, we can start building up the custom transaction management configuration for your test profile. First we define an annotation that will be used to activate custom transaction management (javadoc comments stripped for compactness of the example, see EnableTransactionManagement javadoc for details).
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Import(CustomTransactionManagementConfigurationSelector.class)
public #interface EnableCustomTransactionManagement {
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default Ordered.LOWEST_PRECEDENCE;
}
Then we need an import selector. Note, that since you're using AdviceMode.PROXY, i skipped implementing the ASPECTJ part, but that should be done analogously in order to use AspectJ based transaction management.
public class CustomTransactionManagementConfigurationSelector extends
AdviceModeImportSelector<EnableCustomTransactionManagement> {
#Override
protected String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[] {
AutoProxyRegistrar.class.getName(),
CustomTransactionAttributeSourceConfig.class.getName()
};
case ASPECTJ:
default:
return null;
}
}
}
And finally the part where you will be able to override the transaction attributes. This one subclasses ProxyTransactionManagementConfiguration for AdviceMode.PROXY, you would need an analogous implementation based on AspectJTransactionManagementConfiguration for AdviceMode.ASPECTJ. Feel free to implement your own logic, whether be it a constant override of whatever attributes the original AnnotationTransactionAttributeSource would determine as in my example, or going to greater lengths by introducing and handling your own custom annotation for this purpose.
#Configuration
public class CustomTransactionAttributeSourceConfig
extends ProxyTransactionManagementConfiguration {
#Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
this.enableTx = AnnotationAttributes
.fromMap(importMetadata.getAnnotationAttributes(
EnableCustomTransactionManagement.class.getName(),
false));
Assert.notNull(this.enableTx,
"#EnableCustomTransactionManagement is not present on importing class "
+ importMetadata.getClassName());
}
#Bean
#Role(BeanDefinition.ROLE_INFRASTRUCTURE)
#Override
public TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource() {
private static final long serialVersionUID = 1L;
#Override
protected TransactionAttribute findTransactionAttribute(
Class<?> clazz) {
TransactionAttribute transactionAttribute =
super.findTransactionAttribute(clazz);
if (transactionAttribute != null) {
// implement whatever logic to override transaction attributes
// extracted from #Transactional annotation
transactionAttribute = new DefaultTransactionAttribute(
TransactionAttribute.PROPAGATION_REQUIRED);
}
return transactionAttribute;
}
#Override
protected TransactionAttribute findTransactionAttribute(
Method method) {
TransactionAttribute transactionAttribute =
super.findTransactionAttribute(method);
if (transactionAttribute != null) {
// implement whatever logic to override transaction attributes
// extracted from #Transactional annotation
transactionAttribute = new DefaultTransactionAttribute(
TransactionAttribute.PROPAGATION_REQUIRED);
}
return transactionAttribute;
}
};
}
}
Finally, you'll need to enable the custom transaction management configuration with a configuration class tied to the test profile.
#EnableCustomTransactionManagement(mode=AdviceMode.PROXY)
#Profile("test")
public class TransactionManagementTestConfig {}
I hope this helps.

Categories

Resources