Spring - Intercepting bean creation and injecting custom proxy

16,951

Solution 1

Taking into account your comment under the question all you need is HandlerInterceptor.

http://static.springsource.org/spring/docs/3.2.x/javadoc-api/org/springframework/web/servlet/HandlerInterceptor.html

You need to implement that interface and add it to your configuration, for example:

<mvc:interceptors>
    <bean id="customInterceptor" class="com.example.interceptors.CustomInterceptor"/>
</mvc:interceptors>

This interface provides method preHanlde, which has request, response and HandlerMethod. To check if the method is annotated just try this:

HandlerMethod method = (HandlerMethod) handler;
OnlyIfXYZ customAnnotation = method.getMethodAnnotation(OnlyIfXYZ.class);

Solution 2

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.

Solution 3

I think that not, but I supose that you could autowire the proxy after creating it.

public class MyProcessor extends InstantiationAwareBeanPostProcessorAdapter
    implements BeanFactoryAware {

    private AutowireCapableBeanFactory beanFactory;

     @Override
        public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
            // This is where I thought I would do it, but it then skips setting fields alltogether
            if (beanClass.isAnnotationPresent(Controller.class)) {
                Object proxy = Enhancer.create(beanClass, new MyInterceptor());
                // autowire
                beanFactory.autowireBean(proxy);

                return proxy;
            }
            return null;
        }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = (AutowireCapableBeanFactory) beanFactory;

    }

}

Other alternative is to create a Spring AOP Proxy (using ProxyFactory) in postProcessAfterInitialization method. For this way AbstractAutoProxyCreator could be useful. See BeanNameAutoProxyCreator as sample. But imho, an annotation pointcut (Nicholas answer) do the same and is simpler.

Solution 4

InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation will short-circuit the bean creation approach. The only processing applied is postProcessAfterInitialization. Which means that, autowiring won't happen because AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues will never be called. Therefore, you should manually inject or autowire the properties of the proxied beans in postProcessAfterInitialization method.

Question: Does moving the proxying logic in postProcessAfterInitialization method have an impact to your business requirements? If none, I suggest you do the proxying there.

FYI: If you are not building an API, do the annotation approach as suggested by @nicholas.hauschild.

Share:
16,951

Related videos on Youtube

Sotirios Delimanolis
Author by

Sotirios Delimanolis

Couple posts on Medium: When you think you found a bug in TCP, don’t get cocky, kid. The other side of Stack Overflow content moderation

Updated on September 15, 2022

Comments

  • Sotirios Delimanolis
    Sotirios Delimanolis over 1 year

    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.

    • Sotirios Delimanolis
      Sotirios Delimanolis about 10 years
      These downvotes they come, but they are not explained.
  • Sotirios Delimanolis
    Sotirios Delimanolis about 11 years
    I was looking into that and it probably works, but I dislike the fact that you are hardcoding the around advice with a String. If the annotation took a Class argument instead, I would be much happier.
  • nicholas.hauschild
    nicholas.hauschild about 11 years
    A hardcoded Class vs a hardcoded String...They are both a part of configuration regardless, which is where most of your 'hardcoded' pieces of code should be found.
  • Sotirios Delimanolis
    Sotirios Delimanolis about 11 years
    Yeah, but with the class, it's easier to refactor.
  • DerMiggel
    DerMiggel about 11 years
    Actually, that's just a matter of the IDE. For instance, when you are using IntelliJ IDEA and change the name/package of a class, such an annotation will be refactored as well.
  • Sotirios Delimanolis
    Sotirios Delimanolis about 11 years
    Then won't there be both my controller and a proxy of it in the context?
  • Jose Luis Martin
    Jose Luis Martin about 11 years
    When autowiring no, for Spring AOP Proxy yes.
  • Sotirios Delimanolis
    Sotirios Delimanolis about 11 years
    I can do it in postProcessAfterInitialization , but to copy over the @Autowired fields, I need to do some reflective copy from the original bean to the proxy bean I'm returning.
  • Sotirios Delimanolis
    Sotirios Delimanolis about 11 years
    This Aspect is advising controller methods that may or may not have Http Request and Response objects as method arguments. Is there any way to have spring inject those into the advice methods, like it does with controllers?
  • nicholas.hauschild
    nicholas.hauschild about 11 years
    @SotiriosDelimanolis I have updated my answer to show how you can bind parameters from your proxied methods into your aspect. Let me know if it is what you are looking for.
  • Sotirios Delimanolis
    Sotirios Delimanolis about 11 years
    @nicholas.hauschild That's the way to do it, but you're limited to arguments already on the method, which is understandable.
  • Sotirios Delimanolis
    Sotirios Delimanolis about 11 years
    I would be using preHandle(), so I can validate before proceeding with controller method.
  • Mateusz Mrozewski
    Mateusz Mrozewski about 11 years
    I have updated the answer to match the requirement. preHandle() shoud solve your problem.
  • Sotirios Delimanolis
    Sotirios Delimanolis about 11 years
    And I still won't have access to the Http request and response objects directly.