Spring constructor injection of SLF4J logger - how to get injection target class?
Asked Answered
S

8

20

I'm trying to use Spring to inject a SLF4J logger into a class like so:

@Component
public class Example {

  private final Logger logger;

  @Autowired
  public Example(final Logger logger) {
    this.logger = logger;
  }
}

I've found the FactoryBean class, which I've implemented. But the problem is that I cannot get any information about the injection target:

public class LoggingFactoryBean implements FactoryBean<Logger> {

    @Override
    public Class<?> getObjectType() {
        return Logger.class;
    }  

    @Override
    public boolean isSingleton() {  
        return false;
    }

    @Override
    public Logger getObject() throws Exception {
        return LoggerFactory.getLogger(/* how do I get a hold of the target class (Example.class) here? */);
    }
}   

Is FactoryBean even the right way to go? When using picocontainers factory injection, you get the Type of the target passed in. In guice it is a bit trickier. But how do you accomplish this in Spring?

Shaving answered 14/6, 2010 at 14:46 Comment(3)
Is it really worth it, just to avoid saying ` = LoggerFactory.getLogger()`?Odontology
I don't like the static bind to LoggerFactory, for the same reasons Martin Fowler outlines in martinfowler.com/articles/injection.html. An injected LoggerFactory is an acceptable solution (following the service locator pattern), but a bit verbose. I suppose one could argue that the Log injection needs to use a service locator, since a pure dependency should be target-agnostic. But the locator solution is verbose, other frameworks support it and I would expect Spring to be able to provide some sort of information about the target. I'm just wondering if this is really not possible.Shaving
I mean, this information is passed to BeanPostProcessors: tzavellas.com/techblog/2007/03/31/…. Can the same not be accomplished for constructor injection?Shaving
S
11

I resolved it with a custom BeanFactory. If anyone comes up with a better solution, I would be happy to hear it. Anyway, here's the bean factory:

import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;

public class CustomBeanFactory extends DefaultListableBeanFactory {

    public CustomBeanFactory() {
    }

    public CustomBeanFactory(DefaultListableBeanFactory delegate) {
        super(delegate);
    }

    @Override
    public Object resolveDependency(DependencyDescriptor descriptor,
            String beanName, Set<String> autowiredBeanNames,
            TypeConverter typeConverter) throws BeansException {
        //Assign Logger parameters if required      
        if (descriptor.isRequired()
                && Logger.class.isAssignableFrom(descriptor
                        .getMethodParameter().getParameterType())) {            
            return LoggerFactory.getLogger(descriptor.getMethodParameter()
                    .getDeclaringClass());
        } else {
            return super.resolveDependency(descriptor, beanName,
                    autowiredBeanNames, typeConverter);
        }
    }
}

Example usage with an XML config:

        CustomBeanFactory customBeanFactory = new CustomBeanFactory();      
        GenericApplicationContext ctx = new GenericApplicationContext(customBeanFactory);
        XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ctx);
        xmlReader.loadBeanDefinitions(new ClassPathResource("beans.xml"));
        ctx.refresh();

EDIT:

Below you can find Arend v. Reinersdorffs improved version (see the comments for an explanation).

import java.lang.reflect.Field;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.core.MethodParameter;

public class CustomBeanFactory extends DefaultListableBeanFactory {

    public CustomBeanFactory() {
    }

    public CustomBeanFactory(DefaultListableBeanFactory delegate) {
        super(delegate);
    }

    @Override
    public Object resolveDependency(DependencyDescriptor descriptor,
            String beanName, Set<String> autowiredBeanNames,
            TypeConverter typeConverter) throws BeansException {
        //Assign Logger parameters if required      
        if (Logger.class == descriptor.getDependencyType()) {            
            return LoggerFactory.getLogger(getDeclaringClass(descriptor));
        } else {
            return super.resolveDependency(descriptor, beanName,
                    autowiredBeanNames, typeConverter);
        }
    }

