How to inject a typed map of beans based on a typesafe qualifier in Spring? - spring

See the example below, I'm trying to get a Map of my TypedService beans but I would prefer if the keys were the Type enum values specified in the TypeSafeQualifier instead of the unsafe String "serviceName".
package org.test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Service;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.Map;
import static org.test.Application.Type.ONE;
import static org.test.Application.Type.TWO;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
#SpringBootApplication
public class Application {
#Autowired
Map<String, TypedService> works;
#Autowired
Map<Type, TypedService> fails;
public static void main(String [] args) {
SpringApplication.run(Application.class, args);
}
public enum Type {
ONE,
TWO
}
#Target({TYPE, METHOD, FIELD, CONSTRUCTOR})
#Retention(RUNTIME)
#Qualifier
public #interface TypeSafeQualifier {
Type value();
}
public interface TypedService {
void startSignup();
void activate();
}
#Service
#TypeSafeQualifier(ONE)
public class TypeOneService implements TypedService {
#Override
public void startSignup() {
}
#Override
public void activate() {
}
}
#Service
#TypeSafeQualifier(TWO)
public class TypeTwoService implements TypedService {
#Override
public void startSignup() {
}
#Override
public void activate() {
}
}
}
SpringBoot version: springBootVersion=1.5.3.RELEASE

Spring offers a special approach to handle this type of injection: AutowireCandidateResolver.
In your case the code might be:
package org.test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.LinkedHashMap;
import java.util.Map;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
#SpringBootApplication
public class Application {
#Autowired
Map<String, TypedService> works;
#Autowired
Map<Type, TypedService> fails;
#PostConstruct
private void init() {
System.out.println(fails);
}
public static void main(String[] args) {
final SpringApplication application = new SpringApplication(Application.class);
application.addInitializers(context -> {
context.addBeanFactoryPostProcessor(beanFactory -> {
final DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory;
dlbf.setAutowireCandidateResolver(new MyAutowireCandidateResolver(dlbf));
});
});
application.run(args);
}
#QualifierValue(TypeSafeQualifier.class)
public enum Type {
ONE,
TWO
}
#Target({TYPE, METHOD, FIELD, CONSTRUCTOR})
#Retention(RUNTIME)
#Qualifier
public #interface TypeSafeQualifier {
Type value();
}
public interface TypedService {
void startSignup();
void activate();
}
#Service
#TypeSafeQualifier(Type.ONE)
public class TypeOneService implements TypedService {
#Override
public void startSignup() {
}
#Override
public void activate() {
}
}
#Target({TYPE})
#Retention(RUNTIME)
public #interface QualifierValue {
Class<? extends Annotation> value();
}
#Service
#TypeSafeQualifier(Type.TWO)
public class TypeTwoService implements TypedService {
#Override
public void startSignup() {
}
#Override
public void activate() {
}
}
private static class MyAutowireCandidateResolver extends ContextAnnotationAutowireCandidateResolver {
private final DefaultListableBeanFactory beanFactory;
private MyAutowireCandidateResolver(DefaultListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
#Override
public Object getSuggestedValue(DependencyDescriptor descriptor) {
final Object result = super.getSuggestedValue(descriptor);
if (result != null) {
return result;
}
if (descriptor.getDependencyType() != Map.class) {
return null;
}
final ResolvableType dependencyGenericType = descriptor.getResolvableType().asMap();
final ResolvableType[] typeParams = dependencyGenericType.getGenerics();
final QualifierValue qualifierValue = typeParams[0].getRawClass().getAnnotation(QualifierValue.class);
if (qualifierValue == null) {
return null;
}
final String[] candidateBeanNames = beanFactory.getBeanNamesForType(typeParams[1]);
final LinkedHashMap<Object, Object> injectedMap = new LinkedHashMap<>(candidateBeanNames.length);
for (final String candidateBeanName : candidateBeanNames) {
final Annotation annotation = beanFactory.findAnnotationOnBean(candidateBeanName, qualifierValue.value());
if (annotation == null) {
continue;
}
final Map<String, Object> annotationAttributes = AnnotationUtils.getAnnotationAttributes(annotation, false);
final Object value = annotationAttributes.get("value");
if (value == null || value.getClass() != typeParams[0].getRawClass()) {
continue;
}
injectedMap.put(value, beanFactory.getBean(candidateBeanName));
}
return injectedMap;
}
}
}
First of all, we add TypeQualifierValue annotation to make Spring know about a qualifier with values of the given type.
The second is to customize the SpringApplication in the main method: we use BeanFactoryPostProcessor to set a custom AutowireCandidateResolver.
And the final step: we write MyAutowireCandidateResolver extending ContextAnnotationAutowireCandidateResolver (delegation instead of inheritance is applicable to, it's even a little bit better since one day Spring can migrate to `YetAnotherAutowireCandidateResolver' by default).
The crucial part here is the overridden getSuggestedValue method: here we can customize the injection logic considering the generic types of the dependency (field, method parameter) and by applying some getBean...-like methods from the BeanFactory with some magic of Spring AnnotationUtils class.

