Is it possible to use transactional observers in quarkus with reactive sql client - quarkus

I'm trying to figure out if it's possible to catch transaction events like it's described here, but with the reactive client?
If it's not possible I would appreciate if someone provide an example how it can be implement manually. I want to be able to add some business logic in my application before transaction start, before transaction commit and after transaction commit. And I think that events are best suited for such logic. Thanks in advance.

Finally, I've found a way how it can be implemented manually. Reactive hibernate has such a method in the Mutiny session implementation:
Uni<T> executeInTransaction(Function<Mutiny.Transaction, Uni<T>> work) {
return work.apply( this )
// only flush() if the work completed with no exception
.call( this::flush )
.call( this::beforeCompletion )
// in the case of an exception or cancellation
// we need to rollback the transaction
.onFailure().call( this::rollback )
.onCancellation().call( this::rollback )
// finally, when there was no exception,
// commit or rollback the transaction
.call( () -> rollback ? rollback() : commit() )
.call( this::afterCompletion );
}
So, as you can see, two methods (beforeCompletion and afterCompletion) are called in the chain, which allows us to add custom logic before and after transaction commit. Those methods execute contract implementations from the queue. I'll show you an example of the "before" event.
First of all, we should create some qualifier annotations.
The #Entity annotation we are going to use to attach event listeners for the specific entity event:
package com.example.annotation;
import java.io.Serial;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.enterprise.util.AnnotationLiteral;
import javax.inject.Qualifier;
import lombok.EqualsAndHashCode;
import com.example.model.BaseEntity;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
#Qualifier
#Retention(RUNTIME)
#Target({METHOD, PARAMETER, FIELD, TYPE})
#Documented
public #interface Entity {
Class<? extends BaseEntity> value();
#EqualsAndHashCode(callSuper = true)
final class Literal extends AnnotationLiteral<Entity> implements Entity {
#Serial
private static final long serialVersionUID = 2137611959567040656L;
private final Class<? extends BaseEntity> value;
private Literal(Class<? extends BaseEntity> value) {
this.value = value;
}
public static Literal of(Class<? extends BaseEntity> value) {
return new Literal(value);
}
#Override
public Class<? extends BaseEntity> value() {
return value;
}
}
}
Let's imagine we have Book and Author entities in our service and both of them extend a BaseEntity model/interface. This base entity is used here as some kind of qualifier, which will be used later.
And some annotations for CRUD actions, here as an example of the "create" action (the same is for the "update" action):
#Qualifier
#Retention(RUNTIME)
#Target({METHOD, PARAMETER, FIELD, TYPE})
#Documented
public #interface Create {
#EqualsAndHashCode(callSuper = true)
final class Literal extends AnnotationLiteral<Create> implements Create {
private static final long serialVersionUID = 2137611959567040656L;
public static final Literal INSTANCE = new Literal();
private Literal() {
}
}
}
Next we create the class, which will trigger events. In this example we register "pre-insert" and "pre-update" listeners:
package com.example.observer;
import java.io.Serial;
import java.lang.annotation.Annotation;
import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.spi.CDI;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.event.spi.*;
import org.hibernate.reactive.session.ReactiveSession;
import com.example.annotation.Entity;
import com.example.annotation.operation.Create;
import com.example.annotation.operation.Update;
import com.example.model.BaseEntity;
import com.example.observer.action.EventReactiveBeforeTransactionCompletionProcess;
/**
* {#inheritDoc}
* <p>Component, which registers 'pre' events</p>
*/
#Slf4j
#ApplicationScoped
public class TransactionProcessRegistrarEventListener implements PreInsertEventListener, PreUpdateEventListener {
#Serial
private static final long serialVersionUID = 6763048376606381859L;
/**
* {#inheritDoc}
*
* #param event event
*/
#Override
public boolean onPreInsert(PreInsertEvent event) {
return register(event, Create.Literal.INSTANCE);
}
/**
* {#inheritDoc}
*
* #param event event
*/
#Override
public boolean onPreUpdate(PreUpdateEvent event) {
return register(event, Update.Literal.INSTANCE);
}
/**
* Register processes
*
* #param event event
* #return result
*/
#SuppressWarnings("unchecked")
private boolean register(AbstractPreDatabaseOperationEvent event, Annotation qualifier) {
Class<? extends BaseEntity> clazz = (Class<? extends BaseEntity>) event.getEntity().getClass();
log.debug("registering '{}' instances. Entity: {}", event.getClass().getSimpleName(), clazz.getSimpleName());
final SessionImplementor session = event.getSession();
List<EventReactiveBeforeTransactionCompletionProcess> beforeProcesses = CDI.current()
.select(EventReactiveBeforeTransactionCompletionProcess.class, Entity.Literal.of(clazz), qualifier)
.stream().toList();
if (beforeProcesses.isEmpty())
log.debug("no 'before' processes found");
beforeProcesses.forEach(process -> {
process.setEvent(event);
((ReactiveSession) session).getReactiveActionQueue()
.registerProcess(process);
log.debug("process {} has been successfully registered", process.getClass().getSimpleName());
});
return false;
}
}
As you can see here we have a custom EventReactiveBeforeTransactionCompletionProcess interface, it will allow us to set current event to the hibernate process (and retrieve it later in the event). Let's create it:
package com.example.observer.action;
import org.hibernate.event.spi.AbstractPreDatabaseOperationEvent;
import org.hibernate.reactive.engine.ReactiveBeforeTransactionCompletionProcess;
public interface EventReactiveBeforeTransactionCompletionProcess extends ReactiveBeforeTransactionCompletionProcess {
<T extends AbstractPreDatabaseOperationEvent> void setEvent(T event);
<T extends AbstractPreDatabaseOperationEvent> T getEvent();
}
Now we have everything to create a custom hibernate integrator, which will allow us to register listeners.
package com.example.observer;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.boot.Metadata;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.integrator.spi.Integrator;
import org.hibernate.service.spi.SessionFactoryServiceRegistry;
import javax.enterprise.context.ApplicationScoped;
#Slf4j
#ApplicationScoped
public class EventListenerIntegrator implements Integrator {
/**
* {#inheritDoc}
*
* #param metadata The "compiled" representation of the mapping information
* #param sessionFactory The session factory being created
* #param serviceRegistry The session factory's service registry
*/
#Override
public void integrate(Metadata metadata,
SessionFactoryImplementor sessionFactory,
SessionFactoryServiceRegistry serviceRegistry) {
log.debug("registering {} integrator...", getClass().getSimpleName());
final EventListenerRegistry eventListenerRegistry = serviceRegistry.getService(EventListenerRegistry.class);
eventListenerRegistry.appendListeners(EventType.PRE_INSERT, new TransactionProcessRegistrarEventListener());
eventListenerRegistry.appendListeners(EventType.PRE_UPDATE, new TransactionProcessRegistrarEventListener());
}
/**
* {#inheritDoc}
*
* #param sessionFactory The session factory being closed.
* #param serviceRegistry That session factory's service registry
*/
#Override
public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
// intentionally do nothing
}
}
And finally, the listener itself (you can retrieve the event here and the entity from it using parent getEvent method):
package com.example.observer.action.before.create.book.validator;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import io.quarkus.arc.Priority;
import io.quarkus.arc.Unremovable;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.event.spi.AbstractPreDatabaseOperationEvent;
import org.hibernate.reactive.session.ReactiveSession;
import com.example.annotation.Entity;
import com.example.annotation.operation.Create;
import com.example.model.Book;
import com.example.observer.action.EventReactiveBeforeTransactionCompletionProcess;
#Getter
#Setter
#Slf4j
#Priority(10)
#Entity(Book.class)
#Create
#Unremovable
#Dependent
public class BookValidator implements EventReactiveBeforeTransactionCompletionProcess {
private AbstractPreDatabaseOperationEvent event;
/**
* {#inheritDoc}
*
* #param session The session on which the transaction is preparing to complete.
*/
#Override
public CompletionStage<Void> doBeforeTransactionCompletion(ReactiveSession session) {
log.debug("validating, enriching or everything you want with the book entity...");
return CompletableFuture.completedStage(null);
}
}
That's all. If anyone knows a better realization, please let me know.

