How to programmatically Autowire a bean based on Qualifier Annotation - java

In Junit5, when using Extensions, we can run BeforeAll and AfterAll methods, and I'm trying to change behaviour of the test using Annotations.
However, I'm using these annotations as #Qualifiers as well for bean initialisation, and want to be able to initialise a bean using the Annotation identified on the test
I wish to programmatically Initialise the Bean on run time using my Qualifier Annotations
I know SpringExtension for Junit5 can get
SpringExtension.getApplicationContext(context).getAutowireCapableBeanFactory()
Using which I can call the bean initialisation factory, but I don't know how to initialise the beans using Annotations which are Qualifiers
I have multiple Beans of same type, identified by Qualifiers
The problem I'm stuck with is
Currently, I'm statically initialising the Type of user credentials using AutoWired and then basis the annotation I'm using the pre-initialized UserCredential using switch case.
The idea is to have a test class #ExtendsWith(ResetPortal.class) and then it indicates, which type of user it can use to Reset (Login before test) with.
I'm using Qualifier Annotations to indicate that, which I can then extract from ExtensionContext from the Junit5 beforeAll methods
Further, I have a UserCredential class being and multiple #Bean definitions for that class for each type of user.
Code
Bean definition, using custom qualifier annotation User1Qualifier
#Bean
#User1Qualifier
public static UserCredentials clientBankLogin(
#Value(LOGIN_USERNAME_1) String username,
#Value(LOGIN_PASSWORD_1) String password) {
return new UserCredentials(username, password);
}
With my Custom qualifier being as below (there are multiple)
#Qualifier
#CustomValueAnnotation("User1")
#Retention(RUNTIME)
#Target({FIELD, PARAMETER, TYPE, METHOD})
public #interface User1Qualifier {}
Now, in the tests, I'm trying to use the same Annotation, which the ResetPortal picks up
#SpringBootTest
#ExtendWith({ResetPortal.class, SpringExtension.class})
final class LoginTest extends SpringTestBase {
#Test
#User1Qualifier
void clientLogin() {}
}
The ResetPortal class, since Junit5 initialises the class and calls it's managed instance, needs Autowired elements to be defined separately
#Service
public class ResetPortal implements BeforeEachCallback, AfterEachCallback {
static UserCredentials user1;
static UserCredentials user2;
Autowired
public void initializeLoginToPortal(#User1Qualifier UserCredentials u1,
#User2Qualifier UserCredentials u2) {
user1 = u1;
user2 = u2;
}
#Override
public void beforeEach(ExtensionContext context) {
// This is the custom Value annotation marked on each UserQualifier
// This is using AnnotationUtils#findAnnotation(java.lang.Class<?>, java.lang.Class<A>)
CustomValueAnnotation loginAs = getLoginAsAnnotation(context);
switch (loginAs){
case "User1" : loginWith(ResetPortal.user1); break;
case "User2" : loginWith(ResetPortal.user2); break;
}
}
}

Related

Error org.springframework.beans.factory.UnsatisfiedDependencyException

