Sunday, October 29, 2017

Spring bean lifecycle, postrocessors, profiler

0. intro

When we create object - we have only one entry point to modify somehow object state: constructor.
In Spring this process is much more complicated:


let's create a simple spring application to see how it goes.

1. Project structure

It's a regular gradle-java project.

conten of buid.gradle file:

group 'com.demien.spring'version '1.0-SNAPSHOT'
apply plugin: 'java'
sourceCompatibility = 1.8
repositories {
    mavenCentral()
}

dependencies {
    compile group: 'org.springframework', name: 'spring-core', version: '5.0.0.RELEASE'    
    compile group: 'org.springframework', name: 'spring-beans', version: '5.0.0.RELEASE'    compile group: 'org.springframework', name: 'spring-context', version: '5.0.0.RELEASE'
    testCompile group: 'junit', name: 'junit', version: '4.12'}



directory structure:




2. Annotations

For deep understanding of some life cycle phases let's create 2 annotations:

- Generate name annotation. If field is annotated by this annotation, that means we have to generate the value for this field - simulation of "name". For that we have also settings: minLenght and maxLenght  - length of name which we have to generate.

package com.demien.spring.lifecycle.annotations;

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

@Retention(RetentionPolicy.RUNTIME)
public @interface GeneratedName {
    int minLength();
    int maxLength();
}

- Profiling annotation. If class has such annotation - we have to measure execution time of every class method.
package com.demien.spring.lifecycle.annotations;

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

@Retention(RetentionPolicy.RUNTIME)
public @interface Profiling {
}


3. Spring bean - messenger

Now let's create a very simple bean which will be printing messages.

Interface is very simple: just 2 methods:

package com.demien.spring.lifecycle.beans;

public interface Messenger {
    void printMessage();
    void setUp(String symbol);
}


Implementation is more complicated:
  - we are using annotations from above
  - we have 3 "state" methods: constructor, init and setup


package com.demien.spring.lifecycle.beans;

import com.demien.spring.lifecycle.annotations.GeneratedName;
import com.demien.spring.lifecycle.annotations.Profiling;

import java.time.LocalTime;

@Profiling
public class SimpleMessenger implements Messenger {

    private String messageText = "NULL";
    private String symbol = "";

    @GeneratedName(minLength = 5, maxLength = 10)
    private String name;

    public SimpleMessenger() {
        System.out.print("Constructor: ");
        printMessage();
    }

    public void init() {
        System.out.print("Init: ");
        printMessage();
    }

    @Override    
    public void setUp(String symbol) {
        this.symbol = symbol;
        System.out.print("Setup: ");
        printMessage();
    }
public void setMessageText(String messageText) { this.messageText = messageText; } @Override
    public void printMessage() {
        System.out.println();
        System.out.println(messageText + ", " + name+symbol+" ["+ LocalTime.now()+"]");
    }


}


4. JMX profiler settings 

For profiler, it's better to have ability to turn on/off when it's needed. We can use JMX for that.
For JMX we need an interface which name ends by MBean and implementation of it:

package com.demien.spring.lifecycle.jmx;

public interface ProfilerSettingsMBean {
    void setEnabled(boolean enabled);
}


package com.demien.spring.lifecycle.jmx;

public class ProfilerSettings implements ProfilerSettingsMBean {

    private boolean enabled;

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
}


5. AppConfig

It's the most complicated and interesting part of our application.

- definition of message bean. Here we also defining "init-method" which will be executed after bean initialization.

@Bean(initMethod = "init")
Messenger messenger() {
    SimpleMessenger messenger = new SimpleMessenger();
    messenger.setMessageText("Hello");
    return messenger;
}
-

- We want to do some actions(execute messenger.setUp method) when spring created application context - on context refresh event.

@Bean
ApplicationListener<ContextRefreshedEvent> refreshedEventApplicationListener() {
    return new ApplicationListener<ContextRefreshedEvent>() {
        @Override        
        public void onApplicationEvent(ContextRefreshedEvent event) {
            ApplicationContext ctx = event.getApplicationContext();
            Messenger messenger = ctx.getBean(Messenger.class);
            messenger.setUp("!");
        }
    };

}

