Sunday 12 July 2020

Intro to Java cglib

Introduction 


cglib is a powerful, high performance and quality Code Generation Library. It is used to extend JAVA classes and implements interfaces at runtime. See samples and API documentation to learn more about features.
Byte Code Generation Library is high level API to generate and transform JAVA byte code. It is used by AOP, testing, data access frameworks to generate dynamic proxy objects and intercept field access.

Java로 programming을 하는 당신은 적어도 한번은 cglib library를 사용했을 것이다! cglib은 Java bytecode generation library로서 compile time 이후에 class를 조작하거나 생성할 수 있게 한다. bytecode를 추가적으로 장착한다는 맥락에서 bytecode instrumentation library라고도 불린다. cglib을 통해서 대상 class에 대해 dynamic proxy, method intercept가 가능하게 한다. 우리가 널리 사용하는 Spring AOP, Hibernate, Mockito가 cglib을 활용하여 구현되었다. 먼저 간단하게 cglib의 concept를 알아보고 용처에 대해서도 알아보도록 하자. 

Concepts


public class SimpleService {
    
    public String hello(String name) {
        return "Hello~ " + name;    }
    public Integer length(String name) {
        return name.length();    }
}

cglib을 적용할 class는 위와 같다. 대상 class를 subclassing 하기 위해서 cglib의 Enhancer class가 필요하다. Enhancer는 method intercept를 가능하게 하는 dynamic proxy를 생성한다. 

public static SimpleService createFixedValueProxy() {
    Enhancer enhancer = new Enhancer();    enhancer.setSuperclass(SimpleService.class);    enhancer.setCallback((FixedValue) () -> "Hello~ Fixed!");
    return (SimpleService) enhancer.create();}

@Testpublic void testFixedValueProxy() {
    SimpleService proxy = SimpleService.createFixedValueProxy();    assertEquals("Hello~ Fixed!", proxy.hello("abc"));    assertEquals("Hello~ Fixed!", proxy.hello("def"));    assertEquals("Hello~ Fixed!", proxy.hello(anyString()));}

FixedValue는 callback interface로서 proxied method에 대해 고정된 값을 return한다. intercept에 대한 어떤 정보도 제공받지 않는다. 

public static SimpleService createAroundAdviceProxy() {
    Enhancer enhancer = new Enhancer();    enhancer.setSuperclass(SimpleService.class);    enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
        if (method.getDeclaringClass() == Object.class) {
            throw new Exception("declaring class is not a SimpleService");        }
        switch (Method.find(method.getName())) {
            case HELLO:
                return "Hello~ Proxy";            case LENGTH:
                return proxy.invokeSuper(obj, args);            default:
                throw new IllegalArgumentException("cannot find "+ method.getName());        }
    });    return (SimpleService) enhancer.create();}

@Testpublic void testAroundAdviceProxy() {
    SimpleService proxy = SimpleService.createAroundAdviceProxy();    assertEquals("Hello~ Proxy", proxy.hello("hi"));    assertEquals("Hello~ Proxy", proxy.hello("hello"));    assertEquals("Hello~ Proxy", proxy.hello(anyString()));    assertEquals((Integer) 2, proxy.length("hi"));    assertEquals((Integer) 5, proxy.length("hello"));}

cglib을 활용해서 조금 더 동적인 기능을 제공하려면 MethodIntercepter interface를 사용한다. MethodIntercepter는 method, args 등의 정보를 제공 받아서 다양한 처리가 가능하다. 위 code처럼 method signature 별로 처리를 할 수 있다. 

Case Study

Spring AOP

@Before("execution(* com.xyz.myapp.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }

@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }

@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }

Spring AOP에서 제공하는 annotation을 통해서 method의 실행 전, 후, exception 처리 후 시점에 대해 추가적인 처리가 가능하다. Spring AOP에서는 AOP 구현을 위해 JDK dynamic proxy 혹은 cglib을 사용한다. JDK dynamic proxy의 사용이 기본적으로 권장되지만 대상 object가 interface를 가지지 않은 경우 cglib이 사용된다. 이에 대한 내용와 code는 아래 link에서 확인할 수 있다. 

https://docs.spring.io/spring/docs/3.0.0.M3/reference/html/ch08s06.html
https://github.com/spring-projects/spring-framework/blob/v5.1.4.RELEASE/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java

@Transactional 


@Transactional
public void businessLogic() {
... use entity manager inside a transaction ...
}
UserTransaction utx = entityManager.getTransaction(); 

try { 
    utx.begin(); 
    businessLogic();
    utx.commit(); 
} catch(Exception ex) { 
    utx.rollback(); 
    throw ex; 
} 

Spring에서 persistent layer에 대한 transaction 처리를 위해 @Transactional annotation을 사용하는데 이때 cglib이 활용된다. code 상의 @Transactional이 아래 code처럼 transaction begin, commit or rollback이 될 수 있도록 bytecode가 generate 된다. 

Mockito

https://proandroiddev.com/mockito-2-x-over-powermock-migration-tips-and-tricks-top-ten-118c52abd1d8

Mockito 2+ 에서는 더이상 cglib으로 구현이 되어있지 않지만, 이전 버전의 Mockito는 cglib을 통해서 구현되었다. Mockito는 대상 class의 method를 아무런 동작을 하지 않는 empty implementation 형태로 확장하였다. 

Conclusion

Java cglib에 대해서 간단히 알아보았다! Java의 bytecode instrumentation 기법 혹은 dynamic proxy이 cglib을 통해서만 구현되어 있는 것은 아니지만 cglib에서 Enhancer class를 통해 subclassing을 하는 기본 원리는 비슷할 것이다 예상한다. 앞으로 마주칠 다양한 AOP 기법이나 testing framework에 대한 이해를 한층 더 높일 수 있을 것으로 기대해본다. 

https://github.com/taehyeok-jang/cglib-example
위 code는 해당 repository에서 확인할 수 있다. 

References 

- https://github.com/cglib/cglib
- https://www.baeldung.com/cglib
- http://cglib.sourceforge.net/apidocs/net/sf/cglib/Enhancer.html
- https://dzone.com/articles/how-does-spring-transactional
- https://docs.spring.io/spring/docs/3.0.0.M3/reference/html/ch08s06.html
- https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-advice

1 comment: