+9

Spring Boot proxy mechanism

Sring AOP (Aspect Oriented Programming)

image.png

image.png

AOP về cơ bản dùng để trả lời câu hỏi sau:

Liệu có thể thay đổi flow của một chương trình mà hạn chế gần như thấp nhất việc thay đổi cấu trúc của chương trình?

Có thể nói AOP là một mô hình được phát triển nhằm tăng hiệu quả và bổ sung khả năng tái sử dụng lại mã nguồn cho hạn chế mà OOP đang gặp phải.

Khi dùng OOP bạn sẽ chia phần mềm thành những phần nhỏ nhất, có tính năng riêng biệt tạm gọi là blackbox , sau đó lắp ghép lại để thành một thể thống nhất, việc này giúp bạn dễ bảo trì và quản lí , tuy nhiên một vấn đề khác nảy sinh đó là khi bạn gặp phải sự thay đổi hoặc muốn tận dụng lại dự án cũ để thay đổi thành sản phẩm mới thì lại gặp khó khăn, vì dù chia ra nhỏ nhất, tuy nhiên các blackbox lại được gắn chặt với nhau ( nối cứng ) nên khi muốn thay đổi bạn phải tìm cách tháo ra , điều này làm bạn phải thay đổi cấu trúc mã nguồn tăng nguy cơ gặp lỗi và độ khó của dự án cao thì việc này càng phức tạp. Điều này có thể thấy rõ thông qua việc implements interface. Nếu cần chỉnh sửa interface trong lúc có hàng ngàn class implements đó thì quả thực là một rắc rối lớn. AOP giúp bạn không cần phải thay đổi blackbox mà vẫn có thể thêm tính năng vào cho nó.

Việc sử dụng AOP khiến Spring Boot trở thành một framework đơn giản, dễ dùng vì che giấu được những logic phức tạp ở bên dưới, ví dụ như xử lý database transaction với @Transactional hay lazy-loading, logging. Trong các nghiệp vụ thông thường chúng ta ít khi cần dùng đến Spring AOP, tuy nhiên kỹ thuật này được dùng trong các framework rất nhiều, vì vậy đôi lúc việc nắm được cơ chế hoạt động bên dưới lại trở nên cần thiết.

Dynamic Agent vs Static Agent

image.png

Spring AOP sử dụng kỹ thuật proxying (tương tự Proxy design pattern). Kỹ thuật này tạo ra một middeman (agent) wrap around blackbox để xử lý additional logic.

Có hai loại agent là static agent (compile time)dynamic agent (runtime).

Có hai cách để thêm additional logic vào method đó chính là thông qua:

  • Proxy creation
  • Bytecode manipulation (cglib, asm, javassist, bcel).

Với static agent chúng ta tạo ra proxy class tại compile time. Hãy xem code ví dụ sau đây để hiểu rõ hơn.

Thứ tự các bước cần làm:

  1. Tạo interface và implemetation class.

    Action.java

    public interface Action {
        void doSomething();
    }
    

    RealObject.java

    public class RealObject implements Action {
    
        @Override
        public void doSomething() {
            System.out.println("do something");
        }
    }
    
  2. Tạo proxy class cũng implement interface.

    StaticAgentHandler.java

    public class StaticAgentHandler implements Action {
    
        private final Action realObject;
    
        public StaticAgentHandler(Action realObject) {
            this.realObject = realObject;
        }
    
        @Override
        public void doSomething() {
            System.out.print("proxy do: ");
            realObject.doSomething();
        }
    }
    
  3. Inject target object vào proxy class và gọi override method từ proxy class trong target class.

     public static void main(String[] args) {
        StaticAgentHandler staticAgent = new StaticAgentHandler(new RealObject());
        staticAgent.doSomething();
    }
    

Chúng ta có thể add thêm log cho method doSomeThing() của đối tượng RealObject bằng static agent như trên, tuy nhiên cách làm này có một hạn chế là mỗi agent chỉ có thể đi kèm một interface. Khi số lượng interface nhiều lên thì số agent cũng phải tăng lên tương ứng. Điều này khá bất tiện trong trường hợp chúng ta muốn xử lý một logic như nhau cho nhiều interface khác nhau. Đó là lý do mà dynamic agent (JDK dynamic proxy) được ra đời từ JDK 1.3 để giải quyết vấn đề trên.

Có hai cách để implement dynamic agent trong Spring AOP đó chính là:

  • Dynamic agent based on JDK (built-in)
  • Dynamic agent based on CGLib (Code Generation Library - external library)

JDK dynamic proxy vs CGLib proxy

image.png

JDK dynamic proxy được tạo ra khi và chỉ khi thông qua interface

image.png

DynamicAgentHandler.java

public class DynamicAgentHandler implements InvocationHandler {

    private final Object realObject;

    public DynamicAgentHandler(Object realObject) {
        this.realObject = realObject;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //Agent extension logic
        System.out.print("proxy do: ");

        return method.invoke(realObject, args);
    }
    
    //JDK dynamic proxy require interface Action.class.
    public static void main(String[] args) {
        System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        RealObject realObject = new RealObject();
        
        //Đoạn code này tạo một instance của lớp proxy (Ex: $Proxy0) và cast nó về Action.class.
        Action dynamicAgent = (Action) java.lang.reflect.Proxy.newProxyInstance(
                ClassLoader.getSystemClassLoader(),
                new Class[]{Action.class},
                new DynamicAgentHandler(realObject)
        );
        dynamicAgent.doSomething();
    }
}

JDK dynamic proxy sử dụng kỹ thuật proxy creation tạo ra proxy class com.sun.proxy.$Proxy0 tại runtime thông qua Java Reflection - java.lang.reflect.Proxy.

$Proxy0.class được tạo ra tại runtime.

