Spring - Intercepting bean creation and injecting custom proxy

Sotirios Delimanolis picture Sotirios Delimanolis · Mar 24, 2013 · Viewed 14.9k times · Source

I have a @Controller with @Autowired fields and handler methods that I want to annotate with custom annotations.

For example,

@Controller
public class MyController{
    @Autowired
    public MyDao myDao;

    @RequestMapping("/home")
    @OnlyIfXYZ
    public String onlyForXYZ() {
        // do something
        return "xyz";
    }
}

Where @OnlyIfXYZ is an example of a custom annotation. I was thinking I would intercept the Controller bean creation, pass my own CGLIB proxy on which Spring can then set properties, like the autowired field.

I tried using a InstantiationAwareBeanPostProcessor but that solution doesn't work great because postProcessBeforeInstantiation() short-circuits the rest of the process. I tried with postProcessAfterInitialization(), like below

public class MyProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        // Here the bean autowired fields are already set
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object aBean, String aBeanName) throws BeansException {
        Class<?> clazz = aBean.getClass();
        // only for Controllers, possibly only those with my custom annotation on them
        if (!clazz.isAnnotationPresent(Controller.class))
            return aBean;

        Object proxy = Enhancer.create(clazz, new MyMethodInterceptor());
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            try {
                // get the field and copy it over to the proxy
                Object objectToCopy = field.get(aBean);
                field.set(proxy, objectToCopy);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                return aBean;
            }
        }   
        return proxy;
    }
}

This solution uses reflection to copy over all the fields of the target bean to the proxy bean (kind of hacky for my taste). But I don't have access to the HttpServletRequest and HttpServletResponse objects if those aren't arguments in the method I'm intercepting.

Is there another callback I can inject into Spring bean creation logic to inject my own Proxy Controller before Spring populates its properties? I need to be able to access the HttpServletRequest and HttpServletResponse objects regardless of if the Controller handler method has it in its definition, ie. as arguments.

N.B The @Autowired field is also a proxy, it is annotated with @Transactional so Spring proxies it up.

EDIT: The AOP solution works nicely for intercepting the method invocation, but I can't find a way to access the HttpServletRequest and HttpServletResponse objects, if they aren't already method arguments.

I'm probably going to end up using HandlerInterceptorAdapter, but I was hoping I can do it with OOP so as to not add the overhead to methods that don't need it.

Answer

nicholas.hauschild picture nicholas.hauschild · Mar 24, 2013

Take a look at Spring AOP. It has exactly the facilities you are after. For your example, you could do something like this:

@Aspect
@Component
public class MyAspect {
    @Around("@annotation(path.to.your.annotation.OnlyIfXYZ)")
    public Object onlyIfXyz(final ProceedingJoinPoint pjp) throws Exception {
        //do some stuff before invoking methods annotated with @OnlyIfXYZ
        final Object returnValue = pjp.proceed();
        //do some stuff after invoking methods annotated with @OnlyIfXYZ
        return returnValue;
    }
}

It is worth noting that Spring will only apply the proxy to classes that are a part of its application context. (which it appears is the case in your example)

You can also use Spring AOP to bind parameters to your aspect method. This can be done in various ways, but the one you are after is probably args(paramName).

@Aspect
@Component
public class MyAspect2 {
    @Around("@annotation(path.to.your.annotation.OnlyIfXYZ) && " +
        "args(..,request,..)")
    public Object onlyIfXyzAndHasHttpServletRequest(final ProceedingJoinPoint pjp,
            final HttpServletRequest request) throws Exception {
        //do some stuff before invoking methods annotated with @OnlyIfXYZ
        //do something special with your HttpServletRequest
        final Object returnValue = pjp.proceed();
        //do some stuff after invoking methods annotated with @OnlyIfXYZ
        //do more special things with your HttpServletRequest
        return returnValue;
    }
}

This aspect should do a part of what you are after. It will proxy methods annotated with @OnlyIfXYZ that ALSO take in a HttpServletRequest as a parameter. Further, it will bind this HttpServletRequest into the Aspect method as a passed in parameter.

I understand that you are after potentially both HttpServletRequest and HttpServletResponse, so you should be able to modify the args expression to take in both request and response.