Related

In Spring Boot Test, how do I map a temporary folder to a configuration property?

I want to do a self-cleaning test
In my situation, I have one of the components depend on a directory
public class FileRepositoryManagerImpl implements ....
#Value("${acme.fileRepository.basePath}")
private File basePath;
}
The value is defined in the application.yml file, and in DEV it points to a directory under build.
This is not the worst idea, because gradle clean will eventually clean up the mess the tests create.
But, really, what I would like to achieve here, is to make sure that every test runs in an isolated temporary directory that is cleaned up after execution.
I know that JUnit has a tool for the temporary directories. But once I have defined that directory in the scope of JUnit 4, how do I tell Spring to use that temporary directory?
I tried the inner class unsuccessfully:
#RunWith(SpringRunner.class)
#SpringBootTest(classes = { SecurityBeanOverrideConfiguration.class, App.class })
#EnableConfigurationProperties
public abstract class AbstractFileRepositoryManagerIntTests {
private final static TemporaryFolder temporaryFolder = new TemporaryFolder();
#ClassRule
public static TemporaryFolder getTemporaryFolder()
{
return temporaryFolder;
}
#ConfigurationProperties(prefix = "acme")
static class Configuration
{
public FileRepository getFileRepository()
{
return new FileRepository();
}
static class FileRepository
{
public File basePath() throws Exception
{
return temporaryFolder.newFolder("fileRepositoryBaseDir");
}
}
}
}
I was thinking about tinkering with the Environment, but what should be the correct way to inject properties programmatically in a Spring Boot test?
I can think of at least four different approaches to your problem. All with their own advantages and disadvantages.
Approach 1: ReflectionTestUtils
You are using #Value annotation on a private instance property (please, don't to that anymore!). Hence, you can not change acme.fileRepository.basePath on the fly without reflection.
package demo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;
import java.io.File;
#SpringBootApplication
public class FileRepositoryApp {
public static void main(String[] args) {
SpringApplication.run(FileRepositoryApp.class, args);
}
#Component
public class FileRepository {
#Value("${acme.fileRepository.basePath}")
private File basePath;
public File getBasePath() {
return basePath;
}
}
}
Changing basePath after each test with ReflectionTestUtils.setField. Because we are using Spring's TestExecutionListener, that gets initialized before Junit rules are initialized, we are forced to manage the temporary folder in beforeTestExecution and afterTestMethod.
package demo;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.util.ReflectionTestUtils;
import java.io.IOException;
import static junit.framework.TestCase.assertEquals;
import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS;
#RunWith(SpringRunner.class)
#SpringBootTest(classes = FileRepositoryApp.class)
#TestExecutionListeners(listeners = FileRepositoryAppTest.SetBasePath.class, mergeMode = MERGE_WITH_DEFAULTS)
public class FileRepositoryAppTest {
private static TemporaryFolder temporaryFolder = new TemporaryFolder();
#Autowired
private FileRepositoryApp.FileRepository fileRepository;
#Test
public void method() {
System.out.println(temporaryFolder.getRoot().getAbsolutePath());
System.out.println(fileRepository.getBasePath());
assertEquals(temporaryFolder.getRoot(), fileRepository.getBasePath());
}
#Test
public void method1() {
System.out.println(temporaryFolder.getRoot().getAbsolutePath());
System.out.println(fileRepository.getBasePath());
assertEquals(temporaryFolder.getRoot(), fileRepository.getBasePath());
}
static class SetBasePath implements TestExecutionListener {
#Override
public void beforeTestExecution(TestContext testContext) throws IOException {
temporaryFolder.create();
if (testContext.hasApplicationContext()) {
FileRepositoryApp.FileRepository bean = testContext.getApplicationContext().getBean(FileRepositoryApp.FileRepository.class);
ReflectionTestUtils.setField(bean, "basePath", temporaryFolder.getRoot());
}
}
#Override
public void afterTestMethod(TestContext testContext) {
temporaryFolder.delete();
}
}
}
Approach 2: Configuration properties
Introduce a configuration properties class for your application configuration. It gives you type safety for free and we don't rely on reflection anymore.
package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.io.File;
#SpringBootApplication
public class FileRepositoryWithPropertiesApp {
public static void main(String[] args) {
SpringApplication.run(FileRepositoryWithPropertiesApp.class, args);
}
#Component
public class FileRepository {
private final FileRepositoryProperties fileRepositoryProperties;
public FileRepository(FileRepositoryProperties fileRepositoryProperties) {
this.fileRepositoryProperties = fileRepositoryProperties;
}
public File getBasePath() {
return fileRepositoryProperties.getBasePath();
}
}
#Component
#ConfigurationProperties(prefix = "acme.file-repository")
public class FileRepositoryProperties {
private File basePath;
public File getBasePath() {
return basePath;
}
public void setBasePath(File basePath) {
this.basePath = basePath;
}
}
}
Because we are using Spring's TestExecutionListener, that gets initialized before Junit rules are initialized, we are forced to manage the temporary folder in beforeTestExecution and afterTestMethod.
package demo;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.IOException;
import static junit.framework.TestCase.assertEquals;
import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS;
#RunWith(SpringRunner.class)
#SpringBootTest(classes = FileRepositoryWithPropertiesApp.class)
#TestExecutionListeners(listeners = FileRepositoryWithPropertiesTest.SetBasePath.class, mergeMode = MERGE_WITH_DEFAULTS)
public class FileRepositoryWithPropertiesTest {
private static TemporaryFolder temporaryFolder = new TemporaryFolder();
#Autowired
private FileRepositoryWithPropertiesApp.FileRepository bean;
#Test
public void method() {
System.out.println(temporaryFolder.getRoot().getAbsolutePath());
System.out.println(bean.getBasePath());
assertEquals(temporaryFolder.getRoot(), bean.getBasePath());
}
#Test
public void method1() {
System.out.println(temporaryFolder.getRoot().getAbsolutePath());
System.out.println(bean.getBasePath());
assertEquals(temporaryFolder.getRoot(), bean.getBasePath());
}
static class SetBasePath implements TestExecutionListener {
#Override
public void beforeTestExecution(TestContext testContext) throws IOException {
temporaryFolder.create();
if (testContext.hasApplicationContext()) {
FileRepositoryWithPropertiesApp.FileRepositoryProperties bean = testContext.getApplicationContext().getBean(FileRepositoryWithPropertiesApp.FileRepositoryProperties.class);
bean.setBasePath(temporaryFolder.getRoot());
}
}
#Override
public void afterTestMethod(TestContext testContext) {
temporaryFolder.delete();
}
}
}
Approach 3: Refactor your code (my favorite)
Extract basePath into its own class and hide it behind an api. Now you don't need to poke with your application properties and a temporary folder anymore.
package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.io.File;
#SpringBootApplication
public class FileRepositoryWithAbstractionApp {
public static void main(String[] args) {
SpringApplication.run(FileRepositoryWithAbstractionApp.class, args);
}
#Component
public class FileRepository {
private final FileRepositorySource fileRepositorySource;
public FileRepository(FileRepositorySource fileRepositorySource) {
this.fileRepositorySource = fileRepositorySource;
}
public File getBasePath() {
return fileRepositorySource.getBasePath();
}
}
#Component
public class FileRepositorySource {
private final FileRepositoryProperties fileRepositoryProperties;
public FileRepositorySource(FileRepositoryProperties fileRepositoryProperties) {
this.fileRepositoryProperties = fileRepositoryProperties;
}
// TODO for the sake of brevity no real api here
public File getBasePath() {
return fileRepositoryProperties.getBasePath();
}
}
#Component
#ConfigurationProperties(prefix = "acme.file-repository")
public class FileRepositoryProperties {
private File basePath;
public File getBasePath() {
return basePath;
}
public void setBasePath(File basePath) {
this.basePath = basePath;
}
}
}
We don't need any additional testing facility anymore and we can use #Rule on TemporaryFolder instead.
package demo;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import static junit.framework.TestCase.assertEquals;
import static org.mockito.Mockito.when;
#RunWith(SpringRunner.class)
#SpringBootTest(classes = FileRepositoryWithAbstractionApp.class)
public class FileRepositoryWithAbstractionTest {
#Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
#MockBean
private FileRepositoryWithAbstractionApp.FileRepositorySource fileRepositorySource;
#Autowired
private FileRepositoryWithAbstractionApp.FileRepository bean;
#Before
public void setUp() {
when(fileRepositorySource.getBasePath()).thenReturn(temporaryFolder.getRoot());
}
#Test
public void method() {
System.out.println(temporaryFolder.getRoot().getAbsolutePath());
System.out.println(bean.getBasePath());
assertEquals(temporaryFolder.getRoot(), bean.getBasePath());
}
#Test
public void method1() {
System.out.println(temporaryFolder.getRoot().getAbsolutePath());
System.out.println(bean.getBasePath());
assertEquals(temporaryFolder.getRoot(), bean.getBasePath());
}
}
Approach 4: TestPropertySource
Use Spring's TestPropertySource annotation to override properties in a test selectively. Because a Java anntotation can not have a dynamic value, you need to decide beforehand where you want to create your directory and keep in mind your test is bound to a specific operating system due to the used os path separator.
package demo;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static demo.FileRepositoryTestPropertySourceTest.BASE_PATH;
#RunWith(SpringRunner.class)
#SpringBootTest(classes = FileRepositoryApp.class)
#TestPropertySource(properties = "acme.fileRepository.basePath=" + BASE_PATH)
public class FileRepositoryTestPropertySourceTest {
static final String BASE_PATH = "/tmp/junit-base-path";
private Path basePath = Paths.get(BASE_PATH);;
#Autowired
private FileRepositoryApp.FileRepository fileRepository;
#Before
public void setUp() throws IOException {
Files.deleteIfExists(basePath);
Files.createDirectories(basePath);
}
#After
public void after() throws IOException {
Files.deleteIfExists(basePath);
}
#Test
public void method() {
System.out.println(fileRepository.getBasePath());
}
}
If you use JUnit 5.4+ then you can leverage their #TempDir that works just fine without manual lifecycle management of the directory. That is you don't need to create and delete it manually, in contrast to #TemporaryFolder from JUnit 4.
Here is a working example of how you can achieve your goal:
//Your bean into which you want to inject the property
#Component
public class FileRepositoryManager {
#Value("${acme.fileRepository.basePath}")
private File basePath;
public File getBasePath() {
return basePath;
}
}
//Test that uses ApplicationContextInitializer machinery to set the desired properties
#SpringBootTest
#ContextConfiguration(initializers = Initializer.class)
class FileRepositoryManagerTest {
#TempDir
static File tempDir;
#Autowired
FileRepositoryManager fileRepositoryManager;
#Test
void basePathIsSet() {
assertNotNull(fileRepositoryManager.getBasePath());
}
static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void initialize(ConfigurableApplicationContext context) {
TestPropertyValues.of(
"acme.fileRepository.basePath=" + tempDir
).applyTo(context);
}
}
}