Related

Decorate or Intercept Spring #Autowired Bean

I am upgrading old Java EE application to Spring based solution. In the old applications there were custom annotations to create proxy inject proxy bean and intercept the method invocation [Interceptor classes implements MethodInterceptor) or (implements InvocationHandler), which used to perform some before and after execution stuff.
We have replaced those custom annotations with Spring marker interfaces like #Service, #Repository etc. and we are able to use #Autowire the bean instances. Now my question is how to intercept these autowired beans to perform per and post execution activities. One solution I can think is to use Spring AOP and use #Around pointcut. Just want to know is there any other and better alternative which can be used like
extending org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
Using BeanFactoryPostProcessor or BeanPostProcessor.
Using InstantiationAwareBeanPostProcessor
I have used this alternative instead of AOP. I have used Spring's bean pre & post processor call back. Below is the code snippet.
Application Context Provider, to get Spring beans statically
package com.appname.config;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* #author dpoddar
*
*/
#Component("applicationContextProvider")
public class ApplicationContextProvider implements ApplicationContextAware{
private static ApplicationContext ctx = null;
public static ApplicationContext getApplicationContext() {
return ctx;
}
#Override
#Autowired
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
ApplicationContextProvider.ctx = ctx;
}
/**
* Returns the Spring managed bean instance of the given class type if it exists.
* Returns null otherwise.
* #param beanClass
* #return
*/
public static <T extends Object> T getBean(Class<T> beanClass) {
return ctx.getBean(beanClass);
}
/**
* Returns the Spring managed bean instance of the given class type if it exists.
*
* #param <T>
* #param name
* #param beanClass
* #return
*/
public static <T extends Object> T getBean(String name,Class<T> beanClass) {
return ctx.getBean(name,beanClass);
}
}
Spring Bean Post Processor, InstantiationAwareBeanPostProcessor adds before and after initialization call backs
package com.appname.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.SpringProxy;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import com.appname.core.ExecutionContext;
import com.appname.core.di.FacadeService;
import com.appname.interceptors.BusinesServiceInterceptor;
import com.appname.interceptors.FacadeServiceInterceptor;
import com.appname.interceptors.RepositoryInterceptor;
import net.sf.cglib.proxy.Enhancer;
/**
* #author dpoddar
*
*/
#Component
public class AppSpringBeanPostProcessor extends AutowiredAnnotationBeanPostProcessor implements InstantiationAwareBeanPostProcessor {
private static Logger logger = LoggerFactory.getLogger(AppSpringBeanPostProcessor.class);
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
Class<?> clazz = bean.getClass();
AutowireCapableBeanFactory factory = ApplicationContextProvider.getApplicationContext().getAutowireCapableBeanFactory();
if(clazz.isAnnotationPresent(FacadeService.class)) {
//This is to instatiate InvocationHandler classes
//return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), new FacadeServiceInterceptor(bean));
FacadeServiceInterceptor interceptor = new FacadeServiceInterceptor();
Enhancer e = new Enhancer();
e.setSuperclass(clazz);
e.setInterfaces(new Class[]{SpringProxy.class}); /// Identification Spring-generated proxies
e.setCallback(interceptor);
Object o = e.create();
factory.autowireBean( o ); //Autowire Bean dependecies to the newly created object
return o;
}else if(clazz.isAnnotationPresent(Service.class)) {
BusinesServiceInterceptor interceptor = new BusinesServiceInterceptor();
Enhancer e = new Enhancer();
e.setSuperclass(clazz);
e.setInterfaces(new Class[]{SpringProxy.class});
e.setCallback(interceptor);
Object o = e.create();
factory.autowireBean( o );
return o;
}else if(clazz.isAnnotationPresent(Repository.class)) {
ExecutionContext.newInstance();
RepositoryInterceptor interceptor = new RepositoryInterceptor();
Enhancer e = new Enhancer();
e.setSuperclass(clazz);
e.setInterfaces(new Class[]{SpringProxy.class});
e.setCallback(interceptor);
return e.create();
}else {
return bean;
}
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}

