Spring Annotation and Qualifier Based Scan - java

Is there a way in spring to apply qualifiers while performing an annotation based component scan?
I have a couple of classes annotated with my custom annotation, MyAnnotation.
#MyAnnotation
public class ClassOne {
}
#MyAnnotation
public class ClassTwo {
}
#Configuration
#ComponentScan(basePackages = { "common" }, useDefaultFilters = false, includeFilters = { #ComponentScan.Filter(type = FilterType.ANNOTATION, value = MyAnnotation.class) })
public class ClassProvider {
}
What I want to do is, scan a subset of the classes with this annotation, selectively based on some condition say some input from the user.
Is it possible to say specify a qualifier along with the annotation and also specify it with the component scan filter, something like this -
#MyAnnotation (qualifier = "one")
public class ClassOne {
}
#MyAnnotation (qualifier = "two")
public class ClassTwo {
}
#Configuration
#ComponentScan(basePackages = { "common" }, useDefaultFilters = false, includeFilters = { #ComponentScan.Filter(type = FilterType.ANNOTATION, value = MyAnnotation.class, qualifier = "one") })
public class ClassProvider {
}
so that only ClassOne gets scanned?

You can implement a custom TypeFilter so that your #ComponentScan can look like:
#ComponentScan(includeFilters = { #ComponentScan.Filter(type = FilterType.CUSTOM, value = MyAnnotation.class) })
And the TypeFilter implementation:
public class TypeOneFilter implements TypeFilter {
#Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
final AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
if (annotationMetadata.hasAnnotation(MyAnnotation.class.getName())) {
final Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(MyAnnotation.class.getName());
return "one".equals(attributes.get("qualifier"));
}
return false;
}
}

Related

ConfigurationProperties doesn't work when different prefixes and same values are used

I have the following yaml-property file:
myPrefix:
value: Hello
myPrefix2:
value: World
And two classes
#PropertySource("classpath:permission-config.yml")
#ConfigurationProperties(prefix = "myPrefix")
#Component
#Getter
#Setter
public class ViewUsers {
private String value;
}
and
#PropertySource("classpath:permission-config.yml")
#ConfigurationProperties(prefix = "myPrefix2")
#Component
#Getter
#Setter
public class ManageUsers {
private String value;
}
Then null is injected.
Or, if I try to use #Value then ONLY latest value is retrieved, which is the last one (World), the preceding ones are always ignored.
Try the following approach:
Remove #Component from configuration properties (ViewUsers and ManageUsers)
Instead, use the following construction:
#PropertySource("classpath:permission-config.yml")
#ConfigurationProperties(prefix = "myPrefix")
public class ViewUsers {
private String value; // getter, setter
}
#PropertySource("classpath:permission-config.yml")
#ConfigurationProperties(prefix = "myPrefix2")
public class ManageUsers {
private String value; // getter, setter
}
#Configuration
#EnableConfigurationProperties({ViewUsers.class, ManageUsers.class}
public class MySampleConfiguration {
... beans here...
}
Also make sure that Lombok annotations are working as expected (Try to use without lombok just for the POC).
Update 1
As #M. Deinum has kindly stated, PropertySource like this doesn't work with yaml files.
You can try the following workaround:
#Configuration
#PropertySource(name = "someName", value = "classpath:foo.yaml", factory = YamlPropertySourceFactory.class)
public class MyConfig {
}
import org.springframework.boot.env.YamlPropertySourceLoader;
public class YamlPropertySourceFactory implements PropertySourceFactory {
#Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
final List<PropertySource<?>> load = new YamlPropertySourceLoader().load(name, resource.getResource());
return load.get(0);
}
}

Custom Spring (Boot) annotation: #ConditionalOnProperty with default key name