How to inject a properties in a class with implements ImportBeanDefinitionRegistrar?

I wanna use the properties to set some swagger docket to spring but I cant get the properties when I implements ImportBeanDefinitionRegistrar and get an error
Caused by: java.lang.NoSuchMethodException:
com.github.sofior.swagger.SwaggerAutoConfiguration.<init>()
#Configuration
#EnableSwagger2
#EnableConfigurationProperties(SwaggerProperties.class)
public class SwaggerAutoConfiguration implements ImportBeanDefinitionRegistrar {
private final SwaggerProperties properties;
public SwaggerAutoConfiguration(SwaggerProperties properties) {
this.properties = properties;
}
#Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
System.out.println(properties);
properties.getDockets().forEach((docketName, docketProperties) -> {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Docket.class);
builder.addConstructorArgValue(docketProperties.getType());
builder.addConstructorArgValue(docketProperties.getType());
registry.registerBeanDefinition(docketName, builder.getRawBeanDefinition());
});
}
}
I think it is impossible to do this, because spring have two phase
1.bean registration
2.bean initialization and instantiation
SwaggerProperties can only be used after phase 2 when it is finished to instantiate, but registerBeanDefinitions is the phase 1
Basically you need to inject the properties to your class constructor.
So the configurations should be Autowired in order to work them.
#Autowired
public SwaggerAutoConfiguration(SwaggerProperties properties) {
this.properties = properties;
}
This should fix your "properties" is null issue.
the workaround of this question is to read a new properties during registerBeanDefinitions
EnableCustomSwagger
import org.springframework.context.annotation.Import;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Import(SwaggerAutoConfiguration.class)
public #interface EnableCustomSwagger {
String path() default "";
}
SwaggerAutoConfiguration
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
public class SwaggerAutoConfiguration implements ImportBeanDefinitionRegistrar {
#Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
String clsName = EnableCustomSwagger.class.getName();
AnnotationAttributes attrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(clsName, false));
if (!attrs.getString("path").equals("")) {
String path = attrs.getString("path");
ResourceLoader loader = new DefaultResourceLoader();
Resource resource = loader.getResource(path);
// you can get the value from your property files
}
//how can I get properties here,the properties is null
// properties.getDockets().forEach((docketName, docketProperties) -> {
// BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Docket.class);
// builder.addConstructorArgValue(docketProperties.getType());
// builder.addConstructorArgValue(docketProperties.getType());
// registry.registerBeanDefinition(docketName, builder.getRawBeanDefinition());
// });
}
}
Application
#SpringBootApplication
#EnableCustomSwagger(path="classpath:docklet.properties")
public class Application {
}
spring 2.x
import org.springframework.boot.context.properties.bind.Binder;
public class MultipleDataSourceComponentRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
...
private Environment environment;
#Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
#Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
ConfigurationProperties annotationCp = MultipleDataSourceSetProperties.class.getAnnotation(ConfigurationProperties.class);
MultipleDataSourceSetProperties properties = Binder.get(environment).bind(annotationCp.prefix(), MultipleDataSourceSetProperties.class).get();
}
...