How to implement Aspect on Spring crud repository methods

I'm doing a project in which we have lot of entities on which we will be doing CRUD operations. I have created a base entity class and in all the other entities i have extended the base entity class which is having common fields like created_date, created_by, last_updated_date, last_updated_by etc. Now, i want to implement aspect on Spring CrudRepository methods and set the above mentioned fields while saving.
I've tried implementing something like this but not working.
package com.cerium.aop;
import java.util.Date;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.cerium.datamodel.AccountDataModel;
import com.cerium.domain.Account;
import com.cerium.domain.BaseEntity;
import com.cerium.util.Constants;
/**
* #author Manikanta B Cerium
*
*/
#Component
#Aspect
public class SampleAspect {
private static final Logger LOG = LoggerFactory.getLogger(SampleAspect.class);
#Around("execution(* com.cerium.repository.*.save (com.cerium.domain.BaseEntity)) && args(saveData)")
public Object beforeSave(ProceedingJoinPoint proceedingJoinPoint, Object saveData) throws Throwable {
LOG.debug("Into aspect before save: {}", saveData);
BaseEntity baseEntity = (BaseEntity) proceedingJoinPoint.proceed(new Object[] { saveData });
// set the fields here......
baseEntity.setCreatedDate(new Date());
System.out.println(saveData);
return baseEntity;
}
}
To work with aspect we should first define a pointcut method with the filter expression (in your case for 'save' methods), then create a method to handle this pointcut:
#Component
#Aspect
public class CommonSaveAspect {
#Pointcut("execution(* com.cerium.repository.*.save(..))")
public void commonSave() {
}
#Around("commonSave()")
public Object addCommonData(final ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
if (Iterable.class.isAssignableFrom(args[0].getClass())) {
//noinspection unchecked
Iterable<BaseEntity> entities = (Iterable<BaseEntity>) args[0];
entities.forEach(entity -> {
// set the fields here...
});
}
if (args[0] instanceof BaseEntity) {
BaseEntity entity = (BaseEntity) args[0];
// set the fields here...
}
return pjp.proceed(args);
}
}
More info

