package com.example.aop;
import org.aspectj.lang.annotation.Pointcut;
public class Pointcuts {
// @LogTraceAnnotation 애노테이션에 적용
@Pointcut("@annotation(com.example.annotations.LogTraceAnnotation)")
public void logTrace() {
}
// exceptClear()를 제외한
// com.example.app 폴더 하위 모든 메소드에 적용
@Pointcut("execution(* com.example.app..*(..)) && exceptClear()")
public void appPackage() {
}
// exceptClear()를 제외한
// com.example.app.service 폴더 하위 모든 메소드에 적용
@Pointcut("execution(* com.example.app.service..*(..)) && exceptClear()")
public void allService() {
}
// exceptClear()를 제외한
// com.example.app.controller 폴더 하위 모든 메소드에 적용
@Pointcut("execution(* com.example.app.controller..*(..)) && exceptClear()")
public void allController() {
}
// com.example.app 폴더 하위 Impl로 끝나는
// clear라는 메소드명을 가진에 모든 메소드에 적용
@Pointcut("!execution(* com.example.app..*Impl.clear(..))")
public void exceptClear() {
}
}
Aspect의 순서를 정하기 위해선 @Order 애노테이션을 적용해야하는데
이것을 어드바이스 단위가 아니라 클래스 단위로 적용할 수 있다
그래서 하나의 애스펙트에 여러 어드바이스가 있으면 순서를 보장 받을 수 없다.
따라서 애스펙트를 별도의 클래스로 분리해야 한다.
package com.example.aop;
import com.example.annotations.RetryAnnotation;
import com.example.aop.trace.TraceStatus;
import com.example.aop.trace.LogTrace;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
@Slf4j
public class Aspects {
private Aspects() {}
@RequiredArgsConstructor
@Aspect
@Order(1)
// @Component
public static class LogTraceAspect {
private final LogTrace logTrace;
@Around("com.example.aop.Pointcuts.logTrace() || com.example.aop.Pointcuts.appPackage()")
public <T> T trace(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Aspects.LogTraceAspect 메소드 시작");
// 로그 상태 값
TraceStatus status = null;
try {
String message = joinPoint.getSignature().toShortString();
// 로그 출력 시작
status = logTrace.start(message);
// 로직 수행
// noinspection unchecked
T result = (T) joinPoint.proceed();
// 로그 출력 종료
logTrace.end(status);
log.info("Aspects.LogTraceAspect 메소드 종료");
return result;
} catch (Exception e) {
// 에러 로그
logTrace.exception(status, e);
throw e;
}
}
}
@Aspect
@Order(2)
// @Component
public static class RetryAspect {
@Around("@annotation(retryAnnotation)")
public <T> T retry(ProceedingJoinPoint joinPoint, RetryAnnotation retryAnnotation) throws Throwable {
log.info("Aspects.RetryAspect 메소드 시작");
// 실패 시 반복 횟수
int maxRetryCount = retryAnnotation.value();
Exception exception = null;
for (int tryCount = 1; tryCount <= maxRetryCount; tryCount++) {
try {
log.warn("{}/{}", tryCount, maxRetryCount);
// 로직 수행
// noinspection unchecked
log.info("Aspects.RetryAspect 메소드 종료");
return (T) joinPoint.proceed();
} catch (Exception e) {
exception = e;
}
}
// exception 이 null 이 아닌지 확인
assert exception != null;
throw exception;
}
}
@Aspect
@Order(3)
// @Component
public static class TransactionAspect {
@Around("com.example.aop.Pointcuts.allService()")
public <T> T transaction(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Aspects.TransactionAspect 메소드 시작");
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
// 로직 수행
// noinspection unchecked
T result = (T) joinPoint.proceed();
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
log.info("Aspects.TransactionAspect 메소드 종료");
}
}
}
}
package com.example.config;
import com.example.aop.Aspects;
import com.example.aop.Aspects.*;
import com.example.aop.trace.LogTrace;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//@Configuration
@RequiredArgsConstructor
public class AspectConfig {
private final LogTrace logTrace;
@Bean
public LogTraceAspect logTraceAspect() {
return new Aspects.LogTraceAspect(logTrace);
}
@Bean
public RetryAspect retryAspect() {
return new RetryAspect();
}
@Bean
public TransactionAspect transactionAspect() {
return new TransactionAspect();
}
}
package com.example;
import com.example.app.repository.ItemRepository;
import com.example.app.repository.UserRepository;
import com.example.domain.item.Item;
import com.example.domain.user.Grade;
import com.example.domain.user.User;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@Import({AspectConfig.class})
@SpringBootApplication
@RequiredArgsConstructor
public class SampleApplication {
private final ItemRepository itemRepository;
private final UserRepository userRepository;
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
}
@SpringBootApplication
에는 @ComponentScan
애노테이션이 포함되어 있으므로
@Configuration
과 @Component
애노테이션을 찾아 스프링 빈을 알아서 등록한다.
따라서 Aspects 파일의 @Component의
주석 해제 시 @ComponentScan
으로 Aspect들이 모두 스프링 빈으로 등록되어 AspectConfig
가 없어도 된다.
지금과 같이 @Component
와 @Configuration
이 주석 처리되어있거나 없는 경우
@Import({AspectConfig.class})
애노테이션으로 AspectConfig
를 설정 정보로 수동 등록하여 포함시키고
AspectConfig
는 Aspect들을 스프링 빈으로 수동 등록한다.