From agent to AOP, how to write an AOP framework?

Posted by Clinger on Sun, 30 Jan 2022 04:05:52 +0100

background

AOP (aspect oriented programming), as a supplement to OOP (object oriented programming), provides another programming idea of thinking about program structure. The modular unit of OOP is Class, while the modular unit of AOP is aspect. Aspect modularizes concerns across multiple classes, such as logs, transactions, and so on. This article starts from agent, gradually transitions to AOP, and implements a simple AOP framework, aiming to deepen our own and everyone's understanding of AOP.

scene

In daily development, we usually need to print the log. On the log, we can print the parameters and return values of the method, and count the execution time of the method. In the simplest way, we may use the following similar code.

@Slf4j
public interface IService {
    String doSomething(String param);
}

@Slf4j
public class ServiceImpl implements IService{

    @Override
    public String doSomething(String param) {
        long startTime = System.currentTimeMillis();
        log.info("Service#doSomething starts to execute. The parameters are: {} ", param);

        // Specific business logic
        String result = detailBiz();

        long costTime = System.currentTimeMillis() - startTime;
        log.info("Service#doSomething ends execution. The result is: {}, execution duration: {} ms", result, costTime);

        return result;
    }

    private String detailBiz() {
        return null;
    }
}

If there are few methods to print logs, there is no problem for us to add the code to print logs before and after the execution of the business code of each method. However, if there are many methods to print logs, there is no doubt that this method will greatly increase our workload, and it will be very time-consuming and laborious to modify the logic of log printing.

In the above code, printing logs is the focus of our non business functions, which are usually distributed throughout the project. In order to reduce duplicate code and increase scalability and maintainability, we can use agents to isolate business code from non business code. There are many ways to implement proxy in Java. For details, see my previous article Several ways to create agents in Java . If the business class implements the interface, we can choose JDK dynamic proxy to avoid manually creating more static proxy classes. Use JDK dynamic agent to process log printing. The code is as follows.

public interface IService {
    String doSomething(String param);
}

public class ServiceImpl implements IService{

    @Override
    public String doSomething(String param) {
        // Specific business logic
        String result = detailBiz();
        return result;
    }

    private String detailBiz() {
        return null;
    }
}

@Slf4j
public class LogProxy {

    private Object obj;

    public LogProxy(Object obj) {
        this.obj = obj;
    }

    public Object createProxy() {
        ClassLoader classLoader = obj.getClass().getClassLoader();
        Class<?>[] interfaces = obj.getClass().getInterfaces();
        return Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // Search method
                if (method.getDeclaringClass() == IService.class &&
                        "doSomething".equals(method.getName()) &&
                        method.getParameterCount() == 1 &&
                        method.getParameterTypes()[0] == String.class) {

                    // Before method execution
                    long startTime = System.currentTimeMillis();
                    log.info("Service#doSomething starts to execute. The parameters are: {} ", args[0];

                    // Execution method
                    String result = (String) method.invoke(obj, args);

                    // After method execution
                    long costTime = System.currentTimeMillis() - startTime;
                    log.info("Service#doSomething ends execution. The result is: {}, execution duration: {} ms", result, costTime);

                    return result;
                }
                return null;
            }
        });
    }
}

The above code creates the proxy object of the given instance, filters out the methods we are interested in, and adds our own log printing code before and after the method is executed.

AOP terminology

In the above code of using agent to print logs, although the concept of AOP is not proposed, in fact, we have applied AOP to our log printing function. The terms of AOP are explained in combination with the above example.

Joint Point

Joint Point is a point of program execution, including constructor execution, method execution, field assignment, etc. it is the interception point for us to execute additional code, such as the execution of IService#doSomething method in the above example. As a static language, Java cannot be modified once its structure is defined. It can only intercept the execution of methods at runtime, which can help us complete most functions. If you need to intercept the constructor or field, you need a special compiler to generate the relevant bytecode during compilation.

Pointcut

Pointcut is used to filter out the joint points we are interested in. Joint Point contains many methods, so which method are we interested in and need to do additional logic? This requires pointcut filtering. As in the above example, we filter out the IService#doSomething method, and the log will be printed only when this method is executed.

Advice

Advice is an action executed at a specific Joint Point, which can be subdivided into Around Advice, Before Advice and After Advice. In the example, the code that prints the log before and after the execution of the business code is advice.

Introduction

Introduction, also known as inter type declaration, is used to add additional interfaces to proxy objects. Daily development use is relatively small.

Aspect

Aspect is the modularization of the concerns of multiple classes across domains. For example, in the example, we focus on the log printing of the parameters and return values of the methods executed on multiple classes. It is very similar to class in OOP. Class is used to modularize code, including member variables, constructors, methods, etc., while aspect includes Introduction, Pointcut and Advice. In the example, we do not provide the aspect class.

