【问题标题】:Spring constructor injection of SLF4J logger - how to get injection target class?SLF4J记录器的Spring构造函数注入-如何获取注入目标类?
【发布时间】:2011-03-03 13:32:01
【问题描述】:

我正在尝试使用 Spring 将 SLF4J 记录器注入到这样的类中:

@Component
public class Example {

  private final Logger logger;

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

我找到了我已经实现的FactoryBean 类。但问题是我无法获得有关注入目标的任何信息:

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? */);
    }
}   

FactoryBean 是正确的方法吗?当使用 picocontainers factory injection 时,你会得到传入目标的Type。在guice 中它有点trickier。但是你如何在 Spring 中实现这一点呢?

【问题讨论】:

  • 真的值得吗,只是为了避免说` = LoggerFactory.getLogger()`?
  • 我不喜欢静态绑定到 LoggerFactory,原因与 Martin Fowler 在 martinfowler.com/articles/injection.html 中概述的原因相同。注入的 LoggerFactory 是一种可接受的解决方案(遵循服务定位器模式),但有点冗长。我想有人可能会争辩说,日志注入需要使用服务定位器,因为纯依赖应该是与目标无关的。但是定位器解决方案很冗长,其他框架支持它,我希望 Spring 能够提供有关目标的某种信息。我只是想知道这是否真的不可能。
  • 我的意思是,这个信息被传递给 BeanPostProcessors:tzavellas.com/techblog/2007/03/31/…。构造函数注入也不能做到这一点吗?

标签: java spring slf4j constructor-injection


【解决方案1】:
  1. 为什么要为每个实例创建一个新的记录器?典型的模式是每个类有一个记录器(作为私有静态成员)。

  2. 如果您真的想这样做:也许您可以编写一个记录器工厂类,然后注入它?比如:

    @Singleton 
    public class LogFactory { 
        public Logger getLogger(Object o) {  
            return LoggerFactory.getLogger(o.getClass());  
        }  
    }
    

【讨论】:

【解决方案2】:

是的,你走错了方向。如果我是你,我会注入 LoggerFactory。如果您想隐藏它是 slf4j,那么我将定义一个 LoggerFactory 接口并注入一个委托给 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);
    }
}

然而,在你去那里之前,这大概就是 org.apache.commons.logging 正在做的事情吧? http://commons.apache.org/logging/

您使用 Log 而不是 Logger:

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

Apache 然后查看类路径以查看您是否有 log4j 或其他,并将其委托给它找到的“最好的”。 Slf4j 替换了类路径中的 log4j,因此如果您加载了它(并且排除了 apache log4j),则公共日志记录将委托给它。

【讨论】:

  • 这与 Mike 给出的答案几乎相同。我想通过登录,因为我喜欢纯 oo 风格。 “服务定位器”方法是可行的,但有点侵入性。有人可能会争辩说,运行时类不应该是 getLog 中接口的一部分,但我希望 Spring 传递某种描述目标的结构。这根本不可能吗?
  • 最重要的是,自动发现的公共日志记录黑客正是 slf4j 诞生的原因。
  • Spring 不具备 FactoryBean 的这种能力,没有。如果 X 依赖于 Y 但 Y 在构造 X 时知道 X,它会创建某种循环依赖。
【解决方案3】:

我使用自定义 BeanFactory 解决了这个问题。如果有人提出更好的解决方案,我会很高兴听到它。无论如何,这是 bean 工厂:

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);
        }
    }
}

XML 配置的使用示例:

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

编辑:

您可以在下面找到 Arend v. Reinersdorffs 的改进版本(有关说明,请参阅 cmets)。

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.");
    }
}

【讨论】:

  • 工作得很好,谢谢。两点:1.应该是Logger.class == ...而不是isAssignableFrom(...)isAssignableFrom() 适用于 Logger 的任何子类,但不能将 Logger 注入到其子类的字段中。例如,@Autowired MyLogger myLogger;class MyLogger implements Logger 总是会抛出异常。 2. Logger 没有注入可选依赖有什么原因?
  • 1.好点子。我想我打算做相反的事情,即descriptor.getMethodParameter().getParameterType().isAssignableFrom(Logger.class),但这会将Loggers注入Object字段,所以也许equals是要走的路。 2.不,没有理由。上面的代码适用于我的情况,但可以随意修改它。如果您有更新,请将其发送给我,我将编辑帖子。干杯
  • @AlexanderTorstling 这是我的测试版本,有一些小的改进:pastebin.com/b3hnV73U
  • @Arend v. Reinersdorff 谢谢!我将其添加到答案中。
  • 我实现了一个非常相似的解决方案,但是在创建自己的 Loggable bean 实例的单元测试中遇到了问题。如果单元测试不是 Spring 感知测试(即,使用 SpringJUnit4ClassRunner 和/或包括 @ContextConfiguration 运行),那么显然 bean 后处理器不会运行,因此 Loggables 没有设置它们的 Log 字段。由于我有很多真正的单元测试测试(即不希望或不需要涉及 Spring),我正在寻找一种干净的方法来处理这个问题,而无需为 Log 字段公开一个公共设置器.有什么想法吗?
