애스펙트J는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공한다.
포인트컷 표현식은 execution
같은 포인트컷 지시자로 시작한다. 줄여서 PCD라 한다.
종류
execution
: 메서드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용하고, 기능도 복잡하다.within
: 특정 타입 내의 조인 포인트를 매칭한다.args
: 인자가 주어진 타입의 인스턴스의 조인 포인트this
: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트target
: Target 객체(스프링 AOP 프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트@target
: 실행 겍체의 클래스에 주어진 타입의 어노테이션이 있는 조인 포인트@annotation
: 메서드가 주어진 어노테이션을 가지고 있는 조인 포인트를 매칭@args
: 전달된 실제 인수의 런타임 타입이 주어진 타입의 어노테이션을 갖는 조인 포인트bean
: 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.execution
을 가장 많이 사용하고, 나머지는 자주 사용하지 않는다.
execution 문법
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
*
와 같은 패턴을 지정할 수 있다.매칭 조건
접근제어자?: public
반환타입: String
선언타입?: hello.aop.member.MemberServiceImpl
메서드이름: hello
파라미터: (String)
예외?: 생략
메서드와 포인트컷 표현식의 모든 내용이 정확하게 일치한다. 따라서 true
를 반환한다.
매칭 조건
접근제어자?: 생략
반환타입: *
선언타입?: 생략
메서드이름: *
파라미터: (...)
예외?: 없음
*
는 아무 값이 들어와도 된다는 뜻.
파라미터의 ..
는 파라미터의 타입과 파라미터 수가 상관없다는 뜻.
0개도 되고 여러개도 되고
@Test
void nameMatch() {
pointcut.setExpression("execution(* hello(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void nameMatchStar1() {
pointcut.setExpression("execution(* hel*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void nameMatchStar2() {
pointcut.setExpression("execution(* *el*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void nameMatchFalse() {
pointcut.setExpression("execution(* nono(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
@Test
void packageExactMatch1() {
pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.hello(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void packageExactMatch2() {
pointcut.setExpression("execution(* hello.aop.member.*.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void packageExactFalse() {
pointcut.setExpression("execution(* hello.aop.*.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
@Test
void packageMatchSubPackage1() {
pointcut.setExpression("execution(* hello.aop.member..*.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void packageMatchSubPackage2() {
pointcut.setExpression("execution(* hello.aop..*.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
타입 매칭 - 부모 타입 허용
부모는 자식을 품을 수 있기 때문에 가능
타입 매칭 - 부모가 가지고 있는 메서드만 가능
MemberServiceImpl
클래스만 소장하고 있는 internal
메서드를 확인하려는 경우 없다고 뜸. 부모는 hello
라는 메서드만 가지고 있기 때문.
파라미터
//String 타입의 파라미터 허용
@Test
void argsMatch() {
pointcut.setExpression("execution(* *(String))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
//파라미터 X
@Test
void argsMatchNoArgs() {
pointcut.setExpression("execution(* *())");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
//정확히 하나의 파라미터 허용, 모든 타입
@Test
void argsMatchStar() {
pointcut.setExpression("execution(* *(*))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
//(), (Xxx), (Xxx, Xxx)
@Test
void argsMatchAll() {
pointcut.setExpression("execution(* *(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
//처음이 무조건 String
//(String), (String, Xxx), (String, Xxx, Xxx)
@Test
void argsMatchComplex() {
pointcut.setExpression("execution(* *(String, ..))"); // String, * 는 파라미터가 2개
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
execution 파라미터 매칭 규칙
(String)
: 정확하게 String
타입 파라미터()
: 파라미터 X(*)
: 정확히 하나의 파라미터, 대신 모든 타입(*, *)
: 정확히 두 개의 파라미터, 모든 타입(..)
: 숫자와 무관하게 모든 파라미터. 없어도 됨.(String, ..)
: (String)
, (String, Xxx)
, (String, Xxx, Xxx)
...(String, *)
: 파라미터 2개만 가능. 처음은 String
특정 타입을 지정할 수 있고, 매칭되는 메서드를 전부 가져옴.
@Test
void withinExact() {
pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void withinStar() {
pointcut.setExpression("within(hello.aop.member.*Service*)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void withinSubPackage() {
pointcut.setExpression("within(hello.aop..*)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
within
의 사용법은 execution
과 유사하다. 그래도 execution
보다 기능이 적다.
하지만 차이점이 있는데,
execution
과 다르게, 부모를 가져와 자식 비교를 할 수 없기 때문에 인터페이스 선정을 할 수 없다.
근데 잘 안씀. 이유는 execution
이 더 좋아서.
강의중에 파일을 통채로 복사할 정도로 잘 안쓰는듯.
args
: 인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭
execution
과 args
의 차이점
execution
은 파라미터 타입이 정확히 매칭되어야 한다.args
는 부모 타입을 허용한다.args vs execution
args
의 경우에는 런타임에 전달된 인수로 보기 때문에 부모타입도 허용이 가능하다.
execution
의 경우 메서드 시그니처로 판단하기 때문에 타입이 동일해야 한다.
args
의 경우 단독으로는 잘 안쓴다고 함.
정의
@target
: 실행 객체의 클래스에 주어진 타입의 어노테이션이 있는 조인 포인트
@within
: 주어진 어노테이션이 있는 타입 내 조인 포인트
@target(hello.aop.member.annotation.ClassAop)
@within(hello.aop.member.annotation.ClassAop)
@target vs @within
@target
은 인스턴스의 모든 메서드를 조인 포인트로 적용한다.
@within
은 해당 타입 내에 있는 메서드만 조인 포인트로 적용한다.
package hello.aop.pointcut;
import hello.aop.member.annotation.ClassAop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
@Slf4j
@Import({AtTargetAtWithinTest.Config.class})
@SpringBootTest
public class AtTargetAtWithinTest {
@Autowired
Child child;
@Test
void success() {
log.info("child Proxy={}", child.getClass());
child.childMethod(); //부모, 자식 모두 있는 메서드
child.parentMethod(); //부모 클래스만 있는 메서드
}
static class Config {
@Bean
public Parent parent() {
return new Parent();
}
@Bean
public Child child() {
return new Child();
}
@Bean
public AtTargetAtWithinAspect atTargetAtWithinAspect() {
return new AtTargetAtWithinAspect();
}
}
static class Parent {
public void parentMethod(){} //부모에만 있는 메서드
}
@ClassAop
static class Child extends Parent {
public void childMethod(){}
}
@Slf4j
@Aspect
static class AtTargetAtWithinAspect {
//@target: 인스턴스 기준으로 모든 메서드의 조인 포인트를 선정, 부모 타입의 메서드도 적용
@Around("execution(* hello.aop..*(..)) && @target(hello.aop.member.annotation.ClassAop)")
public Object atTarget(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@target] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//@within: 선택된 클래스 내부에 있는 메서드만 조인 포인트로 선정, 부모 타입의 메서드는 적용되지 않음
@Around("execution(* hello.aop..*(..)) && @within(hello.aop.member.annotation.ClassAop)")
public Object atWithin(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@within] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
}
@target
: 내꺼와 연결된 부모. 동적
@within
: 내꺼만. 정적
주의
args
, @args
, @target
는 단독으로 사용하면 안된다.
실행 시점에 일어나는 포인트컷 적용도 결국에는 프록시가 있어야 할 수 있다.
프록시 없이 사용하면, 모든 스프링 빈에 AOP를 적용하려고 한다.
그럼 오류가 발생한다.
그러므로 execution
등과 같이 범위를 정해주는 것과 함께 사용해줘야한다.
@annotation
: 메서드가 주어진 어노테이션을 가지고 있는 조인 포인트를 매칭
@annotation(hello.aop.member.annotation.methodAop)
이렇게 사용한다.
쉽게 말해 @MethodAop
어노테이션이 걸린 곳을 프록시로 만들어 주는 것이다.
좀 유용하게 사용한다.
@args
의 경우, 파라미터로 들어온 인수 클래스 내부에 어노테이션이 있을때 프록시를 생성해준다고 한다. 이건 잘 안쓴다고 함.
@args(test.Check)
이런식으로 사용하면 @Check
가 들어온 파라미터 객체의 내부에 있을 경우, 프록시 생성.
bean
: 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정한다.
bean(orderService) || bean(*Respository)
*
와 같은 패턴을 사용할 수 있다.
다음 포인트컷 표현식을 사용해서 어드바이스에서 매개변수를 전달할 수 있다.
this target, args, @target, @within, @annotation, @args
@Before("allMember() && args(arg, ..)")
public void logArgs3(String arg) {
log.info("[logArgs3] arg={}", arg);
}
포인트컷의 이름과 매개변수의 이름을 맞춰야 한다. arg
추가로 타입의 메서드에 지정한 타입으로 제한된다. String
으로 받기 때문에 String
으로 제한됨.
package hello.aop.pointcut;
import hello.aop.member.MemberService;
import hello.aop.member.annotation.ClassAop;
import hello.aop.member.annotation.MethodAop;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.Joinpoint;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
@Slf4j
@Import(ParameterTest.ParameterAspect.class)
@SpringBootTest
public class ParameterTest {
@Autowired
MemberService memberService;
@Test
void success() {
log.info("memberService Proxy={}", memberService.getClass());
memberService.hello("helloA");
}
@Slf4j
@Aspect
static class ParameterAspect {
@Pointcut("execution(* hello.aop.member..*.*(..))")
private void allMember() {
}
@Around("allMember()")
public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
Object arg1 = joinPoint.getArgs()[0];
log.info("[logArgs1]{}, arg={}", joinPoint.getSignature(), arg1);
return joinPoint.proceed();
}
@Around("allMember() && args(arg,..)")
public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
log.info("[logArgs2]{}, arg={}", joinPoint.getSignature(), arg);
return joinPoint.proceed();
}
@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
log.info("[logArgs3] arg={}", arg);
}
@Before("allMember() && this(obj)")
public void thisArgs(JoinPoint joinpoint, MemberService obj) {
log.info("[this]{}, obj={}", joinpoint.getSignature(), obj.getClass());
}
@Before("allMember() && target(obj)")
public void targetArgs(JoinPoint joinpoint, MemberService obj) {
log.info("[target]{}, obj={}", joinpoint.getSignature(), obj.getClass());
}
@Before("allMember() && @target(annotation)")
public void atTarget(JoinPoint joinpoint, ClassAop annotation) {
log.info("[@target]{}, obj={}", joinpoint.getSignature(), annotation.getClass());
}
@Before("allMember() && @within(annotation)")
public void atWithin(JoinPoint joinpoint, ClassAop annotation) {
log.info("[@within]{}, obj={}", joinpoint.getSignature(), annotation.getClass());
}
@Before("allMember() && @annotation(annotation)")
public void atAnnotation(JoinPoint joinpoint, MethodAop annotation) {
log.info("[@annotation]{}, annotationValue={}", joinpoint.getSignature(), annotation.value());
}
}
}
logArgs1
: joinPoint.getArgs()[0]
에서 매개변수를 받음
logArgs2
: args(arg,..)
에서 매개변수를 받음
logArgs3
: @Before
를 사용하여 매개변수를 받고, 추가로 String
으로 제한함
this
: 프록시 객체를 받음
target
: 실제 대상 객체를 받음
@target
, @within
: 타입의 어노테이션을 받음
@annotation
: 메서드의 어노테이션을 받음. 해당 어노테이션의 값도 받을 수 있음
this
: 스프링 AOP 프록시를 호출
target
: 스프링 AOP 프록시가 실제로 호출하는 대상을 호출
this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)
*
패턴 사용할 수 없다.
부모 타입 허용
this vs target
어려운 내용인데, 또 그렇게 중요하지는 않음.
또 많이 사용하지도 않음.
JDK 동적 프록시
: 인터페이스가 필수이고, 인터페이스를 구현한 프록시 객체를 생성
CGLIB
: 인터페이스가 있어도 구체 클래스를 상속받아서 프록시 객체를 생성
이때는 문제가 없다.
this
: proxy
객체를 보고 판단한다.
target
: target
객체를 보고 판단한다.
MemberServiceImpl 구체 클래스 지정
this
: 프록시는MemberService
인터페이스를 기반으로 구현된 새로운 클래스이다. 그러므로 MemberServiceImpl
을 전혀 모르기 때문에 AOP 적용 대상이 아니다.
target
: MemberServiceImpl
이므로 AOP 적용 대상이다.
this
: proxy
객체를 보고 판단한다.
target
: target
객체를 보고 판단한다.
MemberServiceImpl 구체 클래스 지정
this
: 프록시는MemberServiceImpl
를 상속받아 구현된 새로운 클래스이다. 그러므로 포인트컷의 대상이 된다.
target
: MemberServiceImpl
이므로 AOP 적용 대상이다.
정리
this
의 경우 구체 클래스를 지정하면 다른 결과가 나올 수 있다.
package hello.aop.pointcut;
import hello.aop.member.MemberService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
/**
* 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=true")
public class ThisTargetTest {
@Autowired
MemberService memberService;
@Test
void success() {
log.info("memberService Proxy={}", memberService.getClass());
memberService.hello("helloA");
}
@Slf4j
@Aspect
static class ThisTargetAspect {
//부모 타입 허용
@Around("this(hello.aop.member.MemberService)")
public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-interface] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//부모 타입 허용
@Around("target(hello.aop.member.MemberService)")
public Object doTargetInterface(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[target-interface] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//부모 타입 허용
@Around("this(hello.aop.member.MemberServiceImpl)")
public Object doThis(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-impl] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//부모 타입 허용
@Around("target(hello.aop.member.MemberServiceImpl)")
public Object doTarget(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[target-impl] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
}
CGLIB 일때
JDK 일때