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)")
Related
I thank you ahead for your time to read my request. I'm new to the Spring Service Locator Factory Method design Pattern and I don't understand the approach behind it. However I followed a turtorial and have been able to implement the Post request for my user registratio spring maven application. My src/main/java folder cointains this five packages:
Config
Controller
Model
Registry
Service
The Config package is to centralize the creation of users and its java class is as bellow:
package com.nidservices.yekoregistration.config;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.ServiceLocatorFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.nidservices.yekoregistration.registry.ServiceRegistry;
#Configuration
public class UserConfig {
#Bean
public FactoryBean<?> factoryBean() {
final ServiceLocatorFactoryBean bean = new ServiceLocatorFactoryBean();
bean.setServiceLocatorInterface(ServiceRegistry.class);
return bean;
}
}
The Registry package is to adapt the service base on the type of entity to create and is as bellow:
package com.nidservices.yekoregistration.registry;
public interface AdapterService<T> {
public void process(T request);
}
package com.nidservices.yekoregistration.registry;
public interface ServiceRegistry {
public <T> AdapterService<T> getService(String serviceName);
}
The Service package contains the different types of entity that inherit the User Model and the User Model is as bellow:
public class User implements Serializable {
private UUID id;
private String userIdentifier;
private String userType;
public String getUserIdentifier() {
return userIdentifier;
}
public void setUserIdentifier(String userIdentifier) {
this.userIdentifier = userIdentifier;
}
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
#Override
public String toString() {
return "User [userIdentifier=" + userIdentifier + ", UserType=" + userType + "]";
}
}
And the Post Request defined in the Controller is as bellow:
package com.nidservices.yekoregistration.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.nidservices.yekoregistration.model.User;
import com.nidservices.yekoregistration.registry.ServiceRegistry;
#RestController
#RequestMapping("/user")
public class UserController {
#Autowired
private ServiceRegistry serviceRegistry;
#PostMapping
public void processStudentDetails(#RequestBody User user) {
serviceRegistry.getService(user.getUserType()).process(user);
}
}
Now I'm struggling to make the GET Request to get all created users. I'm used with the DAO design pattern and very new with the concept behind ServiceLocatorFactoryBean. I appreciate your help to help me implement my CRUD endpoints using ServiceLocatorFactoryBean. Thanks in advance.
I am getting the error while using hibernate in spring boot application No qualifying bean of type TransactionManager' available
I am using the following config class:
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
#org.springframework.context.annotation.Configuration
#EnableTransactionManagement
public class Config {
#Bean
public SessionFactory sessionFactory() {
Configuration configuration = new Configuration();
configuration.configure();
configuration.addAnnotatedClass(Ct.class);
configuration.addAnnotatedClass(St.class);
SessionFactory sessionFactory = configuration.buildSessionFactory();
return sessionFactory;
}
}
#RestController
public class RestAPIController {
#Autowired
private SessionFactory sessionFactory;
#PutMapping("/addS")
#Transactional
public void addSt(#RequestParam("cc") String cc,#RequestParam("st") String st) {
CC cc1= new CC();
CC.setCode(cc);
State state = new State(cc,st);
sessionFactory.getCurrentSession().save(state);
}
}
}
The main reason I added the #Transactional in the addSt method is due to error: The transaction was still an active when an exception occurred during Database.
So I turned to use spring boot for managing transactions. I am not sure what to do here.
--------------------UPDATED CODE--------------------
#Repository
public interface StateRepository extends CrudRepository<State, String> {}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.ArrayList;
import java.util.List;
#Service
#Transactional
public class StateService {
#Autowired
private StateRepository stateRepository;
public void save(State state) {
stateRepository.save(state);
}
public List<State> findAll() {
List<State> states = new ArrayList<>();
stateRepository.findAll().forEach(states::add);
return states;
}
}
For starters use proper layers and write a service and use JPA instead of plain Hibernate. If you want a Session you can always use EntityManager.unwrap to obtain the underlying Session.
#Service
#Transactional
public StateService {
#PersistenceContext
private EntityManager em;
public void save(State state) {
em.persist(state);
}
Use this service in your controller instead of the SessionFactory.
#RestController
public class RestAPIController {
private final StateService stateService;
RestAPIController(StateService stateService) {
this.stateService=stateService;
}
#PutMapping("/addS")
public void addSt(#RequestParam("cc") String cc, #RequestParam("st") String st) {
CC cc1= new CC();
CC.setCode(cc);
State state = new State(cc,st);
stateService.save(state);
}
}
Now ditch your Config class and restart the application.
NOTE
When using Spring Data JPA it is even easier, define a repository extending CrudRepository and inject that into the service instead of an EntityManager. (I'm assuming that Long is the type of primary key you defined).
public interface StateRepository extends CrudRepository<State, Long> {}
#Service
#Transactional
public StateService {
private final StateRepository states;
public StateService(StateRepository states) {
this.states=states;
}
public void save(State state) {
states.save(state);
}
}
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
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;
}
I have a web service which receives a data object(Let's call the class Student). At the web service, I wrap it using a StudentWrapper object as follows
new StudentWrapper(student)
and I want the StudentWrapper class to have methods such as save which would save the data to the database. I want to use the spring framework to annotate the save method so that it will run within a transaction. But then the StudendWrapper object would have to be a spring bean(defined in XML). If it is a spring bean, then I won't be instantiating it as I have shown above.
My question is how can I make the StudentWrapper a Spring bean (so that I can use Spring annotations to manage the transactions) but pass the Student object (that I receive over the web service) in to the StudentWrapper?
If there are any other suggestions that would help me in solving this problem, please share them as well.
If you really want to create the object using a constructor, make the StudentWrapper #Configurable and read up about using AspectJ to create prototype bean definitions for domain objects (section 9.8 of the reference manual.)
A simpler alternative, if you don't want to go with AspectJ but don't want a direct dependency on Spring is to encapsulate the prototype bean creation in a factory. I'll show you using JavaConfig, though you can do something similar in XML.
First the student object...
package internal;
public class Student {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
#Override
public String toString() {
return "Student{name='" + name + "'}";
}
}
And now the wrapper object...
package internal;
public class StudentWrapper {
private Student student;
public StudentWrapper(Student student) {
this.student = student;
}
public Student getStudent() {
return student;
}
#Override
public String toString() {
return "StudentWrapper{student='" + student + "'} " + super.toString();
}
}
And now the factory,
package internal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
#Component
public class StudentWrapperFactory {
#Autowired
private ApplicationContext applicationContext;
public StudentWrapper newStudentWrapper(Student student) {
return (StudentWrapper) this.applicationContext.getBean("studentWrapper", student);
}
}
And now the JavaConfig, equivalent to an XML configuration
package internal;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
#Configuration
#ComponentScan(basePackages = "internal")
public class FooConfig {
#Bean
#Scope("prototype")
public StudentWrapper studentWrapper(Student student) {
return new StudentWrapper(student);
}
}
Finally the unit test...
package internal;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = {FooConfig.class})
public class FooIntegrationTest {
#Autowired
private StudentWrapperFactory studentWrapperFactory;
#Test
public void foo() {
Student student1 = new Student("student 1");
Student student2 = new Student("student 2");
StudentWrapper bean1 = this.studentWrapperFactory.newStudentWrapper(student1);
StudentWrapper bean2 = this.studentWrapperFactory.newStudentWrapper(student2);
System.out.println(bean1);
System.out.println(bean2);
}
}
produces
StudentWrapper{student='Student{name='student 1'}'} internal.StudentWrapper#1b0fa7ff
StudentWrapper{student='Student{name='student 2'}'} internal.StudentWrapper#20de643a
As you can see from the object references of StudentWrapper, they're different prototype beans. #Transactional methods should work as expected in StudentWrapper.