    private Class<?> getDeclaringClass(DependencyDescriptor descriptor) {
        MethodParameter methodParameter = descriptor.getMethodParameter();
        if (methodParameter != null) {
            return methodParameter.getDeclaringClass();
        }
        Field field = descriptor.getField();
        if (field != null) {
            return field.getDeclaringClass();
        }
        throw new AssertionError("Injection must be into a method parameter or field.");
    }
}
Shaving answered 15/6, 2010 at 12:5 Comment(5)
Works nicely, thanks. Two points: 1. It should be Logger.class == ... instead of isAssignableFrom(...). isAssignableFrom() is true for any subclass of Logger, but Logger cannot be injected into a field of its subclass. For example @Autowired MyLogger myLogger; for a class MyLogger implements Logger will always throw an exception. 2. Any reason why the Logger is not injected into optional dependencies?Pelson
1. Good point. I think I intended to do the opposite, i.e descriptor.getMethodParameter().getParameterType().isAssignableFrom(Logger.class), but this would inject Loggers into Object fields, so maybe equals is the way to go. 2. No, no reason. The code above worked for my case, but feel free to modify it. If you have an update, send it to me and I'll edit the post. CheersShaving
@AlexanderTorstling Here's my testing version with some small improvements: pastebin.com/b3hnV73UPelson
@Arend v. Reinersdorff Thank you! I added it to the answer.Shaving
I've implemented a very similar solution, but ran into a problem in unit tests that create their own instances of Loggable beans. If the unit test is not a Spring-aware test (ie, run with the SpringJUnit4ClassRunner and/or including @ContextConfiguration) then obviously the bean post-processor doesn't run and thus the Loggables don't have their Log field set. As I have a lot of tests that are truly unit tests (ie don't want or need to have Spring involved), I'm looking for a clean way to handle this problem, without resorting to exposing a public setter for the Log field. Any thoughts?Fullblooded
N
20

Here is an alternative to your solution. You could achieve your goal with BeanFactoryPostProcessor implementation.

Let's assume you want to have a class with logging. Here it is:

  package log;
  import org.apache.log4j.Logger;

  @Loggable
  public class MyBean {

     private Logger logger;
  }

As you could see this class does nothing and created just to be a logger container for simplicity. The only remarkable thing here is @Loggable annotation. Here its source code:

package log;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Loggable {
}

This annotation is only a marker for further processing. And here is a most interesting part:

package log;

import org.apache.log4j.Logger;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

import java.lang.reflect.Field;