I have a problem that prevents me from moving forward and I don't know how to solve it. I have a class called Validator where I store validation functions and I need to do unit tests for each one and validate its functionality, but there are some of them that use the Spring Environment instance in which it accesses the properties file. If I do "normal" unit tests, whenever I call the function where this feature is implemented, it returns me Environment is null. I have tried using #Autowired instead of instantiating with new in the test class, the use of #RunWith(SpringRunner.class) and since the test classes and the function classes are in different packages I have also used the #ComponentScan and it gives me an error... What am I doing wrong or what am I doing wrong?
I have the code of the Validator class in this way:
#Component
public class Validator {
#Autowired
public Environment env;
public CodRespuestaWS validateTypeOperation(TypeOperation typeOperation, String operation) {
String response = env.getProperty(typeOperation.toString() + "." + operation);
if (OK.equalsIgnoreCase(response)) {
return new CodResponseWS();
}
return new CodResponseWS(CodResponseWS.ER100, typeOperation.toString()+" not allowed:" + operation);
}
}
And in the test I do it this way:
#ComponentScan(basePackages = "com.functions.validators")
#RunWith(SpringRunner.class)
public class ValidateRequestHigh {
RequestHigh requestHigh = new RequestHigh();
CodResponseWS response;
#Autowired Validator validator;
HighValidator highValidator = new HighValidator();
UserData userData = new UserData();
#Test
public void test() throws Exception {
response = validator.validateTypeOperation(TypeOperation.typeOperationHigh, "high");
System.out.println(response.getCodResponse());
}
}
The problem that I mentioned before the NULL, is that when executing the test it did not even reach the print that I have set, but rather it stayed on the line
String response = env.getProperty(typeOperation.toString() + "." +
operation);
And it marked the error that env was NULL and now it returns a different one
And the error that returns me now is:
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name
'functiontest.unittest.ValidateRequestHighTest': Unsatisfied
dependency expressed through field 'validator'; nested exception is
org.springframework.beans.factory.NoSuchBeanDefinitionException: No
qualifying bean of type 'com.functions.validators.Validator'
available: expected at least 1 bean which qualifies as autowire
candidate. Dependency annotations:
{#org.springframework.beans.factory.annotation.Autowired(required=true)}
I guess it's already understood, but I'll detail it more just in case, the functions are in
src/main/java/com.functions.validators/Validators.java
and the tests
src/main/test/functiontest.unittest/ValidateRequestHighTest.java
I am using version 5.1.2.RELEASE of SpringBoot and JUnit 4
Using field injection makes Validator class impossible to be unit tested because it provides no way to pass the Environment instance to it. but you can spin up the whole application context and configure it to do dependency injection stuff using #SpringBootTest which is not a unit test but integration test.
if I do "normal" unit tests, whenever I call the function where this
feature is implemented, it returns me Environment is null.
Because when you use new to instantiate a bean its not a Spring managed bean anymore and its dependencies are not resolved.
I have tried using #Autowired instead of instantiating with new in the
test class, the use of #RunWith(SpringRunner.class) and since the test
classes and the function classes are in different packages I have also
used the #ComponentScan and it gives me an error
You should also add SpringBootTest annotation for loading into application context.
If you want to really unit test your class you don't need application context instead do the following
Change the Validator class like:
#Component
public class Validator {
private Environment env;
public Validator(Environment env) {
this.env = env;
}
....
}
And for unit testing it do:
public class ValidatorTest {
Validator validator;
MockEnvironment environment;
#Before
public void setUp() {
environment = new MockEnvironment();
environment.setProperty("key1", "value1");
//...
environment.setProperty("keyn", "valuen");
validator = new Validator(environment);
}
#Test
public void test() {
// test stuff
}
}

Spring Boot Failed to use #Value for injection

Spring Boot version <version>2.2.0.RELEASE</version>
Error goes as follows:
Description:
Parameter 2 of constructor in
com.shawn.foodrating.service.impl.AdServiceImpl required a bean of
type java.lang.Integer that could not be found.
Action:
Consider defining a bean of type 'java.lang.Integer' in your configuration.
My Code:
#Service
#Transactional(rollbackOn = Exception.class)
#AllArgsConstructor
public class AdServiceImpl implements AdService {
private AdRepository repository;
private FileService fileService;
#Value("${app.ad.DefaultPageSize}")
private Integer DEFAULT_PageSize;
#Value("${app.ad.ImagePath}")
private String AD_IMAGE_PATH;
#Value("${app.ad.ImageUrl}")
private String AD_IMAGE_URL;
Load property file
#SpringBootApplication
#PropertySource("classpath:app.properties")
public class FoodRatingApplication {
public static void main(String[] args) {
SpringApplication.run(FoodRatingApplication.class, args);
}
}
Not Sure what is wrong with it.
When you use Lombok's #AllArgsConstructor it must create a constructor for all your fields, those annotated with #Value and those that aren't.
Now Lombok doesn't even know anything about #Value annotation of spring. So the generated constructor looks something like this:
public AdServiceImpl(AdRepository repository, FileService fileService, Integer DEFAULT_PageSize, String AD_IMAGE_PATH, String AD_IMAGE_URL) {
this.repository = repository;
....
}
You can run Delombok to see the actually generated code.
Spring on the other hand when sees a single constuctor tries to call it to create the bean (AdServiceImpl) in this case, and only after that iterates through its fields and inject data annotated by #Value.
Now, when spring calls the constructor, it sees an integer (DEFAULT_PageSize), has no clue that its a value (and spring has to inject something brcause its a constructor injection), and throws an Exception.
So in terms of resolution:
Don't use all args constructor of lombok in this case and instead create a non-lombok constructor for AdRepository and FileService only)
Alternatively create a constructor with #Value annotated parameters instead of field injection (remove #Value on fields):
public AdServiceImpl(AdRepository repository, FileService fileService, #Value(${app.ad.DefaultPageSize}"} Integer DEFAULT_PageSize, #Value(...) String AD_IMAGE_PATH, #Value(...) String AD_IMAGE_URL) {
this.repository = repository;
....
}
The root cause is that you are using Lombok #AllArgsConstructor while some of the properties are populated by #Value(..).
Replace the #AllArgsConstructor with #RequiredArgsConstructor and give it a try.
Note: Adding this comment for future references.
Add to projeck lombok.config file with this lines:
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value

How to qualify bean with constructor injection

I have an interface which is defined in two places like that:
#Configuration
public class AppContext {
#Bean
public SomeInterface usageOne() {
return new SameImpl();
}
#Bean
public SomeInterface usageTwo() {
return new SameImpl(someOtherConfig);
}
#Bean
public Client clientOne(SomeInterface usageOne) {
return new Client(usageOne);
}
#Bean
public OtherClient clientTwo(SomeInterface usageTwo) {
return new OtherClient(usageTwo);
}
}
My client implementation classes do not have any annotations only required constructor. How to qualify the correct interface implementation usage in that case? I don't want to use #Primary as in my case it's semantically incorrect to name one of the usages as primary (they are in some sense equal). I need to pass the same interface with the same implementation class but configured differently for specific use cases of respected clients. I was thinking that naming the parameter by which I inject the implementation to the bean creation method is enough, but Spring complains with: required a single bean, but 2 were found. I don't understand how should I use the #Qualifier annotation.
I'm running with Spring Boot 2.0.4.RELEASE and respected beans and clients created in separate configuration classes so I cannot just call usageTwo() method when creating OtherClient like that: new OtherClient(usageTwo()); as this method is not available in clients configuration class.
As mentioned by #chrylis in the comments, you can simply add the #Qualifier annotation to the #Bean methods like this:
#Bean
public Client clientOne(#Qualifier("usageOne") SomeInterface usageOne) {
return new Client(usageOne);
}
#Bean
public OtherClient clientTwo(#Qualifier("usageTwo") SomeInterface usageTwo) {
return new OtherClient(usageTwo);
}
The value specified as value for the #Qualifier annotation is the respective bean's name. That is either the name of the corresponding #Bean method or the value of the annotation if used like this #Bean("usageThree").

Trying to inject an object into JUnit test

I have had to adapt a project I've been working on to work differently, using an injected object (documentDao) to access the methods for adding/updating/etc. records in a database. Where necessary I simply injected this object into the constructor, but of course this won't work with JUnit tests (which can only have no-argument constructors), so I'm stuck on how to get the object into the test class.
The first code snippet shows a dumbed-down version of one of the test classes. The problem is that I need to create the documentDao object so I can pass it as an argument into the BackendApiController instantiation statement.
The second snippet is the first part of the DocumentDaoImpl class, which needs to be injected.
Any suggestions would be welcomed.
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(loader = AnnotationConfigContextLoader.class)
public class ApiBackendTests {
#Configuration
#PropertySource(value = "classpath:system.properties")
static class ContextConfiguration {
}
private static BackendApiController backendApiController = new BackendApiController(documentDao);
#Test
public void retrieveSampleStatementList() {
String response = backendApiController.genericStatementList(x,y,z);
String eStatementId = "";
if (response.indexOf("_id") > 0) {
eStatementId = response.substring(response.indexOf("<_id>") + 5, response.indexOf("</_id>"));
}
// if this test is true, then at least one statement document was found in the above search.
assertTrue(response.indexOf("_id") > 0);
}
}
#Repository
public class DocumentDaoImpl<T> implements DocumentDao<T> {
public DocumentDaoImpl() {
}
#Inject
DBCollection dbCollection;
#Inject
GridFS gridFS;
#Autowired
ObjectMapper objectMapper;
#Override
public String insert(CommonDocument document) {
There's still not enough information to say anything for sure, but I believe you can try using #Autowired to wire up your needed component:
#Autowired
private DocumentDao documentDao;
You got the error:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.roler.res.test.ApiBackendTests': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.roler.res.mongodb.dao.DocumentDao
That means Spring isn't aware of the DocumentDao bean yet. There are several ways to do that, but I think the easiest way is putting this in your configuration context:
<context:component-scan base-package="package.contain.your.dao"/>
It will tell Spring to scan the package in search for components with annotation.
UPDATE: since you don't use XML configuration, #ComponentScan is the way to go

How To Access Spring Bean name?

#Named("myUniqueName")
public class ReportDashboardDao implements DashboardDAO{
//STUFF
}
how can i access the string inside #Named tag when i am injecting DashboardDAO like this :
#Named
public class DshboardDaoConsumer(){
#Inject List<DashboardDAO> dashboardDAO;
//STUFF
}
Use a Map instead
#Inject
Map<String, DashboardDao> dashBoardDaos;
This will inject a Map with bean names as keys and daos as values.
Of course, you could also read the annotation value from class instances.
You can't. You're injecting by type. After injection has been done, Spring does not leave behind any relation between the bean's object and the bean's name.
You might want to check out ApplicationContext#getBeanNamesByType() depending on what you want to do.
By implementing BeanNameAware.
#Named("myUniqueName")
public class ReportDashboardDao implements DashboardDAO, BeanNameAware{
//STUFF
private String beanName;
#Override
public Void setBeanName(String beanName) {
this.beanName = beanName;
}
}
So that Spring can inject the beanName into the bean. If you add a public String getBeanName(); in your DashboardDAO interface, DashboardDaoConsumer will be able to obtain it.
In this particular case, Spring will inject the name you specified in the annotation.

Categories