【解决方案4】:

这是您的解决方案的替代方案。您可以通过 BeanFactoryPostProcessor 实现来实现您的目标。

假设您想要一个带有日志记录的类。这里是:

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

  @Loggable
  public class MyBean {

     private Logger logger;
  }

正如你所见,这个类什么都不做,只是为了简单起见而创建一个记录器容器。这里唯一值得注意的是 @Loggable 注释。 这里是它的源代码:

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 {
}

此注释仅作为进一步处理的标记。这是最有趣的部分:

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();
                }
            }
        }
    }
}

它搜索所有 bean,如果一个 bean 被标记为 @Loggable,它会使用名称 logger 初始化其私有字段。你可以更进一步,在 @Loggable 注释中传递一些参数。例如,可以是logger对应的字段名。

我在这个例子中使用了 Log4j,但我想它应该与 slf4j 以完全相同的方式工作。

【讨论】:

  • +1 我见过的最清晰的 BeanFactoryPostProcessor 示例
  • 感谢您的帖子。您的示例与我在上述 cmets 之一中引用的链接 tzavellas.com/techblog/2007/03/31/… 非常相似。它工作得很好,但不能在构造函数上工作。我想使用构造函数注入,因为这意味着记录器可以在构造函数中使用,并且我可以将字段声明为 final,这是多线程应用程序中的一个好习惯。理想情况下,构造函数注入的工作方式与字段注入相同,但这需要某种构造函数参数构建器。
  • @disown 我认为您正在尝试解决不存在的问题。您不太可能必须在开始时防止多线程问题。 ApplicationContext 后代保证是线程安全的 (forum.springsource.org/showthread.php?t=11791)。
  • 那么......有没有什么干净的方法可以让记录器在构造函数中也可用?构造函数注入看起来很混乱,需要构造函数签名中的空间以及将注入的记录器保存到实例字段中的另外两行......如果我正确理解了构造函数注入。这是唯一的方法吗?
【解决方案5】:

我正在尝试将此功能引入官方 SLF4J API。请支持/投票/投稿:https://issues.jboss.org/browse/JBLOGGING-62

(此功能已由 JBoss Logging + Seam Solder 实现,请参阅http://docs.jboss.org/seam/3/latest/reference/en-US/html/solder-logging.html

11.4。本机记录器 API

您还可以注入一个“普通的旧”Logger(来自 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!");
    }

}

从此 Logger 创建的日志消息将具有与 bean 实现类的完全限定类名相同的类别(记录器名称)。您可以使用注释显式指定类别。

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

您还可以使用对类型的引用来指定类别:

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

很抱歉没有提供相关答案。

【讨论】:

    【解决方案6】:

    尝试类似:

    @Component
    public class Example {
    
      @Autowired
      @Qualifier("exampleLogger")
      private final Logger logger;
    
    }
    

    还有:

    <bean id="exampleLogger" class="org.slf4j.LoggerFactory" factory-method="getLogger">
      <constructor-arg type="java.lang.Class" value="package.Example"/>        
    </bean>
    

    【讨论】:

      【解决方案7】:

      从 Spring 4.3.0 开始,您可以使用 InjectionPoint 或 DependencyDescriptor 作为 bean 生成方法的参数:

      @Component
      public class LoggingFactoryBean {
          @Bean
          public Logger logger(InjectionPoint injectionPoint) {
              Class<?> targetClass = injectionPoint.getMember().getDeclaringClass();
              return LoggerFactory.getLogger(targetClass);
          }
      }
      

      【讨论】:

        【解决方案8】:

        为了让您的代码更加了解 Spring,请使用 InjectionPoint 来定义记录器,即:

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

        这里需要@Scope("prototype") 来在每次调用方法时创建'logger' bean 实例。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-03-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多