public class LoggerBeanFactoryPostProcessor implements BeanFactoryPostProcessor{

    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        String[] names = beanFactory.getBeanDefinitionNames();
        for(String name : names){
            Object bean = beanFactory.getBean(name);
            if(bean.getClass().isAnnotationPresent(Loggable.class)){
                try {
                    Field field = bean.getClass().getDeclaredField("logger");
                    field.setAccessible(true);
                    field.set(bean, Logger.getLogger(bean.getClass()));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

It searches through all beans, and if a bean is marked as @Loggable, it initialize its private field with name logger. You could go even further and pass some parameters in @Loggable annotation. For example, it could be a name of field corresponding to logger.

I used Log4j in this example, but I guess it should work exactly the same way with slf4j.

Nonessential answered 15/6, 2010 at 19:38 Comment(4)
+1 Clearest BeanFactoryPostProcessor example I've seen anywhereMoleskins
Thank you for your post. Your example is quite similar to the link I referenced in one of the comments above, tzavellas.com/techblog/2007/03/31/…. It works well, but cannot work on constructors. I would like to use constructor injection since this means that the logger can be used in the constructor, and that I can declare the field final, a good habit in multi-threaded applications. Ideally constructor injection would work in the same way as field injection, but this would require some sort of constructor argument builders.Shaving
@disown I think you're trying to solve non-existent problem. It's very unlikely you'll have to prevent multithreading issues on start. ApplicationContext descendants are guaranteed to be thread-safe (forum.springsource.org/showthread.php?t=11791).Nonessential
So... is there any clean way to have the logger available in constructor as well? Constructor injection looks messy, requiring the space in the constructor signature and two additional lines of saving the injected logger into the instance's field... if I have understood constructor injection correctly. Is that the only way?Coadjutrix
G
16

To make your code more Spring aware use the InjectionPoint to define the loggers, i.e.:

@Bean
@Scope("prototype")
public Logger logger(InjectionPoint ip) {
    return Logger.getLogger(ip.getMember().getDeclaringClass());
}

@Scope("prototype") is needed here to create 'logger' bean instance every time method is called.

Gamut answered 16/1, 2017 at 15:7 Comment(1)
Why have I not been doing this always?Rubio
S
11

I resolved it with a custom BeanFactory. If anyone comes up with a better solution, I would be happy to hear it. Anyway, here's the bean factory:

import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;

public class CustomBeanFactory extends DefaultListableBeanFactory {

    public CustomBeanFactory() {
    }

    public CustomBeanFactory(DefaultListableBeanFactory delegate) {
        super(delegate);
    }

    @Override
    public Object resolveDependency(DependencyDescriptor descriptor,
            String beanName, Set<String> autowiredBeanNames,
            TypeConverter typeConverter) throws BeansException {
        //Assign Logger parameters if required      
        if (descriptor.isRequired()
                && Logger.class.isAssignableFrom(descriptor
                        .getMethodParameter().getParameterType())) {            
            return LoggerFactory.getLogger(descriptor.getMethodParameter()
                    .getDeclaringClass());
        } else {
            return super.resolveDependency(descriptor, beanName,
                    autowiredBeanNames, typeConverter);
        }
    }
}

Example usage with an XML config:

        CustomBeanFactory customBeanFactory = new CustomBeanFactory();      
        GenericApplicationContext ctx = new GenericApplicationContext(customBeanFactory);
        XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ctx);
        xmlReader.loadBeanDefinitions(new ClassPathResource("beans.xml"));
        ctx.refresh();

EDIT:

Below you can find Arend v. Reinersdorffs improved version (see the comments for an explanation).

import java.lang.reflect.Field;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.core.MethodParameter;

public class CustomBeanFactory extends DefaultListableBeanFactory {

    public CustomBeanFactory() {
    }

    public CustomBeanFactory(DefaultListableBeanFactory delegate) {
        super(delegate);
    }

    @Override
    public Object resolveDependency(DependencyDescriptor descriptor,
            String beanName, Set<String> autowiredBeanNames,
            TypeConverter typeConverter) throws BeansException {
        //Assign Logger parameters if required      
        if (Logger.class == descriptor.getDependencyType()) {            
            return LoggerFactory.getLogger(getDeclaringClass(descriptor));
        } else {
            return super.resolveDependency(descriptor, beanName,
                    autowiredBeanNames, typeConverter);
        }
    }

    private Class<?> getDeclaringClass(DependencyDescriptor descriptor) {
        MethodParameter methodParameter = descriptor.getMethodParameter();
        if (methodParameter != null) {
            return methodParameter.getDeclaringClass();
        }
        Field field = descriptor.getField();
        if (field != null) {
            return field.getDeclaringClass();
        }
        throw new AssertionError("Injection must be into a method parameter or field.");
    }
}
Shaving answered 15/6, 2010 at 12:5 Comment(5)
Works nicely, thanks. Two points: 1. It should be Logger.class == ... instead of isAssignableFrom(...). isAssignableFrom() is true for any subclass of Logger, but Logger cannot be injected into a field of its subclass. For example @Autowired MyLogger myLogger; for a class MyLogger implements Logger will always throw an exception. 2. Any reason why the Logger is not injected into optional dependencies?Pelson
1. Good point. I think I intended to do the opposite, i.e descriptor.getMethodParameter().getParameterType().isAssignableFrom(Logger.class), but this would inject Loggers into Object fields, so maybe equals is the way to go. 2. No, no reason. The code above worked for my case, but feel free to modify it. If you have an update, send it to me and I'll edit the post. CheersShaving
@AlexanderTorstling Here's my testing version with some small improvements: pastebin.com/b3hnV73UPelson
@Arend v. Reinersdorff Thank you! I added it to the answer.Shaving
I've implemented a very similar solution, but ran into a problem in unit tests that create their own instances of Loggable beans. If the unit test is not a Spring-aware test (ie, run with the SpringJUnit4ClassRunner and/or including @ContextConfiguration) then obviously the bean post-processor doesn't run and thus the Loggables don't have their Log field set. As I have a lot of tests that are truly unit tests (ie don't want or need to have Spring involved), I'm looking for a clean way to handle this problem, without resorting to exposing a public setter for the Log field. Any thoughts?Fullblooded
E
2

Try something like:

@Component
public class Example {

  @Autowired
  @Qualifier("exampleLogger")
  private final Logger logger;

}

And:

<bean id="exampleLogger" class="org.slf4j.LoggerFactory" factory-method="getLogger">
  <constructor-arg type="java.lang.Class" value="package.Example"/>        
</bean>
Entree answered 24/9, 2012 at 14:34 Comment(0)
P
1

Since Spring 4.3.0 you can use InjectionPoint or DependencyDescriptor as parameters for bean producing methods:

@Component
public class LoggingFactoryBean {
    @Bean
    public Logger logger(InjectionPoint injectionPoint) {
        Class<?> targetClass = injectionPoint.getMember().getDeclaringClass();
        return LoggerFactory.getLogger(targetClass);
    }
}
Pelson answered 11/6, 2016 at 15:6 Comment(0)
B
0

Yeah, you are going in the wrong direction. If I were you I would inject the LoggerFactory. If you want to hide that it is slf4j then I'd define a LoggerFactory interface and inject a class which delegates through to slf4j Logger.

public interface LoggerFactory {
    public Logger getLogger(Class<?> clazz);
}
...
import org.slf4j.LoggerFactory;
public class Slf4jLoggerFactory implements LoggerFactory {
    public Logger getLogger(Class<?> clazz) {
        return org.slf4j.LoggerFactory.getLogger(clazz);
    }
}

However, before you go there, this is approximately what org.apache.commons.logging is doing right? http://commons.apache.org/logging/

You use Log's instead of Loggers:

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class CLASS {
    private Log log = LogFactory.getLog(CLASS.class);
    ...

Apache then looks through the classpath to see if you have log4j or others and delegates to the "best" one that it finds. Slf4j replaces log4j in the classpath so if you have it loaded (and apache log4j excluded) commons logging will delegate to it instead.

Bogtrotter answered 14/6, 2010 at 23:17 Comment(3)
This is almost the same answer as Mike gave. I would like pass the log in, since I like a pure oo style. The 'service locator' approach is feasible, but a bit intrusive. One could argue that the runtime class should not be part of the interface in getLog, but I would expect Spring to pass on some sort of structure describing the target. Is this not at all possible?Shaving
And on top of all, the commons logging hack of auto-discovery was the very reason that slf4j was born.Shaving
Spring does not have this capacity with FactoryBean, no. It would create some sort of circular dependency if X depends on Y but Y knows about X when it is constructed.Bogtrotter
C
-1
  1. Why are you creating a new logger for each instance? The typical pattern is to have one logger per class (as a private static member).

  2. If you really do want to do it that way: Maybe you can write a logger factory class, and inject that? Something like:

    @Singleton 
    public class LogFactory { 
        public Logger getLogger(Object o) {  
            return LoggerFactory.getLogger(o.getClass());  
        }  
    }
    
Calamondin answered 14/6, 2010 at 14:58 Comment(1)
1: See wiki.apache.org/commons/Logging/StaticLog 2: This is what I have done already. It works ok, but is very verbose IMO.Shaving
S
-3

I am trying to get this feature into official SLF4J API. Please support/vote/contribute: https://issues.jboss.org/browse/JBLOGGING-62

(this feature is already implemented by JBoss Logging + Seam Solder, see http://docs.jboss.org/seam/3/latest/reference/en-US/html/solder-logging.html)

11.4. Native logger API

You can also inject a "plain old" Logger (from the JBoss Logging API):

import javax.inject.Inject;
import org.jboss.logging.Logger;

public class LogService {

    @Inject
    private Logger log;

    public void logMessage() {
        log.info("Hey sysadmins!");
    }

}

Log messages created from this Logger will have a category (logger name) equal to the fully-qualified class name of the bean implementation class. You can specify a category explicitly using an annotation.

@Inject @Category("billing")
private Logger log;

You can also specify a category using a reference to a type:

@Inject @TypedCategory(BillingService.class)
private Logger log;

Sorry for not providing a relevant answer.

Ser answered 18/4, 2011 at 11:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.