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