slim slim - 10 days ago 5
Java Question

Spring AOP on objects created by factories which are beans

I have a piece of Spring AOP advice which is not triggering when the auth(..) method is called:

@AfterReturning(pointcut="execution(* auth(..))",
returning="result")
public void trigger(final Boolean result) {
System.out.println("I was triggered " + result);
}


This is because the Authenticator object with the Auth method is not a Spring bean, but it is created by a factory which is a Spring bean:

<bean id="passwordUserAuthFact" class="foo.bar.PasswordAuthFactory">
...
</bean>

<bean id="server" class="foo.bar.Server">
<property name="authFactory" ref="passwordUserAuthFact"/>
...
</bean>


So when the application does:

session.authenticator = server.getAuthFactory().create();


...
session.authenticator
is an unproxied plain PasswordAuthFactory, and AOP isn't going to happen on it.

It seems to me that it should be possible to tell Spring about beans which are factories, so that it can wrap the factory in a proxy, such that objects returned by
create()
are themselves wrapped in an AOP proxy.

That is, Spring would dynamically create a wrapper around my factory like this:

public Foo create() {
Foo instance = target.create();
return wrapWithAopProxy(instance);
}


But I can't see a ready-made way to do that.

I would prefer not to make my classes Spring-aware. The factory class itself is third-party, so I would prefer a solution in which I don't change its source.

Is there a common pattern to achieve this? Or a better approach to the problem?

Answer

You main alternative, would be to go with programmatic aspect definition since you cannot play with the sources.

Implement a new authentication factory decorator that wraps your old authentication factory. The former would mainly delegate authenticator creation to the latter, then wraps the returned object with a ProxyBean and register the needed advice(s).

Sprig enables these beans to be loosely coupled and the authenticator POJO a non-managed bean. In below samples, I only aim to provide a glance view on how this can be done leaving implementation details up to your application:

  • Here down a fake Authenticator:

    public class Authenticator
    {
        private String name;
    
        public Authenticator( String name )
        {
            this.name = name;
        }
    
        public void authenticate( Object subject )
        {
            System.out.println( subject + " is being authenticated by: " + name );
        }
    }
    
  • Supposing that your AuthFactory interface looks like below:

    public interface AuthFactory 
    {
        Authenticator create();
    }
    
  • It's legacy implementation, the one providing non managed authentication is as follows:

    public class AuthenticationFactory implements AuthFactory
    {
        @Override
        public Authenticator create()
        {
            return new Authenticator("mocked-authenticator");
        }
    }
    
  • Create a new MethodInterceptor (note that you may need aopalliance dependency) that encapsulates your advice logic:

    public class AuthenticationAdvise implements MethodInterceptor
    {
        @Override
        public Object invoke( MethodInvocation methodInvocation ) throws Throwable
        {
            System.out.println("Before invocation..."); // Your advice logic goes here
            return methodInvocation.proceed();
        }
    }
    
  • Create a new authentication provider decorator, which will be a Spring managed bean:

    public class AuthenticationFactoryDecorator implements AuthFactory {
    
        private AuthFactory authenticationFactoryDelegate;
    
        private MethodInterceptor interceptor;
    
        public AuthenticationFactoryDecorator( final AuthFactory authenticationFactoryDelegate, final MethodInterceptor interceptor )
        {
            this.authenticationFactoryDelegate = authenticationFactoryDelegate;
            this.interceptor = interceptor;
        }
    
        @Override
        public Authenticator create()
        {
             // Create the needed pointcut
            NameMatchMethodPointcut pc = new NameMatchMethodPointcut();
            pc.addMethodName("authenticate");
            // Get an authenticator from your legacy class
            Authenticator auth = authenticationFactoryDelegate.create();
            // Create a new Proxy wrapping your authFactory with the needed pointcut and advice
            ProxyFactory proxyFactory = new ProxyFactory(auth);
            proxyFactory.addAdvice(interceptor);
            return (Authenticator) proxyFactory.getProxy();
        }
    }
    
  • Register the new AuthFactory bean as a Spring component and wire it with your advice and legacy factory beans (e.g. beans.xml):

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <bean id="legacyAuthFactory" class="some.package.AuthenticationFactory"/>
    
        <bean id="interceptor" class="some.package.AuthenticationFactoryAdvise"/>
    
        <bean id="authFactory" class="some.package.AuthenticationFactoryDecorator">
            <constructor-arg name="authenticationFactoryDelegate" ref="legacyAuthFactory"/>
            <constructor-arg name="interceptor" ref="interceptor"/>
        </bean>
    
    </beans>
    

Now your are free to pull the authFactory bean from the Spring Application Context and use it to instantiate new authenticator objects:

public class MainAuthentication
{
    public static void main( String[] args )
    {
        ApplicationContext  applicationContext = new ClassPathXmlApplicationContext("classpath:META-INF/beans.xml");
        AuthFactory factory = applicationContext.getBean("authFactory", AuthFactory.class);
        Authenticator authenticator = factory.create();
        authenticator.authenticate(new MockSubject());
    }

    private static class MockSubject
    {
        @Override
        public String toString() {
            return "MockSubject";
        }
    }
}

Executing below main class, note that you are dealing with a new proxied authenticator instance which is wrapped with your advice logic, as the output shows:

Before invocation...

MockSubject is being authenticated!!!