Write an AOP framework

Why should we use a non generic AOP framework to extract functions? In the example, an agent factory is provided to create agents. However, this agent factory is relatively customized. The filtering of interception methods and the actions performed after filtering are fixed. If there are other requirements, this agent factory cannot meet them. AOP framework provides relatively general and extensible functions to meet our different non business requirements. Next, the agent factory in the sample is transformed to implement a relatively simple AOP framework.

Target object

The proxy object created by the agent factory usually corresponds to a target object. If the current call method is not a Advice concern method, it will call the method directly with the target object. In addition to the interface of the target object itself, the created proxy class can also flexibly implement the user-defined interface. Therefore, ProxyFactory can be transformed as follows.

public class ProxyFactory {

    // Target object
    private Object target;

    // Custom implemented interface
    private List<Class<?>> interfaces = new ArrayList<>();

    public ProxyFactory(Object target) {
        this.target = target;
    }

    public void addInterface(Class<?> ifc) {
        this.interfaces.add(ifc);
    }

    public Object createProxy() {
        ClassLoader classLoader = this.getClass().getClassLoader();
        List<Class<?>> interfaces = new ArrayList<>(this.interfaces);
        interfaces.addAll(Arrays.asList(this.target.getClass().getInterfaces()));
        return Proxy.newProxyInstance(classLoader, interfaces.toArray(new Class[0]), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return null;
            }
        });
    }
}

Method screening

Because the agent is created at run time, currently there are only methods as joint points. Pointcut filters the method and intercepts it if the conditions are met. The code is as follows.

public interface Pointcut {
    // Whether to match the method. Only the matching method is intercepted
    boolean match(Method method);  
}

Execute action

The action to be performed is Advice. Here we only consider Before Advice and After Advice for the time being. The code is as follows.

public interface Advice {
}

public interface BeforeAdvice extends Advice {
    // The target method is executed before execution
    void before(Object proxy, Method method, Object[] args);
}

public interface AfterAdvice extends Advice {
    // Execute after target method execution
    void after(Object result, Object proxy, Method method, Object[] args);
}

Because different interception methods need to perform different actions, Pointcut and Advice also need to be integrated.

public interface PointcutBeforeAdvice extends Pointcut, BeforeAdvice {
}

public interface PointcutAfterAdvice extends Pointcut, AfterAdvice {
}

Finally, modify ProxyFactory as follows.

public class ProxyFactory {

    // Target object
    private Object target;

    // Custom implemented interface
    private List<Class<?>> interfaces = new ArrayList<>();

    // Execute before target method execution
    private List<PointcutBeforeAdvice> beforeAdviceList = new ArrayList<>();

    // Execute after target method execution
    private List<PointcutAfterAdvice> afterAdviceList = new ArrayList<>();

    public ProxyFactory(Object target) {
        this.target = target;
    }

    public void addInterface(Class<?> ifc) {
        this.interfaces.add(ifc);
    }


    public void addAdvice(Advice advice) {
        if (advice instanceof PointcutBeforeAdvice) {
            beforeAdviceList.add((PointcutBeforeAdvice) advice);
        } else if (advice instanceof PointcutAfterAdvice) {
            afterAdviceList.add((PointcutAfterAdvice) advice);
        }
    }

    public Object createProxy() {
        ClassLoader classLoader = this.getClass().getClassLoader();
        List<Class<?>> interfaces = new ArrayList<>(this.interfaces);
        interfaces.addAll(Arrays.asList(this.target.getClass().getInterfaces()));
        return Proxy.newProxyInstance(classLoader, interfaces.toArray(new Class[0]), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                beforeAdviceList.stream().filter(advice -> advice.match(method)).forEach(advice -> advice.before(proxy, method, args));
                Object result = method.invoke(target, args);
                afterAdviceList.stream().filter(advice -> advice.match(method)).forEach(advice -> advice.after(result, proxy, method, args));
                return result;
            }
        });
    }
}

section

Up to now, the AOP proxy framework has been basically completed. This is to use the framework through API. What needs to be done is to integrate Pointcut and Advice into Aspect. A common approach is to read the configuration through annotations or XML, and then create an agent. For annotations, Spring uses AspectJ annotations. Interested partners can consult relevant materials by themselves.

summary

This article introduces AOP from agent. In addition to interpreting the terms in AOP, it also implements an extremely simple AOP framework. In fact, this framework is also extremely imperfect. For example, if the target object of the agent does not implement the interface, we need to switch to cglib to create the agent, do not implement Around Advice, do not implement Throwing Advice, and so on. This article aims to lead you to understand AOP. For more detailed AOP content, you can refer to the implementation of Spring AOP or AspectJ.

Topics: Java AOP