SFTP : BeanPostProcessor interfere with #ServiceActivator and #MessagingGateway

It seems BeanPostProcessor interface implementation is having impact on #ServiceActivator. What should be the way to use BeanPostProcessor with #ServiceActivator. Thanks.
Complete logs are available here logs
Following is Java Config used for SFTP -
package com.ftp.example;
import java.io.File;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.file.FileNameGenerator;
import org.springframework.integration.file.remote.session.CachingSessionFactory;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.sftp.outbound.SftpMessageHandler;
import org.springframework.integration.sftp.session.DefaultSftpSessionFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import com.jcraft.jsch.ChannelSftp.LsEntry;
#Configuration
#EnableScheduling
#EnableAspectJAutoProxy
#EnableAsync
#IntegrationComponentScan
#EnableIntegration
#EnableBatchProcessing
#PropertySource("file:C:\\DEV\\workspace_oxygen\\ftp-example\\ftp-example.properties")
public class DependencySpringConfiguration {
private Logger LOG = LoggerFactory.getLogger(DependencySpringConfiguration.class);
#Value("${project.name}")
private String applicationName;
#Value("${${project.name}.ftp.server}")
private String server;
#Value("${${project.name}.ftp.port}")
int port;
#Value("${${project.name}.ftp.username}")
private String username;
#Value("${${project.name}.ftp.password}")
private String password;
#Value("${${project.name}.ftp.remote.directory}")
private String remoteDirectory;
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
#Bean
public ProcessStarter processStarter() {
return new ProcessStarter();
}
/* #Bean
public LogInjector logInjector() {
return new LogInjector();
}*/
#Bean
public FTPOutService fTPOutService() {
return new FTPOutService();
}
#Bean
public SessionFactory<LsEntry> sftpSessionFactory() {
DefaultSftpSessionFactory sf = new DefaultSftpSessionFactory();
sf.setHost(server);
sf.setPort(port);
sf.setUser(username);
sf.setPassword(password);
sf.setAllowUnknownKeys(true);
return new CachingSessionFactory<LsEntry>(sf);
}
#Bean
#ServiceActivator(inputChannel = "toSftpChannel")
public MessageHandler handler() {
SftpMessageHandler handler = new SftpMessageHandler(sftpSessionFactory());
handler.setRemoteDirectoryExpression(new LiteralExpression(remoteDirectory));
handler.setFileNameGenerator(new FileNameGenerator() {
#Override
public String generateFileName(Message<?> message) {
return "fileNameToBeFtp.txt";
}
});
return handler;
}
#MessagingGateway
public interface MyGateway {
#Gateway(requestChannel = "toSftpChannel")
void sendToSftp(File file);
}
}
And We are calling gateway object like this while doing SFTP
Main class
public class FtpExample {
public static String[] ARGS;
private static final Logger LOG = LoggerFactory.getLogger(FtpExample.class);
public static void main(String[] args) throws Exception {
ARGS = args;
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(DependencySpringConfiguration.class);
ProcessStarter processStarter = ctx.getBean(ProcessStarter.class);
processStarter.startService();
}
}
Other classes -
public class ProcessStarter {
#Inject
private FTPOutService ftpOutService;
public void startService() {
ftpOutService.ftpToBbg();
}
}
public class FTPOutService {
private static Logger log = LoggerFactory.getLogger(FTPOutService.class);
#Inject
private ApplicationContext appContext;
public void ftpToBbg() {
log.info("Starting FTP out process...");
File file = null;
try {
file = new File("C:\\Temp\\log\\debug\\ftp\\priceindex\\for-upload\\ftp-example.txt.REQ");
MyGateway gateway = appContext.getBean(MyGateway.class);
gateway.sendToSftp(file);
log.info("File {} written successfully on remote server", file);
} catch (Exception e) {
log.error("Error while uploading file {}", file, e);
}
}
}
Above code is working fine unless I am not adding following bean declaration in above defined Java Config -
public LogInjector logInjector() {
return new LogInjector();
}
Above bean definition is having following implementation -
public class LogInjector implements BeanPostProcessor {
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
#Override
public Object postProcessBeforeInitialization(final Object bean, String beanName) throws BeansException {
ReflectionUtils.doWithFields(bean.getClass(), new FieldCallback() {
#Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
// make the field accessible if defined private
ReflectionUtils.makeAccessible(field);
if (field.getAnnotation(Log.class) != null) {
if (org.slf4j.Logger.class == field.getType()) {
org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(bean.getClass());
field.set(bean, log);
} else if (java.util.logging.Logger.class == field.getType()) {
java.util.logging.Logger log = java.util.logging.Logger.getLogger(bean.getClass().toString());
field.set(bean, log);
}
}
}
});
return bean;
}
}
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
#Documented
public #interface Log {
}
Once any BeanPostProcessor implementation is added in Java Config, it creates problem and application not able to see toSftpChannel -
org.springframework.beans.factory.NoSuchBeanDefinitionException: No
bean named 'toSftpChannel' available at
org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:685)
at
org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1199)
at
org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:284)
at
org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at
org.springframework.integration.support.channel.BeanFactoryChannelResolver.resolveDestination(BeanFactoryChannelResolver.java:88)
at
org.springframework.integration.support.channel.BeanFactoryChannelResolver.resolveDestination(BeanFactoryChannelResolver.java:45)
at
org.springframework.integration.gateway.MessagingGatewaySupport.getRequestChannel(MessagingGatewaySupport.java:327)
at
org.springframework.integration.gateway.MessagingGatewaySupport.send(MessagingGatewaySupport.java:368)
at
org.springframework.integration.gateway.GatewayProxyFactoryBean.invokeGatewayMethod(GatewayProxyFactoryBean.java:477)
at
org.springframework.integration.gateway.GatewayProxyFactoryBean.doInvoke(GatewayProxyFactoryBean.java:429)
at
org.springframework.integration.gateway.GatewayProxyFactoryBean.invoke(GatewayProxyFactoryBean.java:420)
at
org.springframework.integration.gateway.GatewayCompletableFutureProxyFactoryBean.invoke(GatewayCompletableFutureProxyFactoryBean.java:65)
at
org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at
org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
at com.sun.proxy.$Proxy57.sendToSftp(Unknown Source)
Looks what you have:
#Bean
public LogInjector logInjector() {
return new LogInjector();
}
If you declare BeanPostProcessors as #Bean you have to specify them with the static modifier: https://docs.spring.io/spring/docs/5.0.0.RELEASE/spring-framework-reference/core.html#beans-factorybeans-annotations
You may declare #Bean methods as static, allowing for them to be called without creating their containing configuration class as an instance. This makes particular sense when defining post-processor beans, e.g. of type BeanFactoryPostProcessor or BeanPostProcessor, since such beans will get initialized early in the container lifecycle and should avoid triggering other parts of the configuration at that point.

