I have two datasources that are equal in their structure but not in their Data.
My application has to deal with both of them at the same time.
I have controller, servie, dao structure that looks like this.
Controller Modell:
#Controller
public abstract class MyFancyControllerModell{
private MyService service;
public MyFancyControllerModell (MyService service){
this.service = service;
}
#RequestMapping(....)
public void editSomeData(String data){....}
}
Controller implementation:
#Controller
#RequestMapping(...)
public class MyControllerImpl1 extends MyFancyControllerModell{
#Autowired
public MyControllerImpl1(#Qualifier("myServiceInstance1") MyService service){
super(service);
}
}
#Controller
#RequestMapping(...)
public class MyControllerImpl2 extends MyFancyControllerModell{
#Autowired
public MyControllerImpl2(#Qualifier("myServiceInstance2") MyService service){
super(service);
}
}
And the Service:
public class MyService{
private MyDao myDao;
public MyService(MyDao myDao){
this.myDao = myDao;
}
#Transactional
public void editSomeData(String data){...}
}
I create the Beans in my configuration class like this:
private DataSource lookupDataSource(final String jndiName) {
final JndiDataSourceLookup dsLookup = new JndiDataSourceLookup();
dsLookup.setResourceRef(true);
return dsLookup.getDataSource(jndiName);
}
#Bean
public DataSource dataSource1(){
return lookUpDataSource("dataSource1");
}
#Bean
public DataSource dataSource2(){
return lookUpDataSource("dataSource2");
}
#Bean
public MyService myServiceInstance1(#Qualifier("dataSource1") DataSource dataSource){
return new(MyService(new MyDao(dataSource))));
}
#Bean
public MyService myServiceInstance1(#Qualifier("dataSource1") DataSource dataSource){
return new(MyService(new MyDao(dataSource)));
}
My question is, is it possible to create transactionmanagers for both datasources without the need to declare in the service layer which transactionmanager is used?
I tried creating them as bean just like the services but that did not work.
Check out here the answers for previous quesion related to transaction manager...
https://stackoverflow.com/a/1961853/7504001
Related
I am using spring-boot-starter-jdbc and trying to use multiple Jdbc DataSources, everything worked when I used named Beans for my JdbcTemplate and then used #Qualifier to inject the right JdbcTemplate into my repository.
Application:
package my.app;
#SpringBootApplication
#EnableAutoConfiguration
public class Application implements CommandLineRunner {
#Autowired
private MyRepository repository;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Override
public void run(String... args) throws Exception {
List<Stuff> stuff = repository.getStuff();
}
}
Configuration:
package my.app;
#Configuration
#ComponentScan
public class AppConfig {
#Bean(name = "datasource1")
#ConfigurationProperties(prefix = "db1.datasource")
public DataSource dataSource1() {
return DataSourceBuilder.create().build();
}
#Bean(name = "db1")
public DataSource db1(#Qualifier("datasource1" DataSource ds) {
return new JdbcTemplate(ds);
}
#Bean(name = "datasource2")
#ConfigurationProperties(prefix = "db2.datasource")
public DataSource dataSource2() {
return DataSourceBuilder.create().build();
}
#Bean(name = "db2")
public DataSource db1(#Qualifier("datasource1" DataSource ds) {
return new JdbcTemplate(ds);
}
}
Repository:
package my.app;
#Repository
public class MyRepository {
private JdbcTemplate db1;
private JdbcTemplate db2;
#Autowired
public class MyRepository(#Qualifier("db1") JdbcTemplate db1, #Qualifier("db2") JdbcTemplate db2) {
this.db1 = db1;
this.db2 = db2;
}
}
When I instantiate MyRepository everything is fine.
I did a little refactoring to try to make new classes for Db1JdbcTemplate and Db2JdbcTemplate so I could inject them without qualifiers. Unfortunately when I do this I get the following exception:
Here's what I attempted to do:
Removed named JdbcTemplate Beans from AppConfig:
package my.app;
#Configuration
#ComponentScan
public class AppConfig {
#Bean(name = "datasource1")
#ConfigurationProperties(prefix = "db1.datasource")
public DataSource dataSource1() {
return DataSourceBuilder.create().build();
}
#Bean(name = "datasource2")
#ConfigurationProperties(prefix = "db2.datasource")
public DataSource dataSource2() {
return DataSourceBuilder.create().build();
}
}
Created 2 new classes for named JdbcTemplates:
package my.app;
#Component
public class Db1JdbcTemplate extends JdbcTemplate {
#Autowired
public Db1JdbcTemplate(#Qualifier("datasource1") DataSource ds1) {
super(ds1);
}
}
package my.app;
#Component
public class Db2JdbcTemplate extends JdbcTemplate {
#Autowired
public Db2JdbcTemplate(#Qualifier("datasource2") DataSource ds2) {
super(ds2);
}
}
Modified MyRepository to use those:
package my.app;
#Repository
public class MyRepository {
private Db1JdbcTemplate db1;
private Db2JdbcTemplate db2;
#Autowired
public class MyRepository(Db1JdbcTemplate db1, Db2JdbcTemplate db2) {
this.db1 = db1;
this.db2 = db2;
}
}
So I have #Component annotations on both of those so I expected to be able to be registered and be able to be Autowired. However when I run things I get the following exception:
Error creating bean with name 'repository' defined in file
[/Users/me/spring/target/classes/my/app/MyRepository.class]:
Unsatisfied dependency expressed through constructor parameter 0;
nested exception is
org.springframework.beans.factory.NoSuchBeanDefinitionException: No
qualifying bean of type 'my.app.Db1JdbcTemplate' available: expected
at least 1 bean which qualifies as autowire candidate. Dependency
annotations: {}
Any ideas what I need to change or how I can debug further? I can always go back to the working version but I'd prefer to be more explicit when possible so the compiler can help me out instead of runtime errors caused by a typo'd Qualifier name.
A more standard way of doing this and avoid typos in qualifier names is to avoid using qualifier names, and instead use Qualifier annotations:
#Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Qualifier
public #interface DB1 {
}
#Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Qualifier
public #interface DB2 {
}
and then, when defining your beans:
#Configuration
#ComponentScan
public class AppConfig {
#Bean
#ConfigurationProperties(prefix = "db1.datasource")
#DB1
public DataSource dataSource1() {
return DataSourceBuilder.create().build();
}
#Bean
#DB1
public JdbcTemplate db1(#DB1 DataSource ds) {
return new JdbcTemplate(ds);
}
#Bean
#ConfigurationProperties(prefix = "db2.datasource")
#DB2
public DataSource dataSource2() {
return DataSourceBuilder.create().build();
}
#Bean
#DB2
public JdbcTemplate db1(#DB2 DataSource ds) {
return new JdbcTemplate(ds);
}
}
And finally when injecting your beans:
public class MyRepository(#DB1 JdbcTemplate db1, #DB2 JdbcTemplate db2) {
...
}
If the main concern is typos, just define your names as constants (something like public static final String DATASOURCE1 = "datasource1";) and consistently use those in your qualifier annotations instead of string literals. No need to add new classes or interfaces.
I have a spring boot test as below
#SpringBootTest(class=AppConfig.class)
Public class AppTest{
#Autowired
private Product product
#Test
Public void test(){
.....
.....
}
}
My AppConfig.class is as below
Public clas AppConfig{
#Mock
EMailService emailService;
public AppConfig(){
MockitoAnnotations.initMocks(this)
}
#Bean
Public Product getProduct(){
return new Product();
}
}
Class Product{
#Autowired
private EMailService emailService
.....
......
}
Even after i defined #Mock EMailService emailService, whem i run the test, I get error EMailService bean not defined.
In your AppTest class
#SpringBootTest(class=AppConfig.class)
public class AppTest{
#Mock
private EMailService emailService;
#InjectMocks
private Product product;
#Test
public void test(){
.....
.....
}
}
Also, I think you do not need the definitions in the AppConfig class anymore
I am creating simple spring boot application. I used spring transaction management to handle the transaction. Here is my code.
ServiceImpl class
#Service("orderService")
public class OrderServiceImpl implements OrderService {
#Autowired
private CustomerDao customerDao;
#Autowired
private OrderDao orderDao;
#Transactional(rollbackFor = Exception.class)
#Override
public Long placeOrder(OrderPlacementRequest request) {
customerDao.save(request.getCustomer());
return orderDao.placeOrder(request.getOrder());
}
}
OrderDaoImpl class,
#Repository("orderDao")
public class OrderDaoImpl extends AbstractHibernateDao implements OrderDao {
#Override
public Long placeOrder(Order order) {
throw new RuntimeException("Test Error Message");
}
}
Configuration class,
#Configuration
#EnableTransactionManagement
public class HibernateConfig {
//Other Configurations
#Autowired
private final Environment environment;
#Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(environment.getRequiredProperty("spring.datasource.driver-class-name"));
dataSource.setUrl(environment.getRequiredProperty("spring.datasource.url"));
dataSource.setUsername(environment.getRequiredProperty("spring.datasource.username"));
dataSource.setPassword(environment.getRequiredProperty("spring.datasource.password"));
return dataSource;
}
#Bean
public DataSourceTransactionManager transactionManager() {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource());
return transactionManager;
}
}
application.properties
spring.aop.proxy-target-class=true
OrderController class
#RestController
#RequestMapping("/order")
public class OrderController {
#Autowired
private OrderService orderService;
#RequestMapping(value = "/place", method = RequestMethod.POST)
public Long placeOrder(#RequestBody OrderPlacementRequest request) {
return orderService.placeOrder(request);
}
}
In my case even though second method placeOrder failed. Customer will be saved in mysql database. What I wanted is to rollback the customer saving method. I went through few articles on transaction management including spring docs and stackoverflow. Still can not find the problem.
Updated with Controller class.
I have a config class that provides two implemenations of the same base bean interface. I want these to be set on the autowired fields conditionally based on an annotation on the field.
public class MyController
{
#Autowired
private MyBeanInterface base;
#Autowired
#MyAnnotation
private MyBeanInterface special;
}
This is a pesudo-code of the config class:
#Configuration
public class ConfigClass
{
#Bean
#Primary
public MyBeanInterface getNormalBeanInterface()
{
return new MyBeanInterfaceImpl();
}
#Bean
//This doesn't work
#ConditionalOnClass(MyAnnotation.class)
public MyBeanInterface getSpecialBeanInterface()
{
return new MyBeanInterfaceForMyAnnotation();
}
}
How can I make the annotated field be populated by the second bean?
Use Qualifier annotation. Example:
Controller:
Add Qualifier annotation at the injected fields with bean id as parameter:
public class MyController
{
#Autowired
#Qualifier("normalBean")
private MyBeanInterface base;
#Autowired
#Qualifier("specialBean")
private MyBeanInterface special;
}
ConfigClass
Specify bean id:
#Configuration
public class ConfigClass
{
#Bean(name="normalBean")
#Primary
public MyBeanInterface getNormalBeanInterface()
{
return new MyBeanInterfaceImpl();
}
#Bean(name="specialBean")
public MyBeanInterface getSpecialBeanInterface()
{
return new MyBeanInterfaceForMyAnnotation();
}
}
I have a #Service bean that I need static access to:
#Service
public class MyBean implements InitializingBean {
private static MyBean instance;
#Override
public void afterPropertiesSet() throws Exception {
instance = this;
}
public static MyBean get() {
return instance;
}
public String someMethod(String param) {
return "some";
}
}
Usage:
#Service
public class OtherService {
public static void makeUse() {
MyBean myBean = MyBean.get();
}
}
Problem: when I write an integration junit test for OtherService that makes use of the stat MyBean access, the instance variable is always null.
#RunWith(SpringRunner.class)
#SpringBootTest
public class ITest {
#Autowired
private OtherService service;
#MockBean
private MyBean myBean;
#Before
public void mock() {
Mockito.when(myBean.someMethod(any()).thenReturn("testvalue");
}
#Test
public void test() {
service.makeUse(); //NullPointerException, as instance is null in MyBean
}
}
Question: how can I write an integration test when using such type of static access to a spring-managed bean?
If you want to influence the #Bean-creation, then use #Configuration
#Configuration
public class MyConfig {
#Bean
public MyBean myBean() {
return new MyBean;
}
#Bean
public OtherService otherService () {
return new OtherService(myBean());
}
}
Then mocking becomes really easy:
#RunWith(SpringRunner.class)
#SpringBootTest
public class ITest {
#MockBean
private MyBean myBean;
#Autowired
private OtherService service;
#Test
public void test() {
// service is initialised with myBean
...
}
}
When more control is needed, then you can choose the following approach. It provides sometimes more control than just a #MockBean. In your test you can easily mock a method just before calling it. This is my preferred way.
#Configuration
public class MyConfig {
...
#Bean
public MyBean getMyBean() {
return mock( MyBean.class);
}
#Bean
public OtherService otherService () {
return new OtherService( getMyBean());
}
}
In the application you can use #Autorwired to access it AND implement method overrule/mocking easily.
#RunWith(SpringRunner.class)
#SpringBootTest
public class AnotherTest {
#Autowired
private MyBean myBean;
#Autowired
private OtherService service;
#Test
public void test() {
when( myBean.method( a, b)).thenReturn( something);
service.doSomething( ....); // with myBean.method() overruled
}
}