手机版 欢迎访问it开发者社区(www.mfbz.cn)网站

当前位置: > 开发

Spring internals - BeanPostProcessor

时间:2021/5/8 1:38:42|来源:|点击: 次

[Refere

Have you ever wondered how spring does things? How field annotated with @Autowired is populated? How asynchronous or scheduled methods are discovered. In this post, I’m going to take a deeper look and scratch a bit on the surface of spring internals. I’ll focus on BeanPostProcessor interface which can be used to achieve interesting things and is used in many various functionalities across spring framework itself.

Spring container boot process

Let’s take a look at the spring boot process which can give us a hint on how important part of the framework this hook is.

@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
prepareRefresh();

    ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
    prepareBeanFactory(beanFactory);

    try {
        postProcessBeanFactory(beanFactory);
        invokeBeanFactoryPostProcessors(beanFactory);
        registerBeanPostProcessors(beanFactory);
        initMessageSource();
        initApplicationEventMulticaster();
        onRefresh();
        registerListeners();
        finishBeanFactoryInitialization(beanFactory);
        finishRefresh();
    } catch (BeansException ex) {
        destroyBeans();
        cancelRefresh(ex);
        throw ex;
    } finally {
        resetCommonCaches();
    }
}

}
org.springframework.context.support.AbstractApplicationContext#refresh

To quickly recap how things are happening during spring context initialization. Firstly all beans definitions are registered in BeanFactory then BeanFactoryPostprocessors are applied on the BeanFactory (this is the moment when you can change definitions of beans registered in BeanFactory and I will dig into this some other day). Next step is registration of BeanPostProcessors. Those registered BeanPostProcessors are executed at a later time when someone is actually requesting bean to be created/retrieved from IoC container - when they are needed (FactoryBeanRegistrySupport#postProcessObjectFromFactoryBean).

BeanPostProcessor interface

public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
	return bean;
}

}
org.springframework.beans.factory.config.BeanPostProcessor

This hook allows customization of bean instance just after bean creation or after it’s been initialized. All BeanPostProcessor which are annotated or declared (anyone remembers xmls?) will be automatically recognized and picked up by the framework when it definitions are registered.

As you can see there are only two methods in this interface: postProcessBeforeInitialization and postProcesAfterInitialization as the name suggest first method is executed before the bean init code (@PostConstruct or afterPropertiesSet hook). After initialization hook is executed when the bean has been initialized (after afterPropertiesSet or @PostConstruct). Extra area of possibilities is presented to the users because BeanPostProcessor hook is executed also on FactoryBean instances so you can customize objects even before they are created (kind off). If you want to short-circuit processing of your bean you can simply return null from postProcessor and processing chain will be broken which means no more of them will be applied.

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsBeforeInitialization

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization

Sample usage

We already know a bit on how it works now let’s try and implement something to take advantage of this interface. Imagine that you want to hack some measurements of your application performance. For the sake of example, we can do very naive implementation using just a couple of lines of code. Let’s assume that I want to measure method execution times, but not all of them, just some of them annotated with following marker interface:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Measured {
String DEFAULT_PERFORMANCE_LOGGER = “PERFORMANCE”;

String value() default DEFAULT_PERFORMANCE_LOGGER;

}
Not much is happening in here yet. It’s just an interface which will be detected at runtime (@Retention(RUNTIME)) and can be applied on method level (@Target(METHOD)). We’ll be using it to detect methods on which we’ll be doing extra measurements.

With this annotation in place we can already create very simple service:

@Service
class SampleService {
private static final Logger log = LoggerFactory.getLogger(SampleService.class);

@Measured
public void doWork(long sleepTime) {
    try {
        log.info("Starting work");
        Thread.sleep(sleepTime);
        log.info("Finishing work");
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

}
Annotation alone will not be enough, and it’s not going to work by itself. We have to hack something extra which will allow us to detect those methods and somehow execute extra code around them. Well, spring does a lot of things like that. Have you ever wondered how transactional annotation works? Luckily spring provides abstractions that simplify a lot.

Let’s do baby steps and first of all find methods in which we are interested in:

private boolean hasAnyMeasuredMethod(Object bean) {
return Stream
.of(ReflectionUtils.getAllDeclaredMethods(bean.getClass()))
.anyMatch(method -> AnnotationUtils.getAnnotation(method, Measured.class) != null);
}
From this, we’ll be able to tell if we should even consider this class as a candidate on which we’d like to do any processing.

Now getting to a bit harder part - measurements itself. If we’d have to implement it by ourselves we’d probably just do something like this:

public void doWork(long sleepTime) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
try {
// do things
} finally {
stopWatch.stop();
log.info(“Execution took {}”, stopWatch.getLastTaskTimeMillis());
}
}
But the problem is that an instance of the object is already created. So we need to do something more complicated and somehow wrap the existing method with time measurements. We are already using spring and it comes with nice abstractions that allow us to do exactly that (no need to struggle with cglib 😉). Using ProxyFactory we can pretty easily introduce some AOP programming and do exactly what we need. Let’s write the implementation of MethodInvocationInterceptor:

private static class MeasuringMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
final Method method = invocation.getMethod();
final Measured annotation = AnnotationUtils.getAnnotation(method, Measured.class);
return annotation == null
? invocation.proceed()
: proceedMeasured(invocation, annotation.value());
}

private Object proceedMeasured(MethodInvocation invocation, String loggerName) throws Throwable {
    final Logger logger = LoggerFactory.getLogger(loggerName);
    final StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    try {
        return invocation.proceed();
    } finally {
        stopWatch.stop();
        logger.warn(
                "Execution of {} took {} ms",
                resolveLogMethod(invocation), stopWatch.getTotalTimeMillis());
    }
}

