Caught up in a weird requirement. I need to attach unique error id to log4j message and return that message id back to interface.So, I though lets create a spring service, like this
public class LoggingService {
protected static Logger logger = LoggerFactory.getLogger(LoggingService.class);
public String debug(String debug_msg)
{
String uniqueMsgId = generateUniqueId();
logger.debug(concatIdWithMsg(uniqueMsgId, debug_msg));
return uniqueMsgId;
}
}
and autowired this to wherever i need it.
public class LoginLogoutController {
#Autowired
LoggingService logger;
#RequestMapping(value = "/login", method = RequestMethod.GET)
public String getLoginPage()
{
logger.debug("Login page requested");
}
}
Although it worked fine, but the source class in logger msg is LoggingService which is obvious. What i want is to pass the class in which LoggingService is autowired so that the logger message shows the original source of problem. I tried somehow to change the service
but got no further idea how to pass source class
public class LoggingService<T> {
protected static Logger logger = null;
Class<T> sourceClass;
public void construct(Class<T> sourceClass)
{
this.sourceClass = sourceClass;
logger = LoggerFactory.getLogger(sourceClass);
}
public String debug(String debug_msg)
{
String uniqueMsgId = generateUniqueId();
logger.debug(concatIdWithMsg(uniqueMsgId, debug_msg));
return null;
}
}
I used this mechanism to inject a logger.
Create this annotation..
/**
* Indicates InjectLogger of appropriate type to
* be supplied at runtime to the annotated field.
*
* The injected logger is an appropriate implementation
* of org.slf4j.Logger.
*/
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
#Retention(RUNTIME)
#Target(FIELD)
#Documented
public #interface InjectLogger {
}
Now lets define a class that actually does the job of injecting the logger implementation.
/**
* Auto injects the underlying implementation of logger into the bean with field
* having annotation <code>InjectLogger</code>.
*
*/
import java.lang.reflect.Field;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.util.ReflectionUtils;
import static org.springframework.util.ReflectionUtils.FieldCallback;
public class LoggerInjector implements BeanPostProcessor {
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
public Object postProcessBeforeInitialization(final Object bean,
String beanName) throws BeansException {
ReflectionUtils.doWithFields(bean.getClass(), new FieldCallback() {
public void doWith(Field field) throws IllegalArgumentException,
IllegalAccessException {
// make the field accessible if defined private
ReflectionUtils.makeAccessible(field);
if (field.getAnnotation(InjectLogger.class) != null) {
Logger log = LoggerFactory.getLogger(bean.getClass());
field.set(bean, log);
}
}
});
return bean;
}
}
Using it is even simpler. Just add the Logger annotation created above to the Log field in the required class.
import org.slf4j.Logger;
public class Demo {
#InjectLogger
private Logger log;
public void doSomething() {
log.info("message");
log.error("Lets see how the error message looks...");
}
}
Why dont you use Spring AOP. AOP provides you much accessibility and features, and you can exploit its interesting features later also, when your application becomes heavy. Spring AOP
Related
I am upgrading old Java EE application to Spring based solution. In the old applications there were custom annotations to create proxy inject proxy bean and intercept the method invocation [Interceptor classes implements MethodInterceptor) or (implements InvocationHandler), which used to perform some before and after execution stuff.
We have replaced those custom annotations with Spring marker interfaces like #Service, #Repository etc. and we are able to use #Autowire the bean instances. Now my question is how to intercept these autowired beans to perform per and post execution activities. One solution I can think is to use Spring AOP and use #Around pointcut. Just want to know is there any other and better alternative which can be used like
extending org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
Using BeanFactoryPostProcessor or BeanPostProcessor.
Using InstantiationAwareBeanPostProcessor
I have used this alternative instead of AOP. I have used Spring's bean pre & post processor call back. Below is the code snippet.
Application Context Provider, to get Spring beans statically
package com.appname.config;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* #author dpoddar
*
*/
#Component("applicationContextProvider")
public class ApplicationContextProvider implements ApplicationContextAware{
private static ApplicationContext ctx = null;
public static ApplicationContext getApplicationContext() {
return ctx;
}
#Override
#Autowired
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
ApplicationContextProvider.ctx = ctx;
}
/**
* Returns the Spring managed bean instance of the given class type if it exists.
* Returns null otherwise.
* #param beanClass
* #return
*/
public static <T extends Object> T getBean(Class<T> beanClass) {
return ctx.getBean(beanClass);
}
/**
* Returns the Spring managed bean instance of the given class type if it exists.
*
* #param <T>
* #param name
* #param beanClass
* #return
*/
public static <T extends Object> T getBean(String name,Class<T> beanClass) {
return ctx.getBean(name,beanClass);
}
}
Spring Bean Post Processor, InstantiationAwareBeanPostProcessor adds before and after initialization call backs
package com.appname.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.SpringProxy;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import com.appname.core.ExecutionContext;
import com.appname.core.di.FacadeService;
import com.appname.interceptors.BusinesServiceInterceptor;
import com.appname.interceptors.FacadeServiceInterceptor;
import com.appname.interceptors.RepositoryInterceptor;
import net.sf.cglib.proxy.Enhancer;
/**
* #author dpoddar
*
*/
#Component
public class AppSpringBeanPostProcessor extends AutowiredAnnotationBeanPostProcessor implements InstantiationAwareBeanPostProcessor {
private static Logger logger = LoggerFactory.getLogger(AppSpringBeanPostProcessor.class);
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
Class<?> clazz = bean.getClass();
AutowireCapableBeanFactory factory = ApplicationContextProvider.getApplicationContext().getAutowireCapableBeanFactory();
if(clazz.isAnnotationPresent(FacadeService.class)) {
//This is to instatiate InvocationHandler classes
//return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), new FacadeServiceInterceptor(bean));
FacadeServiceInterceptor interceptor = new FacadeServiceInterceptor();
Enhancer e = new Enhancer();
e.setSuperclass(clazz);
e.setInterfaces(new Class[]{SpringProxy.class}); /// Identification Spring-generated proxies
e.setCallback(interceptor);
Object o = e.create();
factory.autowireBean( o ); //Autowire Bean dependecies to the newly created object
return o;
}else if(clazz.isAnnotationPresent(Service.class)) {
BusinesServiceInterceptor interceptor = new BusinesServiceInterceptor();
Enhancer e = new Enhancer();
e.setSuperclass(clazz);
e.setInterfaces(new Class[]{SpringProxy.class});
e.setCallback(interceptor);
Object o = e.create();
factory.autowireBean( o );
return o;
}else if(clazz.isAnnotationPresent(Repository.class)) {
ExecutionContext.newInstance();
RepositoryInterceptor interceptor = new RepositoryInterceptor();
Enhancer e = new Enhancer();
e.setSuperclass(clazz);
e.setInterfaces(new Class[]{SpringProxy.class});
e.setCallback(interceptor);
return e.create();
}else {
return bean;
}
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
How can I access properties loaded by <context:property-placeholder> in BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry.
I am unable to use fields annotated with #Value, as they do not seem to be initialized (their values are null).
Setting the value of fields annotated with #Value happens only after the post-processing of the BeanDefinitionRegistry, meaning they are not usable at this stage of the initialization process.
You can however explicitly scan the configuration environment and read the relevant properties' values from there, then use them in your dynamic bean definitions.
To gain access to the configuration environment, you can create your BeanDefinitionRegistryPostProcessor in a method annotated with #Bean, that takes the ConfigurableEnvironment as a parameter.
See the following example:
package com.sample.spring;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.PropertySource;
#Configuration
public class DynamicBeanConfig {
private static final String PROPERTY_KEY = "somename";
#Bean
public BeanDefinitionRegistryPostProcessor beanPostProcessor(ConfigurableEnvironment environment) {
return new PostProcessor(environment);
}
class PostProcessor implements BeanDefinitionRegistryPostProcessor {
private String propertyValue;
/*
* Reads property value from the configuration, then stores it
*/
public PostProcessor(ConfigurableEnvironment environment) {
propertyValue = readProperty(environment);
}
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}
/*
* Creates the bean definition dynamically (using the configuration value), then registers it
*/
#Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(SampleDynamicBean.class);
builder.addPropertyValue("property", propertyValue);
registry.registerBeanDefinition("sampleDynamicBean", builder.getBeanDefinition());
}
/*
* Iterates over all configuration sources, looking for the property value.
* As Spring orders the property sources by relevance, the value of the first
* encountered property with the correct name is read and returned.
*/
private String readProperty(ConfigurableEnvironment environment) {
for (PropertySource<?> source : environment.getPropertySources()) {
if (source instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> propertySource = (EnumerablePropertySource<?>) source;
for (String property : propertySource.getPropertyNames()) {
if (PROPERTY_KEY.equals(property))
{
return (String)propertySource.getProperty(PROPERTY_KEY);
}
}
}
}
throw new IllegalStateException("Unable to determine value of property " + PROPERTY_KEY);
}
}
class SampleDynamicBean {
private String property;
public void setProperty(String property)
{
this.property = property;
}
public String getMessage()
{
return "This message is produced by a dynamic bean, it includes " + property;
}
}
}
The sample code is adapted from this blog post, https://scanningpages.wordpress.com/2017/07/28/spring-dynamic-beans/
I mixed both org.springframework.validation together with JSR-303 annotation.
JSR-303 annotation:
public class Model{
private String type;
private State state;
#NotNull(message = "{comment.notnull}")
private String comment;
}
Spring framework validation:
#Component
public class ModelValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return Model.class.equals(clazz);
}
#Override
public void validate(Object obj, Errors errors) {
Model model = (Model) obj;
if (eventModel.getState() == null) {
errors.reject("state", "error.state.invalidState");
}
}
}
My ValidationMessages_en.properties
error.state.invalidState=Invalid state.
comment.notnull=Comment not null.
Then my configuration:
#Configuration
public class ValidationConfig {
#Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("ValidationMessages");
return messageSource;
}
}
When i run my springboot application, the interpolation works for JSR-303, but not the custom validator, did i miss something? Tried for long time but can't figure out.
Result:
"error_messages": [
{
"error_message": "Comment not null"
},
{
"error_message": "error.state.invalidState"
}
]
I am not sure if there is any easy way but eventually I had to do like this ,
Added below component ,
import java.util.Locale;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.stereotype.Component;
#Component
public class Messages {
#Autowired
private MessageSource messageSource;
private MessageSourceAccessor accessor;
#PostConstruct
private void init() {
accessor = new MessageSourceAccessor(messageSource, Locale.ENGLISH);
}
public String get(String property) {
return accessor.getMessage(property);
}
}
Then I would inject this component in my validator & use above get("error.state.invalidState") method of this class instead of error.state.invalidState directly.
messageSource is the bean that you already defined in system. Locale can be externalized and default locale can be set with another configuration.
MessageSourceAccessor has lots of overloaded methods, you can directly expose that too if wish to provide those options.
I think you have confused between two methods:
reject(String errorCode, String defaultMessage)
rejectValue(String field, String errorCode)
As your configuration seems fine, just doing the following should solve your problem:
errors.rejectValue("state", "error.state.invalidState");
Or
errors.reject("error.state.invalidState", "Some default message to fallback!");
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.
I have spring data mongo custom converters setup via xml as follows
<mongo:mapping-converter id="mongoConverter" db-factory-ref="mongoDbFactory">
<mongo:custom-converters>
<mongo:converter ref="customWriteConverter" />
<mongo:converter ref="customReadConverter" />
</mongo:custom-converters>
</mongo:mapping-converter>
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg ref="mongoDbFactory"/>
<constructor-arg ref="mongoConverter"/>
</bean>
<bean id="customWriteConverter" class="package.WriteConverter" />
<bean id="customReadConverter" class="package.ReadConverter" />
In the custom read/write converter, I would like to re-use spring-data-mongo's default pojo converter to save certain properties as subdocuments.
consider a simplified example -
class A {
B b;
String var1;
int var2;
}
class B {
String var3;
String var4;
}
I want to handle conversion of class A using customWriteConverter and customReadConverter, but in my custom converters I also want to delegate conversion of class B back to spring-data-mongo's default POJO converter.
How can I do this? I have not been able to successfully autowire a MongoConverter or MongoTemplate into the custom converter since the MongoConverter/MongoTemplate bean creation is in progress when it tries to create the custom converter. Is it possible to get access to the default converter and use that from within the custom converter?
This method is used in MongoTemplate class to get a default converter.
private static final MongoConverter getDefaultMongoConverter(MongoDbFactory factory) {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, new MongoMappingContext());
converter.afterPropertiesSet();
return converter;
}
MappingMongoConverter is not final and so can be overridden for a specific purpose. As mentioned in my comment above, look at this question to maybe find out solution to your problem.
If you are converting TO mongo database and want to default some conversions, you could do something like this:
...
#Resource
private ObjectFactory<MappingMongoConverter>
mappingMongoConverterObjectFactory;
private MappingMongoConverter
mappingMongoConverter;
...
//Otherwise, use default MappingMongoConverter
//
if (result == null)
result =
getMappingMongoConverter()
.convertToMongoType(
value
);
...
MappingMongoConverter getMappingMongoConverter() {
if (mappingMongoConverter == null)
mappingMongoConverter =
mappingMongoConverterObjectFactory.getObject();
return
mappingMongoConverter;
}
The MappingMongoConverter cannot be directly #Resource (ed) in my case since it's in the process of being constructed when other converters are being built. So, Spring detects a circular reference. I am not sure if there is a better "lazy" method of doing this without all the run-around of ObjectFactory, getter method, and caching.
Now, if someone can figure out a method of defaulting to standard processing while going back (from DBObject to java Object) that would complete this circle.
This may not be exactly the same use case, but I had to modify existing mongo documents on a lazy basis (without using $project, etc).
Basically, I copied Spring's getDefaultMongoConverter method (which changed since earlier answers here and may change again in the future) and added an argument to pass a custom converter(s). When creating the custom converter itself (FooConverter), I pass in an empty list for the customer converters (this may differ if you have additional converters for sub-documents). Then when creating the final converter I pass in my FooConverter.
Here is some (untested) sample code. This assumes auto-configuration is enabled and thus MongoDbFactory is already wired in. If not, you'll be creating your own MongoDbFactory bean but everything else is pretty much the same.
#Bean
public MongoTemplate mongoTemplate(final MongoDbFactory mongoDbFactory) throws Exception {
FooReadConverter fooConverter = new FooReadConverter(mongoDbFactory);
MongoConverter converter = getMongoConverter(mongoDbFactory, List.of(fooConverter));
return new MongoTemplate(mongoDbFactory, converter);
}
/**
* Get a mongo converter
* #see org.springframework.data.mongodb.core.MongoTemplate#getDefaultMongoConverter
*/
static MongoConverter getMongoConverter(MongoDbFactory factory, List<?> customConverters) {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
MongoCustomConversions conversions = new MongoCustomConversions(customConverters);
MongoMappingContext mappingContext = new MongoMappingContext();
mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
mappingContext.afterPropertiesSet();
MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mappingContext);
converter.setCustomConversions(conversions);
converter.setCodecRegistryProvider(factory);
converter.afterPropertiesSet();
return converter;
}
#ReadingConverter
static class FooReadConverter implements Converter<Document, Foo> {
private final MongoConverter defaultConverter;
public FooReadConverter(MongoDbFactory dbFactory) {
this.defaultConverter = getMongoConverter(dbFactory, List.of());
}
#Override
public Foo convert(Document source) {
boolean isOldFoo = source.containsKey("someKeyOnlyInOldFoo");
Foo foo;
if (isOldFoo) {
OldFoo oldFoo = defaultConverter.read(OldFoo.class, source);
foo = oldFoo.toNewFoo();
} else {
foo = defaultConverter.read(Foo.class, source);
}
return foo;
}
}
Try to inject BeanFactory in your converter and in convert method fetch mongoTemplate from BeanFactory...
I know it's quite late but today I just faced this problem and I thought maybe it's better to share it here.
Since MongoTemplate (and its default converters) are initialized after our custom-converters, it's not possible to inject those directly into our converters, but we can access those by implementing ApplicationContextAware in our converters.
After accessing to mongoTemplate, we can delegate the read/write conversion to it by calling mongoTemplate.getConverter().read and mongoTemplate.getConverter().write methods respectively.
Let's examine an example. Assume we have two POJOs:
public class Outer {
public String var1;
public int var2;
public Inner inner;
}
public class Inner {
public String var3;
public String var4;
}
The WriteConverter could be something like this:
import org.bson.Document;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;
#Component
public class CustomWriteConverter implements Converter<Outer, Document>, ApplicationContextAware {
private ApplicationContext applicationContext;
private MongoTemplate mongoTemplate;
#Override
public Document convert(Outer source) {
// initialize the mongoTemplate
if (mongoTemplate == null) {
this. mongoTemplate = applicationContext.getBean(MongoTemplate.class);
}
// do some custom stuff
Document document = new Document();
document.put("var1", source.var1);
document.put("var2", source.var2);
// Using MongoTemplate's converters
Document inner = new Document();
mongoTemplate.getConverter().write(source.inner, inner);
document.put("inner", inner);
return document;
}
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
And the ReadConverter:
import org.bson.Document;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;
#Component
public class CustomReadConverter implements Converter<Document, Outer>, ApplicationContextAware {
private ApplicationContext applicationContext;
private MongoTemplate mongoTemplate;
#Override
public Outer convert(Document source) {
// initialize the mongoTemplate
if (mongoTemplate == null) {
this. mongoTemplate = applicationContext.getBean(MongoTemplate.class);
}
// do some custom stuff
Outer outer = new Outer();
outer.var1 = source.getString("var1");
outer.var2 = source.getInteger("var2");
// Using MongoTemplate's converters
Inner inner = mongoTemplate.getConverter().read(Inner.class, (Document) source.get("inner"));
outer.inner = inner;
return outer;
}
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
The mongoTemplate could be initialized by multiple threads (because it's in a race condition), but since it has a scope of singleton, there would be no problem.
Now the only thing to do is to register our converters.
Here this working with spring-boot-starter-data-mongodb version 2.5.2
package com.example.mongo;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
#Configuration
public class MongoConfig extends AbstractMongoClientConfiguration {
private #Value("${spring.data.mongodb.database}") String database;
private #Autowired MongoDatabaseFactory mongoDatabaseFactory;
#Override
protected String getDatabaseName() {
return database;
}
#Override
protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDatabaseFactory);
MongoConverter mongoConverter = new MappingMongoConverter(dbRefResolver, new MongoMappingContext());
converterConfigurationAdapter.registerConverters(customConverters(mongoConverter));
}
public List<Converter<?, ?>> customConverters(MongoConverter mongoConverter) {
MyCustomConverter custom = new MyCustomConverter(mongoConverter);
return List.of(custom);
}
}