How to create a custom Spring (Boot) annotation #Country that "inherits" #ConditionalOnProperty, but predefines the property key?
Given a some services that share a common interface
interface WizardService {
void doMagic()
}
and a set of country specific implementations that are selected via #ConditionalOnProperty(name = "country", havingValue = "[iso-code]"), i.e. the implementation is selected based on the value of the country property
#ConditionalOnProperty(name = "country", havingValue = "de")
#Service
class WizardServiceGermany {
#Override void doMagic() { System.out.println("Simsalabim!"); }
}
#ConditionalOnProperty(name = "country", havingValue = "en")
#Service
class WizardServiceGreatBritain {
#Override void doMagic() { System.out.println("Wingardium Leviosa!"); }
}
is it possible to define a custom spring annotation which always sets the name property to "country" by default so that I can have an #Country annotation? For instance:
#Country("en")
#Service
class WizardServiceGreatBritain {
#Override void doMagic() { System.out.println("Wingardium Leviosa!"); }
}
I tried creating a meta-annotation, but it gets ignored (while replacing it with its #ConditionalOnProperty equivalent works fine):
#ConditionalOnProperty(name = "country")
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
#Documented
public #interface Country {
#AliasFor(annotation = ConditionalOnProperty.class, attribute = "havingValue")
String havingValue() default "";
}

How to use dependency-injection with SpringBootTest

