typeMatchInternal()의 경우 MemberServiceImpl를 표현식에 선언했기 때문에 그 안에 있는 internal(String) 메서드도 매칭 대상이 된다.
typeMatchNoSuperTypeMethodFalse() 를 주의해서 보아야 한다.
이 경우 표현식에 부모 타입인 MemberService 를 선언했다.
그런데 자식 타입인 MemberServiceImpl 의
internal(String) 메서드를 매칭하려 한다.
이 경우 매칭에 실패한다.
MemberService 에는 internal(String) 메서드가 없기 때문이다. 부모 타입을 표현식에 선언한 경우 부모 타입에서 선언한 메서드가 자식 타입에 있어야 매칭에 성공한다.
그래서 부모 타입에 있는 hello(String) 메서드는 매칭에 성공한다.
부모 타입에 없는 internal(String) 는 매칭에 실패한다.
@TestvoidtypeMatchInternal()throwsNoSuchMethodException{pointcut.setExpression("execution(* com.example.app.MemberServiceImpl.*(..))");MethodinternalMethod=MemberServiceImpl.class.getMethod("internal",String.class);assertThat(pointcut.matches(internalMethod,MemberServiceImpl.class)).isTrue();}//포인트컷으로 지정한 MemberService는 internal 이라는 이름의 메서드가 없다.@TestvoidtypeMatchNoSuperTypeMethodFalse()throwsNoSuchMethodException{pointcut.setExpression("execution(* com.example.app.MemberService.*(..))");MethodinternalMethod=MemberServiceImpl.class.getMethod("internal",String.class);assertThat(pointcut.matches(internalMethod,MemberServiceImpl.class)).isFalse();}
파라미터 매칭
ExecutionTest에 테스트를 추가해보자.
execution 파라미터 매칭 규칙은 다음과 같다.
(String)
정확하게 String 타입 파라미터
()
파라미터가 없어야 한다.
(*)
정확히 하나의 파라미터
단, 모든 타입을 허용한다.
(*, *)
정확히 두 개의 파라미터
단, 모든 타입을 허용한다.
(..)
숫자와 무관하게 모든 파라미터
단, 모든 타입을 허용한다.
참고로 파라미터가 없어도 된다. 0..*로 이해하면 된다.
(String, ..)
String 타입으로 시작해야 한다.
숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다.
예시
(String)
(String, Xxx)
(String, Xxx, Xxx)
//파라미터가 없어야 함//()@TestvoidargsMatchNoArgs(){pointcut.setExpression("execution(* *())");assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isFalse();}//정확히 하나의 파라미터 허용, 모든 타입 허용//(Xxx)@TestvoidargsMatchStar(){pointcut.setExpression("execution(* *(*))");assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();}//숫자와 무관하게 모든 파라미터, 모든 타입 허용//파라미터가 없어도 됨//(), (Xxx), (Xxx, Xxx)@TestvoidargsMatchAll(){pointcut.setExpression("execution(* *(..))");assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();}//String 타입으로 시작, 숫자와 무관하게 모든 파라미터, 모든 타입 허용//(String), (String, Xxx), (String, Xxx, Xxx) 허용@TestvoidargsMatchComplex(){pointcut.setExpression("execution(* *(String, ..))");assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();}
within
within 지시자는 특정 타입 내의 조인 포인트들로 매칭을 제한한다.
해당 타입이 매칭되면 그 안의 메소드(조인 포인트)들이 자동으로 매칭된다.
execution에서 타입 부분만 사용한다고 보면 된다
테스트 생성
importcom.example.app.MemberServiceImpl;importorg.junit.jupiter.api.BeforeEach;importorg.junit.jupiter.api.DisplayName;importorg.junit.jupiter.api.Test;importorg.springframework.aop.aspectj.AspectJExpressionPointcut;importjava.lang.reflect.Method;importstaticorg.assertj.core.api.Assertions.assertThat;publicclassWithinTest{AspectJExpressionPointcutpointcut=newAspectJExpressionPointcut();MethodhelloMethod;@BeforeEachpublicvoidinit()throwsNoSuchMethodException{helloMethod=MemberServiceImpl.class.getMethod("hello",String.class);}@TestvoidwithinExact(){pointcut.setExpression("within(com.example.app.MemberServiceImpl)");assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();}@TestvoidwithinStar(){pointcut.setExpression("within(com.example.app.*Service*)");assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();}@TestvoidwithinSubPackage(){pointcut.setExpression("within(com.example..*)");assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();}@Test@DisplayName("타켓의 타입에만 직접 적용, 인터페이스를 선정하면 안된다.")voidwithinSuperTypeFalse(){pointcut.setExpression("within(com.example.app.MemberService)");assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isFalse();}@Test@DisplayName("execution은 타입 기반, 인터페이스를 선정 가능.")voidexecutionSuperTypeTrue(){pointcut.setExpression("execution(* com.example.app.MemberService.*(..))");assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();}}
주의사항
표현식에 부모 타입을 지정하면 안 된다.
정확하게 타입이 맞아야 한다.
이 부분이 execution과의 차이점이다.
부모 타입(여기서는 MemberService 인터페이스) 지정시 within 은 실패하고, execution 은 성공하는 것을 확인할 수 있다.
args
args
인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭
기본 문법은 execution의 args 부분과 같다.
execution과 args의 차이점
execution
파라미터 타입이 정확하게 매칭되어야 한다.
클래스에 선언된 정보를 기반으로 판단한다.
args는 부모 타입을 허용한다.
args는 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다.
테스트 생성
pointcut()
AspectJExpressionPointcut에 포인트컷은 한번만 지정할 수 있다.
테스트를 편리하게 진행하기 위해 포인트컷을 여러번 지정하기 위해 포인트컷 자체를 생성하는 메소드
자바가 기본으로 제공하는 String은 Object와 java.io.Serializable의 하위 타입이다.
정적으로 클래스에 선언된 정보만 보고 판단하는 execution(* *(Object))는 매칭에 실패한다.
동적으로 실제 파라미터로 넘어온 객체 인스턴스로 판단하는 args(Object)는 매칭에 성공한다.
@target과 @within은 다음과 같이 타입에 있는 애노테이션으로 AOP 적용 여부를 판단한다.
@target(com.example.annotation.ClassAop)
@within(com.example.annotation.ClassAop)
@target vs @within
@target
인스턴스의 모든 메서드를 조인 포인트로 적용한다.
부모 클래스의 메서드까지 어드바이스를 다 적용한다.
@within
해당 타입 내에 있는 메서드만 조인 포인트로 적용한다.
자기 자신의 클래스에 정의된 메서드에만 어드바이스를 적용한다.
테스트 생성
parentMethod()는 Parent 클래스에만 정의되어 있고, Child 클래스에 정의되어 있지 않기 때문에 @within에서 AOP 적용 대상이 되지 않는다.
실행결과를 보면 child.parentMethod()를 호출 했을 때 @within이 호출되지 않은 것을 확인할 수 있다.
importcom.example.Chapter3Application;importcom.example.annotation.ClassAop;importlombok.extern.slf4j.Slf4j;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Import;importorg.springframework.test.context.ContextConfiguration;@Slf4j@Import({AtTargetAtWithinTest.Config.class})@SpringBootTest@ContextConfiguration(classes=Chapter3Application.class)publicclassAtTargetAtWithinTest{@AutowiredChildchild;@Testvoidsuccess(){log.info("child Proxy={}",child.getClass());child.childMethod();//부모, 자식 모두 있는 메서드child.parentMethod();//부모 클래스만 있는 메서드}staticclassConfig{@BeanpublicParentparent(){returnnewParent();}@BeanpublicChildchild(){returnnewChild();}@BeanpublicAtTargetAtWithinAspectatTargetAtWithinAspect(){returnnewAtTargetAtWithinAspect();}}staticclassParent{publicvoidparentMethod(){}//부모에만 있는 메서드}@ClassAopstaticclassChildextendsParent{publicvoidchildMethod(){}}@Slf4j@AspectstaticclassAtTargetAtWithinAspect{//@target: 인스턴스 기준으로 모든 메서드의 조인 포인트를 선정, 부모 타입의 메서드도 적용@Around("execution(* com.example..*(..)) && @target(com.example.annotation.ClassAop)")publicObjectatTarget(ProceedingJoinPointjoinPoint)throwsThrowable{log.info("[@target] {}",joinPoint.getSignature());returnjoinPoint.proceed();}//@within: 선택된 클래스 내부에 있는 메서드만 조인 포인트로 선정, 부모 타입의 메서드는 적용되지 않음@Around("execution(* com.example..*(..)) && @within(com.example.annotation.ClassAop)")publicObjectatWithin(ProceedingJoinPointjoinPoint)throwsThrowable{log.info("[@within] {}",joinPoint.getSignature());returnjoinPoint.proceed();}}}
주의사항
다음 포인트컷 지시자는 단독으로 사용하면 안된다.
args
@args
@target
이번 테스트의 코드를 보면 execution(* com.example..*(..))를 통해 적용 대상을 줄여준 것을 확인할 수 있다.
args, @args, @target은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다.
실행 시점에 일어나는 포인트컷 적용 여부도 결국 프록시가 있어야 실행 시점에 판단할 수 있다.
프록시가 없다면 판단 자체가 불가능하다.
그런데 스프링 컨테이너가 프록시를 생성하는 시점은 스프링 컨테이너가 만들어지는 애플리케이션 로딩 시점에 적용할 수 있다.
따라서 args, @args, @target같은 포인트컷 지시자가 있으면 스프링은 모든 스프링 빈에 AOP를 적용하려고 시도한다.
프록시가 없으면 실행 시점에 판단 자체가 불가능하다.
이렇게 모든 스프링 빈에 AOP 프록시를 적용하려고 하면 스프링이 내부에서 사용하는 빈 중에는 final로 지정된 빈들도 있기 때문에 오류가 발생할 수 있다.
importcom.example.Chapter3Application;importcom.example.annotation.ClassAop;importcom.example.annotation.MethodAop;importcom.example.app.MemberService;importlombok.extern.slf4j.Slf4j;importorg.aspectj.lang.JoinPoint;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Before;importorg.aspectj.lang.annotation.Pointcut;importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.context.annotation.Import;importorg.springframework.test.context.ContextConfiguration;@Slf4j@Import(ParameterTest.ParameterAspect.class)@SpringBootTest@ContextConfiguration(classes=Chapter3Application.class)publicclassParameterTest{@AutowiredprivateMemberServicememberService;@Testvoidsuccess(){log.info("memberService Proxy={}",memberService.getClass());memberService.hello("helloA");}@Slf4j@AspectstaticclassParameterAspect{//joinPoint.getArgs()[0] 와 같이 매개변수를 전달 받는다.@Pointcut("execution(* com.example.app..*.*(..))")privatevoidallMember(){}@Around("allMember()")publicObjectlogArgs1(ProceedingJoinPointjoinPoint)throwsThrowable{Objectarg1=joinPoint.getArgs()[0];log.info("[logArgs1]{}, arg={}",joinPoint.getSignature(),arg1);returnjoinPoint.proceed();}//args(arg,..) 와 같이 매개변수를 전달 받는다.@Around("allMember() && args(arg,..)")publicObjectlogArgs2(ProceedingJoinPointjoinPoint,Objectarg)throwsThrowable{log.info("[logArgs2]{}, arg={}",joinPoint.getSignature(),arg);returnjoinPoint.proceed();}//@Before 를 사용한 축약 버전이다. 추가로 타입을 String 으로 제한했다.@Before("allMember() && args(arg,..)")publicvoidlogArgs3(Stringarg){log.info("[logArgs3] arg={}",arg);}//this를 통해 프록시 객체를 전달 받는다@Before("allMember() && this(obj)")publicvoidthisArgs(JoinPointjoinPoint,MemberServiceobj){log.info("[this]{}, obj={}",joinPoint.getSignature(),obj.getClass());}//target을 통해 실제 대상 객체를 전달 받는다.@Before("allMember() && target(obj)")publicvoidtargetArgs(JoinPointjoinPoint,MemberServiceobj){log.info("[target]{}, obj={}",joinPoint.getSignature(),obj.getClass());}//@target을 통해 타입의 애노테이션을 전달 받는다.@Before("allMember() && @target(annotation)")publicvoidatTarget(JoinPointjoinPoint,ClassAopannotation){log.info("[@target]{}, obj={}",joinPoint.getSignature(),annotation);}//@within을 통해 타입의 애노테이션을 전달 받는다.@Before("allMember() && @within(annotation)")publicvoidatWithin(JoinPointjoinPoint,ClassAopannotation){log.info("[@within]{}, obj={}",joinPoint.getSignature(),annotation);}//@annotation을 통해 메서드의 애노테이션을 전달 받는다.@Before("allMember() && @annotation(annotation)")publicvoidatAnnotation(JoinPointjoinPoint,MethodAopannotation){log.info("[@annotation]{}, annotationValue={}",joinPoint.getSignature(),annotation.value());}}}
this, target
정의
this
스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
target
Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트
설명
this , target 은 다음과 같이 적용 타입 하나를 정확하게 지정해야 한다
* 같은 패턴을 사용할 수 없다.
부모 타입을 허용한다.
this vs target
스프링에서 AOP를 적용하면 실제 target 객체 대신에 프록시 객체가 스프링 빈으로 등록된다.
this는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 포인트컷을 매칭한다.
target은 실제 target 객체를 대상으로 포인트컷을 매칭한다.
프록시 생성 방식에 따른 차이
스프링은 프록시를 생성할 때 JDK 동적 프록시와 CGLIB를 선택할 수 있다.
둘의 프록시를 생성하는 방식이 다르기 때문에 차이가 발생한다.
JDK 동적 프록시
인터페이스 필수
인터페이스를 구현한 프록시 객체를 생성한다.
CGLIB
인터페이스가 있어도 구체 클래스를 상속 받아서 프록시 객체를 생성한다.
프록시를 대상으로 하는 this 의 경우 구체 클래스를 지정하면 프록시 생성 전략에 따라서 다른 결과가 나올 수 있다.
this , target 지시자는 단독으로 사용되기 보다는 파라미터 바인딩에서 주로 사용된다.
JDK 동적 프록시 적용
MemberService 인터페이스 지정
this(com.example.app.MemberService)
proxy 객체를 보고 판단한다.
this는 부모 타입을 허용하기 때문에 AOP가 적용된다.
target(com.example.app.MemberService)
target 객체를 보고 판단한다.
target 은 부모 타입을 허용하기 때문에 AOP가 적용된다.
MemberServiceImpl 구체 클래스 지정
this(com.example.app.MemberServiceImpl)
proxy 객체를 보고 판단한다.
JDK 동적 프록시로 만들어진 proxy 객체는 MemberService 인터페이스를 기반으로 구현된 새로운 클래스다.
그래서 MemberServiceImpl를 전혀 알지 못하므로 AOP 적용 대상이 아니다.
target(com.example.app.MemberServiceImpl)
target 객체를 보고 판단한다.
target 객체가 MemberServiceImpl 타입이므로 AOP 적용 대상이다.
CGLIB 적용
MemberService 인터페이스 지정
this(com.example.app.MemberService)
proxy 객체를 보고 판단한다.
this 는 부모 타입을 허용하기 때문에 AOP가 적용된다.
target(com.example.app.MemberService)
target 객체를 보고 판단한다.
target은 부모 타입을 허용하기 때문에 AOP가 적용된다.
MemberServiceImpl 구체 클래스 지정
this(com.example.app.MemberServiceImpl)
proxy 객체를 보고 판단한다.
CGLIB로 만들어진 proxy 객체는 MemberServiceImpl를 상속 받아서 만들었기 때문에 AOP가 적용된다.
this가 부모 타입을 허용하기 때문에 포인트컷의 대상이 된다.
target(com.example.app.MemberServiceImpl)
target 객체를 보고 판단한다.
target 객체가 MemberServiceImpl 타입이므로 AOP 적용 대상이다.
테스트 생성
importcom.example.Chapter3Application;importcom.example.app.MemberService;importlombok.extern.slf4j.Slf4j;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.context.annotation.Import;importorg.springframework.test.context.ContextConfiguration;/**
* application.properties
* spring.aop.proxy-target-class=true CGLIB
* spring.aop.proxy-target-class=false JDK 동적 프록시
*/@Slf4j@Import(ThisTargetTest.ThisTargetAspect.class)@SpringBootTest(properties="spring.aop.proxy-target-class=false")//JDK 동적 프록시//@SpringBootTest(properties = "spring.aop.proxy-target-class=true") //CGLIB@ContextConfiguration(classes=Chapter3Application.class)publicclassThisTargetTest{@AutowiredprivateMemberServicememberService;@Testvoidsuccess(){log.info("memberService Proxy={}",memberService.getClass());memberService.hello("helloA");}@Slf4j@AspectstaticclassThisTargetAspect{//부모 타입 허용@Around("this(com.example.app.MemberService)")publicObjectdoThisInterface(ProceedingJoinPointjoinPoint)throwsThrowable{log.info("[this-interface] {}",joinPoint.getSignature());returnjoinPoint.proceed();}//부모 타입 허용@Around("target(com.example.app.MemberService)")publicObjectdoTargetInterface(ProceedingJoinPointjoinPoint)throwsThrowable{log.info("[target-interface] {}",joinPoint.getSignature());returnjoinPoint.proceed();}//this: 스프링 AOP 프록시 객체 대상//JDK 동적 프록시는 인터페이스를 기반으로 생성되므로 구현 클래스를 알 수 없음//CGLIB 프록시는 구현 클래스를 기반으로 생성되므로 구현 클래스를 알 수 있음@Around("this(com.example.app.MemberServiceImpl)")publicObjectdoThis(ProceedingJoinPointjoinPoint)throwsThrowable{log.info("[this-impl] {}",joinPoint.getSignature());returnjoinPoint.proceed();}//target: 실제 target 객체 대상@Around("target(com.example.app.MemberServiceImpl)")publicObjectdoTarget(ProceedingJoinPointjoinPoint)throwsThrowable{log.info("[target-impl] {}",joinPoint.getSignature());returnjoinPoint.proceed();}}}