19

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?

skaffman
  • 398,947
  • 96
  • 818
  • 769
Alexander Torstling
  • 18,552
  • 7
  • 62
  • 74
  • 6
    Is it really worth it, just to avoid saying ` = LoggerFactory.getLogger()`? – skaffman Jun 14 '10 at 18:01
  • I don't like the static bind to LoggerFactory, for the same reasons Martin Fowler outlines in http://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. – Alexander Torstling Jun 15 '10 at 08:29
  • I mean, this information is passed to BeanPostProcessors: http://www.tzavellas.com/techblog/2007/03/31/implementing-seam-style-logger-injection-with-spring/. Can the same not be accomplished for constructor injection? – Alexander Torstling Jun 15 '10 at 08:31

8 Answers8

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.

wax
  • 2,400
  • 3
  • 19
  • 28
  • 1
    +1 Clearest BeanFactoryPostProcessor example I've seen anywhere – Anonymoose Jun 15 '10 at 19:46
  • 1
    Thank you for your post. Your example is quite similar to the link I referenced in one of the comments above, http://www.tzavellas.com/techblog/2007/03/31/implementing-seam-style-logger-injection-with-spring/. 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. – Alexander Torstling Jun 15 '10 at 20:01
  • @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 (http://forum.springsource.org/showthread.php?t=11791). – wax Jun 16 '10 at 07:13
  • 1
    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? – Tuukka Mustonen Jun 18 '10 at 13:34
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.

magiccrafter
  • 5,175
  • 1
  • 56
  • 50
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.");
    }
}
Alexander Torstling
  • 18,552
  • 7
  • 62
  • 74
  • 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? – Arend v. Reinersdorff Sep 25 '13 at 18:10
  • 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. Cheers – Alexander Torstling Sep 26 '13 at 06:56
  • @AlexanderTorstling Here's my testing version with some small improvements: http://pastebin.com/b3hnV73U – Arend v. Reinersdorff Oct 01 '13 at 19:36
  • @Arend v. Reinersdorff Thank you! I added it to the answer. – Alexander Torstling Oct 02 '13 at 13:49
  • 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? – E-Riz Feb 19 '14 at 15:56
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>
Adolfo
  • 798
  • 1
  • 7
  • 15
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);
    }
}
Arend v. Reinersdorff
  • 4,110
  • 2
  • 36
  • 40
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.

Gray
  • 115,027
  • 24
  • 293
  • 354
  • 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? – Alexander Torstling Jun 15 '10 at 08:18
  • 1
    And on top of all, the commons logging hack of auto-discovery was the very reason that slf4j was born. – Alexander Torstling Jun 15 '10 at 08:34
  • 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. – Gray Jun 15 '10 at 13:18
-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());  
        }  
    }
    
Pascal Thivent
  • 562,542
  • 136
  • 1,062
  • 1,124
Mike Baranczak
  • 8,291
  • 8
  • 47
  • 71
-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.

Community
  • 1
  • 1
Hendy Irawan
  • 20,498
  • 11
  • 103
  • 114