I have an application that uses SpringBoot for dependency injection and the app works fine, but testing fails because #Autowired fields aren't being injected during tests.
#SpringBootApplication
public class ProcessorInterface {
protected final static Logger logger = Logger.getLogger( ProcessorInterface.class );
public static void main(String[] args) {
try {
SpringApplication.run(ProcessorInterfaceRunner.class, args);
} catch (Exception ex) {
logger.error("Error running ProcessorInterface", ex);
}
}
}
#Component
#Configuration
#ComponentScan
public class ProcessorInterfaceRunner implements CommandLineRunner {
protected final static Logger logger = Logger.getLogger( ProcessorInterface.class );
#Autowired
private RequestService requestService = null;
#Autowired
private ValidatorService validatorService = null;
#Override
public void run(String... args) throws Exception {
ESPOutTransaction outTransaction = null;
outTransaction = new ESPOutTransaction();
// initialize outTransaction fields
...
// done initializing outTransaction fields
if (validatorService.isValid(outTransaction)) {
System.out.println(requestService.getRequest(outTransaction));
} else {
System.out.println("Bad Data");
}
}
}
#Service
public class ESPRequestService implements RequestService<ESPOutTransaction> {
#Autowired
ValidatorService validatorService = null;
#Override
public String getRequest(ESPOutTransaction outTransaction) throws IllegalArgumentException {
if (!validatorService.isValid(outTransaction)) {
throw new IllegalArgumentException("Invalid parameters in transaction object. " + outTransaction.toString());
}
StringBuffer buff = new StringBuffer("create request XML");
buff.append("more XML");
return buff.toString();
}
}
#Service
public class ESPValidatorService implements ValidatorService {
private static org.apache.log4j.Logger logger = Logger.getLogger(ESPValidatorService.class);
// declare some constants for rules
private static final int MAX_LENGTH_XYZ = 3;
#Override
public boolean isValid(OutTransaction outTransaction) {
ESPOutTransaction espOutTransaction = (ESPOutTransaction)outTransaction;
boolean isValid = true;
if (espOutTransaction == null) {
logger.warn("espOutTransaction is NULL");
isValid = false;
} else {
// XYZ is required
if (espOutTransaction.getXYZ() == null) {
logger.warn("XYZis NULL\r\n" + espOutTransaction.toString());
isValid = false;
}
// XYZ max length = MAX_LENGTH_XYZ
if (espOutTransaction.getXYZ() != null && espOutTransaction.getPubCode().trim().length() > MAX_LENGTH_XYZ) {
logger.warn("XYZis too long (max length " + MAX_LENGTH_XYZ + ")\r\n" + espOutTransaction.toString());
isValid = false;
}
}
return isValid;
}
}
These all work and I get good output when I run the app. When I try to test it though, it fails because it can't find ESPValidatorService to inject into ESPRequestService
#RunWith(Suite.class)
#SuiteClasses({ ESPOutTransactionValidatorTest.class, ESPRequestTest.class })
public class AllTests {}
#RunWith(SpringRunner.class)
#SpringBootTest(classes = {ESPRequestService.class})
public class ESPRequestTest {
#Test
public void testGetRequest() {
ESPRequestService requestService = new ESPRequestService();
String XYZ = "XYZ";
ESPOutTransaction outTransaction = null;
outTransaction = new ESPOutTransaction();
outTransaction.setXYZ(XYZ);
String strRequest = "some expected request XML";
String request = requestService.getRequest(outTransaction);
assertEquals(request, strRequest);
}
}
#RunWith(SpringRunner.class)
#SpringBootTest(classes = ESPValidatorService.class)
public class ESPOutTransactionValidatorTest {
#Test
public void testIsValid() {
ESPValidatorService validatorService = new ESPValidatorService();
ESPOutTransaction outTransaction = null;
// test request = null
assertFalse(validatorService.isValid(outTransaction));
String XYZ = "XYZ";
outTransaction = new ESPOutTransaction();
outTransaction.setXYZ(XYZ);
// test all good
assertTrue(validatorService.isValid(outTransaction));
// test XYZ
outTransaction.setXYZ(null);
assertFalse(validatorService.isValid(outTransaction));
outTransaction.setXYZ("ABCD"); // too long
assertFalse(validatorService.isValid(outTransaction));
outTransaction.setXYZ(XYZ);
}
}
How can I get the unit tests to auto wire?
I see two problems :
1) you don't rely on Spring beans but you create instances with the new operator.
Instead of writing :
ESPRequestService requestService = new ESPRequestService();
you should let Spring inject the instance :
#Bean
ESPRequestService requestService;
2) The #SpringBootTest configuration is not correct.
In each test, you specified a very specific bean class in the classes attribute of #SpringBootTest :
#SpringBootTest(classes = ESPValidatorService.class)
public class ESPOutTransactionValidatorTest {
and
#SpringBootTest(classes = {ESPRequestService.class})
public class ESPRequestTest {
But classes attributes of #SpringBootTest serves to specify the annotated classes to use for loading an ApplicationContext.
The annotated classes to use for loading an ApplicationContext. Can
also be specified using #ContextConfiguration(classes=...). If no
explicit classes are defined the test will look for nested
#Configuration classes, before falling back to a
SpringBootConfiguration search.
So all configuration classes and beans of your application may not be discovered and loaded in the Spring container .
To be able to load all application beans during your tests, the most simple way is not specifying the classes attribute in the #SpringBootTest annotation :
#SpringBootTest
public class ESPRequestTest { ...}
It will look for a Spring bean that holds the #SpringBootConfiguration.
Ideally, it will found the #SpringBootApplication bean of your application.
If the package of the test class is located inside the package (or at a lower level) of the #SpringBootApplication class, it should be automatically discovered.
Otherwise the other way is specifying a configuration that will allow to load all required beans :
#SpringBootTest(classes = MySpringBootApplication.class)
public class ESPRequestTest { ...}

Making a defined spring bean primary

I have 3 beans of the same type defined in spring.xml. This is inside a jar file which I cannot edit. I want to make one of these primary in my spring-boot application using annotation. Is there a way to do it?
A straightforward approach is to use a bridge configuration, which will register the desired bean as a new primary bean. A simple example:
The interface:
public interface Greeter { String greet(); }
The configuration which you don't control:
#Configuration
public class Config1 {
#Bean public Greeter british(){ return () -> "Hi"; }
#Bean public Greeter obiWan(){ return () -> "Hello there"; }
#Bean public Greeter american(){ return () -> "Howdy"; }
}
The bridge configuration:
#Configuration
public class Config2 {
#Primary #Bean public Greeter primary(#Qualifier("obiWan") Greeter g) {
return g;
}
}
The client code:
#RestController
public class ControllerImpl {
#Autowired
Greeter greeter;
#RequestMapping(path = "/test", method = RequestMethod.GET)
public String test() {
return greeter.greet();
}
}
Result of curl http://localhost:8080/test will be
Hello there
You can use #Qualifier("___beanName__") annotation to choose the correct one
I tried #jihor solutions but it doesn't work. I have a NullPointerException in defined configuration.
Then I find the next solution on Spring Boot
#Configuration
public class Config1 {
#Bean
#ConfigurationProperties(prefix = "spring.datasource.ddbb")
public JndiPropertyHolder ddbbProperties() {
return new JndiPropertyHolder();
}
#ConditionalOnProperty(name = "spring.datasource.ddbb.primary", matchIfMissing = false, havingValue = "true")
#Bean("ddbbDataSource")
#Primary
public DataSource ddbbDataSourcePrimary() {
return new JndiDataSourceLookup().getDataSource(ddbbProperties().getJndiName());
}
#ConditionalOnProperty(name = "spring.datasource.ddbb.primary", matchIfMissing = true, havingValue = "false")
#Bean("ddbbDataSource")
public DataSource ddbbDataSource() {
return new JndiDataSourceLookup().getDataSource(ddbbProperties().getJndiName());
}
}
And my application.properties if I need this datasource as primary, otherwise don't set the property or set false.
spring.datasource.ddbb.primary=true

Spring Boot Autowiring of JsonDeserializer in Integration test

I have an application which needs a service to be Spring wired in a JsonDeserializer. The problem is that when I start up the application normally it is wired, but when I start it up in a test, it is null.
The relevant code is:
JSON Serializer/Deserializer:
#Component
public class CountryJsonSupport {
#Component
public static class Deserializer extends JsonDeserializer<Country> {
#Autowired
private CountryService service;
#Override
public Country deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
return service.getById(jsonParser.getValueAsLong());
}
}
}
Domain Object:
public class BookingLine extends AbstractEntity implements TelEntity {
.....other fields
//Hibernate annotations here....
#JsonDeserialize(using = CountryJsonSupport.Deserializer.class)
private Country targetingCountry;
..... other fields
}
Test Class:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(Application.class)
#WebIntegrationTest({"server.port=0"})
#ActiveProfiles("test")
#DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class BookingAndLinesControllerFunctionalTest {
#Test
public void testGetBooking() {
Booking booking = bookingRepositoryHelper.createBooking();
bookingRepository.save(booking);
String uri = String.format("http://localhost:%s/api/v1/booking-and-lines/" + booking.getBookingCode(), port);
Booking booking1 = restTemplate.getForObject(uri, Booking.class); // line which falls over because countryService is null
}
}
Any ideas?
Managed to discover the answer to this one after fiddling around long enough. Just needed some config like this:
#Configuration
#Profile("test")
public class TestConfig {
#Bean
public HandlerInstantiator handlerInstantiator() {
return new SpringHandlerInstantiator(applicationContext.getAutowireCapableBeanFactory());
}
#Bean
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder(HandlerInstantiator handlerInstantiator) {
Jackson2ObjectMapperBuilder result = new Jackson2ObjectMapperBuilder();
result.handlerInstantiator(handlerInstantiator);
return result;
}
#Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(Jackson2ObjectMapperBuilder objectMapperBuilder) {
return new MappingJackson2HttpMessageConverter(objectMapperBuilder.build());
}
#Bean
public RestTemplate restTemplate(MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter) {
List<HttpMessageConverter<?>> messageConverterList = new ArrayList<>();
messageConverterList.add(mappingJackson2HttpMessageConverter);
return new RestTemplate(messageConverterList);
}
}

Categories

Resources