Entity not getting saved in EclipseLink EntityListener

I have written EntityListener using eclipseLink's "DescriptorEventAdapter". I tried almost all variations whatever present online BUT the entity which I am saving from my listener is not getting saved. I suspect something fishy is going on with transaction but didn't get the root cause. Here is the code :
package com.db;
import java.util.Date;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManagerFactory;
import javax.transaction.Transactional;
import javax.transaction.Transactional.TxType;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
import org.eclipse.persistence.jpa.JpaEntityManager;
import org.eclipse.persistence.queries.InsertObjectQuery;
import org.eclipse.persistence.queries.UpdateObjectQuery;
import org.eclipse.persistence.sessions.changesets.DirectToFieldChangeRecord;
import org.eclipse.persistence.sessions.changesets.ObjectChangeSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
#Component
public class NotificationMessageListener extends DescriptorEventAdapter {
public static Logger logger = LoggerFactory.getLogger(NotificationMessageListener.class);
private static final String targetColumn = "STATUS";
//private static AuditRepository auditRepo;
#Autowired
private StatusAuditRepository statusAuditRepo;
#Autowired
private RuleResultAuditRepository ruleResultRepo;
#Autowired
private EntityManagerFactory factory;
JpaEntityManager entityManager = null;
#PostConstruct
public void init() {
try {
entityManager = (JpaEntityManager) factory.createEntityManager();
// Use the entity manager to get a ClassDescriptor for the Entity class
ClassDescriptor desc =
entityManager.getSession().getClassDescriptor(NotificationMessage.class);
// Add this class as a listener to the class descriptor
desc.getEventManager().addListener(this);
} finally {
if (entityManager != null) {
// Cleanup the entity manager
entityManager.close();
}
}
}
/*#Autowired
public void setAuditRepo(AuditRepository auditRepo) {
NotificationMessageListener.auditRepo = auditRepo;
}*/
#Transactional(value = TxType.REQUIRES_NEW)
#Override
public void postInsert(DescriptorEvent event) {
logger.info("post insert is called ");
//NotificationMessage notificationMsg = (NotificationMessage) ((InsertObjectQuery) event.getQuery()).getObject();
//entityManager.getTransaction().begin();
NotificationStatusAudit statusAudit = new NotificationStatusAudit();
statusAudit.setInsertionTime(new Date());
//statusAudit.setNewVal(notificationMsg.getStatus());
statusAudit.setNewVal("abc");
statusAudit.setOldval("asdf");
statusAudit.setTargetColumnName("from listner");
//statusAudit.setTargetRecordId(notificationMsg.getId());
statusAudit.setTargetRecordId(123L);
statusAudit = statusAuditRepo.save(statusAudit);
//entityManager.getTransaction().commit();
//logger.info("Number of records "+statusAuditRepo.count());
//auditRuleResult(notificationMsg.getMessageCorrelationId() , true);
}
#Override
public void postUpdate(DescriptorEvent event) {
ObjectChangeSet objectChanges = ((UpdateObjectQuery) event.getQuery()).getObjectChangeSet();
DirectToFieldChangeRecord statusChanges = (DirectToFieldChangeRecord) objectChanges
.getChangesForAttributeNamed("status");
if (statusChanges != null && !statusChanges.getNewValue().equals(statusChanges.getOldValue())) {
NotificationStatusAudit statusAudit = new NotificationStatusAudit();
statusAudit.setInsertionTime(new Date());
statusAudit.setNewVal("abc");
statusAudit.setOldval("asdf");
statusAudit.setTargetColumnName(targetColumn);
statusAudit.setTargetRecordId((Long) objectChanges.getId());
statusAudit = statusAuditRepo.save(statusAudit);
}
}
}
Here all I have to do is save the record in another (Audit) table when data is getting inserted in one table. My application is spring boot app and am using eclipseLink for persistent. I had to manually register my entity-listener in "PostConstruct" because if it is registered using #EntityListner annotation , spring-data-repos were not getting autowired. Here are my questions :
1) Using EntityListener for my requirement is good approach or should I use direct "save" operations ?
2) I debugged the EntityListener code and method is not initiated a new Transaction even after adding Requires_new. I can see method is not being called $proxy (spring-proxy). I don't understand why ?
I am not sure about what you are doing in your #PostConstruct init() method... but I suspect you should be configuring this DescriptorEventAdapter using EclipseLink's DescriptorCustomizer. Here is an example:
public class MessageEventListener extends DescriptorEventAdapter implements DescriptorCustomizer {
#Override
public void customize(ClassDescriptor descriptor) {
descriptor.getEventManager().addListener(this);
}
#Override
public void postUpdate(DescriptorEvent event) {
ObjectChangeSet objectChanges = ((UpdateObjectQuery) event.getQuery()).getObjectChangeSet();
//More business logic...
}
}
#Entity
#Customizer(MessageEventListener.class)
public class Message {
#Id private long id;
private String content;
}