private String resolveLogMethod(MethodInvocation invocation) {
    return invocation.getMethod().getDeclaringClass().getCanonicalName() + "#" + invocation.getMethod().getName();
}

}
A couple of things is happening in here so let’s start from the beginning. Method interceptor is an Advice (org.aopalliance.aop.Advice) which is the base interface for all aspect-oriented things in spring. With MethodInterceptor we can easily write something that will allow us to intercept method invocation, modify the result or maybe do something extra around the method which is called.

Firstly we examine if the method has annotation @Measured on it. If not just call it and forget about the whole thing. If not let’s do exactly what we would have done if writing it manually.

Now, all we have to do is take advantage of ProxyFactory available in spring and just wrap our class in a proxy and we are good to go.

@Component
class MeasuringBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (hasAnyMeasuredMethod(bean)) {
return measuredProxy(bean);
}

    return bean;
}

private Object measuredProxy(Object bean) {
    ProxyFactory proxyFactory = new ProxyFactory(bean);
    proxyFactory.addAdvice(new MeasuringMethodInterceptor());
    return proxyFactory.getProxy();
}

private boolean hasAnyMeasuredMethod(Object bean) {
    return Stream
            .of(ReflectionUtils.getAllDeclaredMethods(bean.getClass()))
            .anyMatch(method -> AnnotationUtils.getAnnotation(method, Measured.class) != null);
}

}
Complete implementation. Note that there is ready to use implementation provided by spring org.springframework.aop.interceptor.PerformanceMonitorInterceptor so you don’t really need to write one by yourself.

Let’s quickly test (writing unit test for it is out of the scope of this post 😉) our solution and see how it’s working:

@SpringBootApplication
class BeanpostprocessorApplication {

public static void main(String[] args) {
    final ConfigurableApplicationContext ctx = SpringApplication.run(BeanpostprocessorApplication.class, args);
    final SampleService bean = ctx.getBean(SampleService.class);

    for (int i = 0; i < 5; i++) {
        bean.doWork(TimeUnit.SECONDS.toMillis(1000));
    }
}

}
And the logs from the execution:

2019-02-11 19:15:56.717 INFO 22856 — [main] c.p.b.e.s.i.b.SampleService: Starting work
2019-02-11 19:15:57.720 INFO 22856 — [main] c.p.b.e.s.i.b.SampleService: Finishing work
2019-02-11 19:15:57.721 WARN 22856 — [main] PERFORMANCE: Execution of com.pchudzik.blog.example.spring.internals.beanpostprocessor.SampleService#doWork took 1013 ms
2019-02-11 19:15:57.723 INFO 22856 — [main] c.p.b.e.s.i.b.SampleService: Starting work
2019-02-11 19:15:58.728 INFO 22856 — [main] c.p.b.e.s.i.b.SampleService: Finishing work
2019-02-11 19:15:58.728 WARN 22856 — [main] PERFORMANCE: Execution of com.pchudzik.blog.example.spring.internals.beanpostprocessor.SampleService#doWork took 1005 ms
2019-02-11 19:15:58.728 INFO 22856 — [main] c.p.b.e.s.i.b.SampleService: Starting work
2019-02-11 19:15:59.731 INFO 22856 — [main] c.p.b.e.s.i.b.SampleService: Finishing work
2019-02-11 19:15:59.732 WARN 22856 — [main] PERFORMANCE: Execution of com.pchudzik.blog.example.spring.internals.beanpostprocessor.SampleService#doWork took 1004 ms
2019-02-11 19:15:59.732 INFO 22856 — [main] c.p.b.e.s.i.b.SampleService: Starting work
2019-02-11 19:16:00.733 INFO 22856 — [main] c.p.b.e.s.i.b.SampleService: Finishing work
2019-02-11 19:16:00.733 WARN 22856 — [main] PERFORMANCE: Execution of com.pchudzik.blog.example.spring.internals.beanpostprocessor.SampleService#doWork took 1001 ms
2019-02-11 19:16:00.733 INFO 22856 — [main] c.p.b.e.s.i.b.SampleService: Starting work
2019-02-11 19:16:01.736 INFO 22856 — [main] c.p.b.e.s.i.b.SampleService: Finishing work
2019-02-11 19:16:01.736 WARN 22856 — [main] PERFORMANCE: Execution of com.pchudzik.blog.example.spring.internals.beanpostprocessor.SampleService#doWork took 1003 ms
Wasn’t so hard after all right? As you can see hacking something like that is actually pretty easy thing to do.

I’m not responsible for any damage you’ll do on your production servers using this naive implementation 😃 Note the overhead of extra abstraction.

Interesting bean post processors

To give you some ideas what you can achieve with this simple hook let’s take a look at a couple out of the box post processors that are registered by default:

org.springframework.context.support.ApplicationContextAwareProcessor - simple hook that will inject ApplicationContext instance into beans.

org.springframework.context.support.ApplicationListenerDetector - register beans as ApplicationListener (org.springframework.context.ApplicationListener).

org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor - creates proxy around @Async methods so they get executed via ExecutorService.

org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor - hook which recognises @Scheduled methods of the bean when @EnableScheduling is applied.

Summary

Next time you’d like to customize your bean after it’s been created and you are using spring framework in your application you’ll know that there is a hook designed to do exactly that. As you can see achieving things like measuring the execution time of particular methods is not so complicated (at least naive implementation of it). Spring offers a lot of convenient abstractions that allow to hook up into its internals and BeanPostProcessor is just one of them.

Extensive documentation off all official extensions points in spring

Complete examples.

See Also
If you’ve enjoyed or found this post useful you might also like:

Conversions in spring
Poor’s man domain events
Events in spring v2
Events in spring
Introducing springmock
20 Feb 2019 #java #spring

Copyright © 2002-2019 某某自媒体运营 版权所有