- we need post processor for dealing with annotation @GeneratedName - we have to generate name and put it into such field.

@Bean
BeanPostProcessor nameGenerationPostProcessor() {

    return new BeanPostProcessor() {
        @Override        
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            Field[] fields = bean.getClass().getDeclaredFields();
            for (Field field : fields) {
                GeneratedName annotation = field.getAnnotation(GeneratedName.class);
                if (annotation != null) {
                    field.setAccessible(true);
                    ReflectionUtils.setField(field, bean, generateName(annotation.minLength(), annotation.maxLength()));
                }
            }
            return bean;
        }

    };
}


- and also we have to process @Profiling annotation.
For that we can on "beforeInitalization" phase store classes which have such annotation. And on "afterInilialization" phase we can return "proxy" object which will be printing execution time after method invocation if such settings is turned on.

@Bean
BeanPostProcessor profilingPostProcessor() throws Exception {
    Map<String, Class> map = new HashMap<>();
    ProfilerSettings profilerSettings = new ProfilerSettings();
    MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer();
    beanServer.registerMBean(profilerSettings, new ObjectName("Profiling", "name", "settings"));

    return new BeanPostProcessor() {

        @Override        
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            Class<?> beanClass = bean.getClass();
            if (beanClass.isAnnotationPresent(Profiling.class)) {
                map.put(beanName, beanClass);
            }
            return null;
        }

        @Override        
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            Class beanClass = map.get(beanName);
            if (beanClass != null) {
                return Proxy.newProxyInstance(beanClass.getClassLoader(), beanClass.getInterfaces(), new InvocationHandler() {
                    @Override                    public Object invoke(Object proxy, Method method, Object[] objects) throws Throwable {
                        long before = System.nanoTime();

                        Object retval = method.invoke(bean, objects);
                        if (profilerSettings.isEnabled()) {
                            System.out.println("exec time:" + (System.nanoTime() - before));
                        }
                        return retval;
                    }
                });
            }
            return null;
        }
    };
}

6. Main app class

Here we just creating sprint context and executing in a loop printMessage method of our messenger.

package com.demien.spring.lifecycle;

import com.demien.spring.lifecycle.beans.Messenger;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App {

    public static void main(String[] args) throws InterruptedException {

        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.register(AppConfig.class);
        ctx.refresh();
        Messenger messenger = ctx.getBean(Messenger.class);

        System.out.println();
        System.out.println("Execution started");
        while (true) {
            Thread.sleep(3000);
            messenger.printMessage();
        }

    }
}


7. Execution

Now we can run our application:

Constructor: 
NULL, null [21:27:53.542]
Init: 
Hello, Mosxrak [21:27:53.553]
Setup: 
Hello, Mosxrak! [21:27:53.678]

Execution started

Hello, Mosxrak! [21:27:56.682]

Hello, Mosxrak! [21:27:59.683]

Hello, Mosxrak! [21:28:02.683]

Here we can see all phases of bean lifecycle in spring:
- first of all, of course, constructor is executed. On this phase nothing was injected into our bean, that is why we have nulls.
- init method was executed when spring created the bean. So bean has message and also post processor generated name
- the last method is "setup" - it was executed when the whole context was created and refreshed. 

8. Turning on profiling 

Now let's test how profiling works. We have to connect JMX client (for example JConsole, or VisualVM) and connect it to our application.



In Profiling/Settings we can see our flag "enaled" - here we have to enter "true" and press "enter".
After this operation output will be changed:


Hello, Cswwjksic! [21:37:28.926]

Hello, Cswwjksic! [21:37:31.926]

Hello, Cswwjksic! [21:37:34.926]

Hello, Cswwjksic! [21:37:37.928]
exec time:1931750

Hello, Cswwjksic! [21:37:40.929]
exec time:235493

Hello, Cswwjksic! [21:37:43.929]
exec time:184623

Hello, Cswwjksic! [21:37:46.930]
exec time:340191

Hello, Cswwjksic! [21:37:49.930]
exec time:358737


After turning on our profiler prints execution time - so everything works as expected.


9. The end

Full source code can be downloaded from here

No comments:

Post a Comment