Spring validation for RequestBody parameters bound to collections in Controller methods

I have
An Entity:
package org.ibp.soq;
public class MyEntity {
private String field1;
private String field2;
//..getters and setters
}
Validator for the entity:
package org.ibp.soq;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
#Component
public class MyEntityValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return MyEntity.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
MyEntity myEntity = (MyEntity) target;
// Logic to validate my entity
System.out.print(myEntity);
}
}
and
The REST controller with bulk PUT method:
package org.ibp.soq;
import java.util.List;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping("/myEntity")
public class MyEntityRestResource {
#Autowired
private MyEntityValidator myEntityValidator;
#InitBinder
protected void initBinder(final WebDataBinder binder) {
binder.addValidators(this.myEntityValidator);
}
#RequestMapping(method = RequestMethod.PUT)
public void bulkCreate(#RequestBody #Valid List<MyEntity> myEntities) {
// Logic to bulk create entities here.
System.out.print(myEntities);
}
}
When I make a PUT request to this resource with following request body:
[
{
"field1": "AA",
"field2": "11"
},
{
"field1": "BB",
"field2": "22"
}
]
The error I get is:
"Invalid target for Validator [org.ibp.soq.MyEntityValidator#4eab617e]: [org.ibp.soq.MyEntity#21cebf1c, org.ibp.soq.MyEntity#c64d89b]"
I can understand that this is because MyEntityValidator "supports" single MyEntity validation, not validation for ArrayList<MyEntity>.
MyEntityValidator works perfectly if I have single MyEntity object in request body and a corresponding controller method with #RequestBody #Valid MyEntity myEntity parameter.
How can the validator setup I have used, be extended for supporting validation of collection of MyEntity's ?
The solution is to create a custom Validator for Collection and a #ControllerAdvice that registers that Validator in the WebDataBinders.
Validator:
import java.util.Collection;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
/**
* Spring {#link Validator} that iterates over the elements of a
* {#link Collection} and run the validation process for each of them
* individually.
*
* #author DISID CORPORATION S.L. (www.disid.com)
*/
public class CollectionValidator implements Validator {
private final Validator validator;
public CollectionValidator(LocalValidatorFactoryBean validatorFactory) {
this.validator = validatorFactory;
}
#Override
public boolean supports(Class<?> clazz) {
return Collection.class.isAssignableFrom(clazz);
}
/**
* Validate each element inside the supplied {#link Collection}.
*
* The supplied errors instance is used to report the validation errors.
*
* #param target the collection that is to be validated
* #param errors contextual state about the validation process
*/
#Override
#SuppressWarnings("rawtypes")
public void validate(Object target, Errors errors) {
Collection collection = (Collection) target;
for (Object object : collection) {
ValidationUtils.invokeValidator(validator, object, errors);
}
}
}
ControllerAdvice:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;
/**
* Controller advice that adds the {#link CollectionValidator} to the
* {#link WebDataBinder}.
*
* #author DISID CORPORATION S.L. (www.disid.com)
*/
#ControllerAdvice
public class ValidatorAdvice {
#Autowired
protected LocalValidatorFactoryBean validator;
/**
* Adds the {#link CollectionValidator} to the supplied
* {#link WebDataBinder}
*
* #param binder web data binder.
*/
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.addValidators(new CollectionValidator(validator));
}
}
As you might have guessed this cannot be achieved using Spring Validation. Spring Validation implements Bean Validation(JSR 303/349) as opposed to Object validation. Unfortunately a collection is not a Java Bean. You have two options
Wrap your list inside a Java Bean
Call the validator manually in your bulk create method myEntityValidator. validate(targetObject, errors).
Actually, this can be achieved using Spring Validation and JSR303.
Expose a MethodValidationPostProcessor bean.
Annotate your controller class with #Validated (org.springframework.validation.annotation.Validated)
Use the JSR303 validation annotations on your MyEntity fields/methods.
Annotate your RequestBody argument with #Valid (you've already done this in your example).
Add an #ExceptionHandler method to handle MethodArgumentNotValidException. This can be done in the controller or in a #ControllerAdvice class.

