Статьи

Внедрение регистраторов с помощью Spring

В моем текущем проекте мы используем Spring MVC и стараемся использовать как можно больше автопроводки. Я лично настоятельно предпочитаю инжекцию в конструктор, так как это дает мне возможность работать с конечными полями. Мне также нравится иметь возможность вводить все, что нужно классу, включая регистраторы. Большую часть времени я действительно не хочу использовать пользовательские регистраторы из тестов, но иногда я хочу убедиться, что что-то регистрируется правильно, и возможность внедрения регистратора кажется естественным способом сделать это. Итак, с этой преамбулой в стороне, моя проблема заключалась в том, что этого было довольно трудно достичь весной. В частности, я использую SLF4J, и я хочу ввести эквивалент выполнения LoggerFactory.getLogger (MyBusinessObject.class). К сожалению, Spring не предоставляет доступ к месту, где что-то будет введено в любой из доступных крючков.Большинство решений этой проблемы, которые я нашел, основано на использовании BeanPostProcessor для установки поля объекта после его создания. Это противоречит трем моим целям / принципам — я не могу использовать регистратор в конструкторе, поле будет изменчивым, и Spring не сообщит мне, если я допустил ошибку в своем подключении.

Однако было одно решение, которое я нашел в посте StackOverflow — к сожалению, оно не было полным. В частности, мне нужно было использовать его в настройках Spring MVC, а также внутри тестов. Таким образом, этот пост в блоге предназначен для того, чтобы предоставить полное решение для чего-то подобного. Это простая проблема, но работать с ней было удивительно сложно. Но теперь, когда он у меня есть, это будет очень удобно. Этот код для Spring 3.1, и я не проверял его ни на чем другом.

Первая часть этой инъекции состоит в создании нашего собственного BeanFactory — это то, что Spring использует внутри для управления bean-компонентами и зависимостями. По умолчанию он называется DefaultListableBeanFactory, и мы просто подклассируем его следующим образом:

public class LoggerInjectingListableBeanFactory
                extends DefaultListableBeanFactory {

    public LoggerInjectingListableBeanFactory() {
        setParameterNameDiscoverer(
            new LocalVariableTableParameterNameDiscoverer());
        setAutowireCandidateResolver(
           new QualifierAnnotationAutowireCandidateResolver());

    }

    public LoggerInjectingListableBeanFactory(
              BeanFactory parentBeanFactory) {
        super(parentBeanFactory);
        setParameterNameDiscoverer(
            new LocalVariableTableParameterNameDiscoverer());
        setAutowireCandidateResolver(
            new QualifierAnnotationAutowireCandidateResolver());
    }

    @Override
    public Object resolveDependency(
               DependencyDescriptor descriptor, String beanName,
              Set<String> autowiredBeanNames, TypeConverter typeConverter)
                     throws BeansException {
        Class<?> declaringClass = null;

        if(descriptor.getMethodParameter() != null) {
            declaringClass = descriptor.getMethodParameter()
                    .getDeclaringClass();
        } else if(descriptor.getField() != null) {
            declaringClass = descriptor.getField()
                    .getDeclaringClass();
        }

        if(Logger.class.isAssignableFrom(descriptor.getDependencyType())) {
            return LoggerFactory.getLogger(declaringClass);
        } else {
            return super.resolveDependency(descriptor, beanName,
                    autowiredBeanNames, typeConverter);
        }
    }
}

Волшебство происходит внутри resolDependency, где мы можем выяснить объявленный класс, проверив либо параметр метода, либо поле — и затем выясним, является ли запрашиваемая вещь Logger. В противном случае мы просто делегируем супер реализацию.

Чтобы использовать это из чего-либо, нам нужен фактический ApplicationContext, который его использует. Я не нашел никакой ловушки для установки BeanFactory после того, как был создан контекст приложения, поэтому я закончил тем, что создал две новые реализации ApplicationContext — одну для тестов и одну для цели Spring MVC. Они немного отличаются, но старайтесь делать как можно меньше, сохраняя поведение оригинала. Контекст приложения для тестов выглядит следующим образом:

public class LoggerInjectingGenericApplicationContext
                    extends GenericApplicationContext {
    public LoggerInjectingGenericApplicationContext() {
        super(new LoggerInjectingListableBeanFactory());
    }
}

Этот просто вызывает супер-конструктор с экземпляром нашей пользовательской фабрики бинов. Контекст приложения для Spring MVC выглядит следующим образом:

public class LoggerInjectingXmlWebApplicationContext
                    extends XmlWebApplicationContext {
    @Override
    protected DefaultListableBeanFactory createBeanFactory() {
        return new LoggerInjectingListableBeanFactory(
                    getInternalParentBeanFactory());
    }
}

XmlWebApplicationContext не имеет конструктора, который принимает фабрику бинов, поэтому вместо этого мы переопределяем метод createBeanFactory для возврата нашего пользовательского экземпляра. Чтобы реально использовать эти реализации, нужно еще кое-что. Чтобы наши тесты могли использовать его, необходима реализация test.context.support.ContextLoader. Этот код в основном просто скопирован из реализации по умолчанию — к сожалению, он не предоставляет никаких точек расширения, и место, которое я хочу переопределить, находится в середине двух финальных методов. Просто ужасно просто копировать реализации, но для этого нет хуков …

public class LoggerInjectingApplicationContextLoader
                        extends AbstractContextLoader {
    public final ApplicationContext loadContext(
     MergedContextConfiguration mergedContextConfiguration)
                                  throws Exception {
        String[] locations = mergedContextConfiguration.getLocations();
        GenericApplicationContext context =
                  new LoggerInjectingGenericApplicationContext();
        context.getEnvironment().setActiveProfiles(
               mergedContextConfiguration.getActiveProfiles());
        loadBeanDefinitions(context, locations);
        AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
        context.refresh();
        context.registerShutdownHook();
        return context;
    }

    public final ConfigurableApplicationContext
            loadContext(String... locations) throws Exception {
        GenericApplicationContext context =
              new LoggerInjectingGenericApplicationContext();
        loadBeanDefinitions(context, locations);
        AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
        context.refresh();
        context.registerShutdownHook();
        return context;
    }

    protected void loadBeanDefinitions(
            GenericApplicationContext context, String... locations) {
        createBeanDefinitionReader(context).
               loadBeanDefinitions(locations);
    }

    protected BeanDefinitionReader createBeanDefinitionReader(
                      final GenericApplicationContext context) {
        return new XmlBeanDefinitionReader(context);
    }

    @Override
    public String getResourceSuffix() {
        return "-context.xml";
    }
}

Последнее, что необходимо для того, чтобы ваши тесты использовали пользовательскую фабрику бинов, — это указать загрузчик, который будет использоваться в ContextConfiguration в вашем тестовом классе, например:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = "file:our-app-config.xml",
          loader = LoggerInjectingApplicationContextLoader.class)
public class SomeTest {
}

In order to get Spring MVC to pick this up, you can edit your web.xml and add a new init-param for the DispatcherServlet, like this:

    <servlet>
        <servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
        <servlet-class>
           org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>WEB-INF/our-app-config.xml</param-value>
        </init-param>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
               com.example.LoggerInjectingXmlWebApplicationContext
            </param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

This approach seems to work well enough. Some of the code is slightly ugly and I would definitely love to have a better hook for injection points to know where it will get injected. Having factory methods be able to take the receiver object might be very convenient, for example. Being able to customize the bean factory seems like it also should be much easier than this.

 

From http://olabini.com/blog/2011/08/injecting-loggers-using-spring