I customized a Annotation @CustomizedAutowired like @Autowired by using BeanPostProcessor (InjectBeanPostProcessor.java), but I got a NullPointerException when AOP is used.
- Why it is null when using AOP?
- Why DemoController seems to be proxied twice when using AOP?
- what should I do, so that @CustomizedAutowired can work just like @Autowired?
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Inherited
public @interface CustomizedAutowired {}
@RestController
@RequestMapping("/hello")
public class DemoController {
@CustomizedAutowired
private InjectBean injectBean;
@GetMapping("/world")
public LocalDateTime hello() {
injectBean.hello(); // injectBean is null
return LocalDateTime.now();
}
}
@Aspect
@Component
public class AopDemo {
@Pointcut("execution(public java.time.LocalDateTime *(..))")
public void pointcut() {}
@AfterReturning(pointcut = "pointcut()")
public void round() {
System.out.println("after returning");
}
}
@Component
public class InjectBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Class<?> targetClass = bean.getClass();
while (targetClass != null) {
Field[] fields = targetClass.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(CustomizedAutowired.class)) {
field.setAccessible(true);
try {
field.set(bean, new InjectBean());
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
targetClass = targetClass.getSuperclass();
}
return bean;
}
}
@SpringBootApplication
public class DemoApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@CustomizedAutowired
private InjectBean injectBean;
@Override
public void run(String... args) throws Exception {
System.out.println("instance -> " this);
injectBean.hello(); // works fine here
}
}
Here is the result:
CodePudding user response:
You cannot do field.set(bean, new InjectBean())
on the proxy bean, because it does not inherit any private fields. You need to unwrap the proxy and set the field on the original object.
I am not going to comment on the idea of using all that ugly reflection in order to implement your custom injection idea, just help you make it work. You can use AopTestUtils.getTargetObject(bean)
instead of your while-loop in order to get the original object and then easily its class afterwards.
How about this?
package de.scrum_master.spring.q70408968;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.test.util.AopTestUtils;
import java.lang.reflect.Field;
@Component
public class InjectBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// Unwrap bean, in case it is a proxy
Object targetObject = AopTestUtils.getTargetObject(bean);
Class<?> targetClass = targetObject.getClass();
Field[] fields = targetClass.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(CustomizedAutowired.class)) {
field.setAccessible(true);
try {
field.set(targetObject, new InjectBean());
}
catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return bean;
}
}
This is going to work both with and without AOP proxies. Note how I am injecting the field into targetObject
, but returning the original bean
instance (i.e. the proxy in the AOP case).
Then, get rid of the annotated member in the application class, because the application is not a normal Spring component.
package de.scrum_master.spring.q70408968;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
try (ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args)) {
context.getBean(DemoController.class).hello();
}
}
}
Now the application runs just fine.
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
d.s.spring.q70408968.DemoApplication : Started DemoApplication in 7.457 seconds (JVM running for 10.318)
Hello from InjectBean
after returning
Or maybe you prefer a Java streams approach, but that is just cosmetics:
package de.scrum_master.spring.q70408968;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.test.util.AopTestUtils;
import java.util.Arrays;
@Component
public class InjectBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// Unwrap bean, in case it is a proxy
Object targetObject = AopTestUtils.getTargetObject(bean);
Arrays.stream(targetObject.getClass().getDeclaredFields())
.filter(field -> field.isAnnotationPresent(CustomizedAutowired.class))
.forEach(field -> {
field.setAccessible(true);
try {
field.set(targetObject, new InjectBean());
}
catch (IllegalAccessException e) { e.printStackTrace(); }
});
return bean;
}
}
CodePudding user response:
After searching from google, I found that the reason why NPE happens is that we got the wrong targetObject, BeanPostProcessor.postProcessAfterInitialization(Object bean, String beanName)
gives us a proxied bean, and if we use AOP, it will be proxied twice, replace postProcessAfterInitialization
with postProcessBeforeInitialization
can solve this problem, or another solution that can do the Injection Operation before being proxied by AOP.