public class $Proxy0 extends Proxy implements Action {
    
    // Constructor
    public $Proxy0(InvocationHandler h) {
        super(h);
    }

    // Implementation của phương thức từ interface Action
    @Override
    public void doSomething() {
        try {
            Method method = Action.class.getMethod("doSomething");
            super.h.invoke(this, method, null);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

Khi một phương thức được gọi trên proxy thì JVM gọi đến method invoke() của DynamicAgentHandler trước, sau đó mới gọi method của realObject.

image.png

Kỹ thuật này có một hạn chế là chỉ support interface, cho nên với trường hợp concrete class thì không có cách nào tạo proxy được, bởi vì class $Proxy0 được tạo ra đã extends class Proxy rồi mà java lại không hỗ trợ multiple extends.

public static Object newProxyInstance(ClassLoader loader, Class<? >[] interfaces, InvocationHandler h) throws IllegalArgumentException
{
    ......
}

Để xử lý trường hợp này, chúng ta cần sử dụng thêm kỹ thuật byte code manipulation để tạo in-memory proxy class và load vào chương trình đang chạy thông qua một số thư viện khác như CGlib hay ASM.

CGLib proxy được tạo ra thông qua subclassing

image.png

CglibDynamicAgentHandler.java

public class CglibDynamicAgentHandler implements MethodInterceptor {

    public Object getInstance(Object target) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.print("proxy do: ");
        return proxy.invokeSuper(obj, args);
    }
    
    //CGLIB proxy isn't require interface.
    public static void main(String[] args) {
        CglibDynamicAgentHandler cglibDynamicAgent = new CglibDynamicAgentHandler();
        RealObject instance = (RealObject) cglibDynamicAgent.getInstance(realObject);
        instance.doSomething();
    }
}

Code example

Chúng ta sẽ đến với một ví dụ thực tế trong Spring Boot để hiểu rõ hơn cơ chế hoạt động của Spring AOP bằng cách viết một REST API và quan sát xem cách bean được tạo ra thông qua proxy như thế nào. Cấu trúc dự án và code demo như sau:

image.png

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.2'
    id 'io.spring.dependency-management' version '1.0.12.RELEASE'
}

group = 'com.logbasex.ioc-di'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
}

test {
    useJUnitPlatform()
}

application.yml

server:
  port: 8282

spring:
  data:
    mongodb:
      database: config
      uri: mongodb://localhost/local

UserApplication.java

@SpringBootApplication
public class UserApplication {
	public static void main(String[] args) {
		SpringApplication.run(UserApplication.class, args);
	}
}

UserController.java

@RestController
public class UserController {
	
	@Autowired
	private UserServiceImpl userImplSv;
	
	@Autowired
	private UserService userSv;
    
    @Autowired
	private UserRepository userRepo;
	
	@GetMapping("/hello")
	public void hello() {
		//iUserService DEFAULT is wrap around using CGLIB proxy, debug for detail information.
		//if you want to use JDK proxy, please set: proxy-target-class = false
		iUserService.hello();
        userRepo.findAll();
		userServiceImpl.hello();
	}
}

User.java

public class User {
	private String id;
	private String name;
	
	public String getId() {
		return id;
	}
	
	public void setId(String id) {
		this.id = id;
	}
	
	public String getName() {
		return name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
}

UserService.java

public interface UserService {
	String sayHello();
}

UserServiceImpl.java

@Service
public class UserServiceImpl implements UserService {
	
	@Override
	public String sayHello() {
		return "Hello";
	}
}

LogAspect.java

//if we don't create this class, spring do not generate proxy.
@Aspect
@Component
@EnableAspectJAutoProxy
public class LogAspect {
	private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

	@Before("execution(* com.logbasex.aop.service.UserServiceImpl.*(..))")
	public void before(JoinPoint jp) {
		log.info("jp.getSignature().getName() = {}", jp.getSignature().getName());
	}
}

Demo

Spring default proxy configuration

image.png

AOP auto configuration sẽ được thực thi tạo khi thuộc tính spring.aop.auto có giá trị bằng true

image.png

Chúng ta có thể thấy Spring AOP default (hay khi thuộc tính spring.aop.proxy-target-class có giá trị bằng true ) thì sẽ sử dụng CGLIB proxy, còn khi thuộc tính spring.aop.proxy-target-class có giá trị bằng false thì JDK dynamic proxy sẽ được sử dụng.

@Configuration(proxyBeanMethods = false) có nghĩa rằng by default thì khi gọi bean method sẽ không gọi qua proxy. Do đó nếu chúng ta comment class LogAspect.java (which is enable proxyBeanMethod) lại thì sẽ cho ra kết quả như sau:

image.png

Còn nếu chúng ta set thuộc tính proxyBeanMethods = true thông qua việc enable AOP config sử dụng LogAspect.java hoặc thêm @Configuration cho class UserServiceImpl.java thì mỗi lần gọi vào bean method sẽ đi qua CGLIB proxy:

image.png

image.png

Với đoạn code trên nếu bạn set thuộc tính spring.aop.proxy-target-class = false thì khi build lại sẽ bị lỗi, bởi vì JDK dynamic proxy chỉ proxy được interface.

image.png

image.png

Một trường hợp khác đó chính là CGLIB proxy sử dụng cơ chế subclassing, nên nếu UserServiceImpl.java được đánh dấu là final thì build cũng sẽ bị lỗi.

Additional infomation

Có một vài vấn đề với JDK dynamic proxy:

image.png

Vì thế nên từ version Spring Boot 2.0, CGLIB proxying là phương thức mặc định được sử dụng. Có một vài vấn đề về performance như phải gọi constructor 2 lần cho target object và proxy object nhưng đã được giải quyết triệt để trong các phiên bản sau đó.

References


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.