Applying custom annotation advice to spring data jpa repository

I am working on a mysql master slave replication. I am using spring data jpa(spring boot).
What I needed is all write operations to go to master server and read-only operations to be equally distributed among multiple read-only slaves.
For that I need to:
Use special JDBC driver: com.mysql.jdbc.ReplicationDriver
Set replication: in the URL:
spring:
datasource:
driverClassName: com.mysql.jdbc.ReplicationDriver
url: jdbc:mysql:replication://127.0.0.1:3306,127.0.0.1:3307/MyForum?user=root&password=password&autoReconnect=true
test-on-borrow: true
validation-query: SELECT 1
database: MYSQL
Auto commit needs to be turned off. (*)
Connection needs to be set to read-only.
To ensure JDBC Connection is set to read-only, I created an annotation and a simple AOP interceptor.
Annotation
package com.xyz.forum.replication;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by Bhupati Patel on 02/11/15.
*/
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface ReadOnlyConnection {
}
Interceptor
package com.xyz.forum.replication;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.hibernate.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.persistence.EntityManager;
/**
* Created by Bhupati Patel on 02/11/15.
*/
#Aspect
#Component
public class ConnectionInterceptor {
private Logger logger;
public ConnectionInterceptor() {
logger = LoggerFactory.getLogger(getClass());
logger.info("ConnectionInterceptor Started");
}
#Autowired
private EntityManager entityManager;
#Pointcut("#annotation(com.xyz.forum.replication.ReadOnlyConnection)")
public void inReadOnlyConnection(){}
#Around("inReadOnlyConnection()")
public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
Session session = entityManager.unwrap(Session.class);
ConnectionReadOnly readOnlyWork = new ConnectionReadOnly();
try{
session.doWork(readOnlyWork);
return pjp.proceed();
} finally {
readOnlyWork.switchBack();
}
}
}
Following is my spring data repository
package com.xyz.forum.repositories;
import com.xyz.forum.entity.Topic;
import org.springframework.data.repository.Repository;
import java.util.List;
/**
* Created by Bhupati Patel on 16/04/15.
*/
public interface TopicRepository extends Repository<Topic,Integer>{
Topic save(Topic topic);
Topic findByTopicIdAndIsDeletedFalse(Integer topicId);
List<Topic> findByIsDeletedOrderByTopicOrderAsc(Boolean isDelete);
}
Following is my Manager(Service) class.
package com.xyz.forum.manager;
import com.xyz.forum.domain.entry.impl.TopicEntry;
import com.xyz.forum.domain.exception.impl.AuthException;
import com.xyz.forum.domain.exception.impl.NotFoundException;
import com.xyz.forum.entity.Topic;
import com.xyz.forum.replication.ReadOnlyConnection;
import com.xyz.forum.repositories.TopicRepository;
import com.xyz.forum.utils.converter.TopicConverter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
/**
* Created by Bhupati Patel on 16/04/15.
*/
#Repository
public class TopicManager {
#Autowired
TopicRepository topicRepository;
#Transactional
public TopicEntry save(TopicEntry topicEntry) {
Topic topic = TopicConverter.fromEntryToEntity(topicEntry);
return TopicConverter.fromEntityToEntry(topicRepository.save(topic));
}
#ReadOnlyConnection
public TopicEntry get(Integer id) {
Topic topicFromDb = topicRepository.findByTopicIdAndIsDeletedFalse(id);
if(topicFromDb == null) {
throw new NotFoundException("Invalid Id", "Topic Id [" + id + "] doesn't exist ");
}
return TopicConverter.fromEntityToEntry(topicFromDb);
}
}
In the above code #ReadOnlyConnection annotation is specified in manager or service layer. Above pieces of code works fine for me. It is a trivial case where in the service layer I am only reading from slave db and writing into master db.
Having said that my actual requirement is I should be able to use #ReadOnlyConnection in repository level itself because I have quite a few business logic where I do both read/write operation in other classes of service layer.Therefore I can't put #ReadOnlyConnection in service layer.
I should be able to use something like this
public interface TopicRepository extends Repository<Topic,Integer>{
Topic save(Topic topic);
#ReadOnlyConnection
Topic findByTopicIdAndIsDeletedFalse(Integer topicId);
#ReadOnlyConnection
List<Topic> findByIsDeletedOrderByTopicOrderAsc(Boolean isDelete);
}
Like spring's #Transactional or #Modifying or #Query annotation. Following is an example of what I am referring.
public interface AnswerRepository extends Repository<Answer,Integer> {
#Transactional
Answer save(Answer answer);
#Transactional
#Modifying
#Query("update Answer ans set ans.isDeleted = 1, ans.deletedBy = :deletedBy, ans.deletedOn = :deletedOn " +
"where ans.questionId = :questionId and ans.isDeleted = 0")
void softDeleteBulkAnswers(#Param("deletedBy") String deletedBy, #Param("deletedOn") Date deletedOn,
#Param("questionId") Integer questionId);
}
I am novice to aspectj and aop world, I tried quite a few pointcut regex in the ConnectionInterceptor but none of them worked. I have been trying this since a long time but no luck yet.
How to achieve the asked task.
I couldn't get a workaround of having my custom annotation #ReadOnlyConnection(like #Transactional) at a method level,but a small heck did work for me.
I am pasting the code snippet below.
#Aspect
#Component
#EnableAspectJAutoProxy
public class ConnectionInterceptor {
private Logger logger;
private static final String JPA_PREFIX = "findBy";
private static final String CUSTOM_PREFIX = "read";
public ConnectionInterceptor() {
logger = LoggerFactory.getLogger(getClass());
logger.info("ConnectionInterceptor Started");
}
#Autowired
private EntityManager entityManager;
#Pointcut("this(org.springframework.data.repository.Repository)")
public void inRepositoryLayer() {}
#Around("inRepositoryLayer()")
public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().getName();
if (StringUtils.startsWith(methodName, JPA_PREFIX) || StringUtils.startsWith(methodName, CUSTOM_PREFIX)) {
System.out.println("I'm there!" );
Session session = entityManager.unwrap(Session.class);
ConnectionReadOnly readOnlyWork = new ConnectionReadOnly();
try{
session.doWork(readOnlyWork);
return pjp.proceed();
} finally {
readOnlyWork.switchBack();
}
}
return pjp.proceed();
}
}
So in the above code I am using a pointcut like following
#Pointcut("this(org.springframework.data.repository.Repository)")
public void inRepositoryLayer() {}
and what it does is
any join point (method execution only in Spring AOP) where the proxy implements the Repository interface
You can have a look it at
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html
Now all my repository read query methods either start with a prefix "findByXXX"(default spring-data-jpa readable method) or "readXXX"(custom read method with #Query annotation) which in my around method executions matched by the above pointcut. According to my requirement I am setting the JDBC Connection readOnly true.
Session session = entityManager.unwrap(Session.class);
ConnectionReadOnly readOnlyWork = new ConnectionReadOnly();
And my ConnectionReadOnly look like following
package com.xyz.forum.replication;
import org.hibernate.jdbc.Work;
import java.sql.Connection;
import java.sql.SQLException;
/**
* Created by Bhupati Patel on 04/11/15.
*/
public class ConnectionReadOnly implements Work {
private Connection connection;
private boolean autoCommit;
private boolean readOnly;
#Override
public void execute(Connection connection) throws SQLException {
this.connection = connection;
this.autoCommit = connection.getAutoCommit();
this.readOnly = connection.isReadOnly();
connection.setAutoCommit(false);
connection.setReadOnly(true);
}
//method to restore the connection state before intercepted
public void switchBack() throws SQLException{
connection.setAutoCommit(autoCommit);
connection.setReadOnly(readOnly);
}
}
So above settings work for my requirement.
it seems that #Pointcut && #Around should be declared in some way like follows:
#Pointcut(value = "execution(public * *(..))")
public void anyPublicMethod() {
}
#Around("#annotation(readOnlyConnection)")

Resources