How to set jersey in spring boot 1.4.1-RELEASE to convert dates to ISO 8601?

In Spring boot 1.3.6-RELEASE I had the below class registered to jersey. Every java.util.Date field would be read and returned as ISO8601 format. However, when updating to 1.4.1-RELEASE it now sometimes works and sometimes doesn't. What's the new proper way to enable this?
package com.mypackage;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Date;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.ext.Provider;
import com.fasterxml.jackson.databind.util.ISO8601Utils;
#Provider
public class DateTimeParamConverterProvider implements ParamConverterProvider {
#SuppressWarnings("unchecked")
#Override
public <T> ParamConverter<T> getConverter(Class<T> clazz, Type type, Annotation[] annotations) {
if (type.equals(Date.class)) {
return (ParamConverter<T>) new DateTimeParamConverter();
} else {
return null;
}
}
static class DateTimeParamConverter implements ParamConverter<Date> {
#Override
public java.util.Date fromString(String value) {
if (value == null) {
return null;
}
try {
return ISO8601Utils.parse(value, new ParsePosition(0));
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
#Override
public String toString(Date value) {
return ISO8601Utils.format(value);
}
}
}
I register this provider like this:
#Component
#ApplicationPath("/")
public class JerseyConfiguration extends ResourceConfig {
private static final Logger log = Logger.getLogger(JerseyConfiguration.class.getName());
#Autowired
public JerseyConfiguration(LogRequestFilter lrf) {
register(new ObjectMapperContextResolverNonNull());
register(RestServiceImpl.class);
property(ServletProperties.FILTER_FORWARD_ON_404, true);
register(DateTimeParamConverterProvider.class, 6000);
...
Just define this in your application.properties:
spring.jackson.date-format=com.fasterxml.jackson.databind.util.ISO8601DateFormat

Method with #JmsListener not executed when setting custom message converter

Hy,
Having these three classes I have found a problem:
package xpadro.spring.jms;
import javax.jms.ConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.support.converter.MarshallingMessageConverter;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
#SpringBootApplication
public class JmsJavaconfigApplication {
public static void main(String[] args) {
SpringApplication.run(JmsJavaconfigApplication.class, args);
}
#Bean
public Jaxb2Marshaller jaxb2Marshaller(){
Jaxb2Marshaller jaxb = new Jaxb2Marshaller();
Class array[] = new Class[1];
array[0]= xpadro.spring.jms.model.Order.class;
jaxb.setClassesToBeBound(array);
return jaxb;
}
#Bean
#Autowired
public MarshallingMessageConverter marshallingMessageConverter( Jaxb2Marshaller marshaller){
MarshallingMessageConverter mc = new MarshallingMessageConverter();
mc.setMarshaller(marshaller);
mc.setUnmarshaller(marshaller);
return mc;
}
#Bean
#Autowired
public JmsTemplate init(MarshallingMessageConverter messageConverter,ConnectionFactory connectionFactory){
JmsTemplate template = new JmsTemplate();
template.setMessageConverter(messageConverter);
template.setConnectionFactory(connectionFactory);
return template;
}
}
Next, the model:
package xpadro.spring.jms.model;
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
#XmlRootElement(name = "order")
#XmlAccessorType(XmlAccessType.FIELD)
public class Order implements Serializable {
private static final long serialVersionUID = -797586847427389162L;
#XmlElement(required = true)
private String id="";
public Order() {
}
public Order(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
Next, the listener:
package xpadro.spring.jms.listener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
import xpadro.spring.jms.model.Order;
import xpadro.spring.jms.service.StoreService;
#Component
public class SimpleListener {
private final StoreService storeService;
#Autowired
public SimpleListener(StoreService storeService) {
this.storeService = storeService;
}
#JmsListener(destination = "simple.queue")
public void receiveOrder(Order order) {
storeService.registerOrder(order);
}
}
The class that sends messages:
package xpadro.spring.jms.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;
import xpadro.spring.jms.model.Order;
#Service
public class ClientServiceImpl implements ClientService {
private static final String SIMPLE_QUEUE = "simple.queue";
private static final String IN_QUEUE = "in.queue";
private final JmsTemplate jmsTemplate;
#Autowired
public ClientServiceImpl(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
#Override
public void addOrder(Order order) {
//MessageRegistered with a bean
jmsTemplate.convertAndSend(SIMPLE_QUEUE, order);
}
#Override
public void registerOrder(Order order) {
//MessageRegistered with a bean
jmsTemplate.convertAndSend(IN_QUEUE, order);
}
}
The point is that listener(method receiveOrder()) is not executed when I set a custom message converter in the class JmsJavaconfigApplication. When I dont set it, spring boot sets a SimpleMessageConverter and the listener is executed.
Any wrong or missed configuration?
Thanks
You need to provide an marshalling converter to the #JmsListener to unmarshall the message, by configuring the listener container factory see the documentation.

Resources