다음 사이트의 글을 내 맘대로 번역한 글입니다
Spring Boot and Spring AOP - Aspect Oriented Programming
In this tutorial, We are going to learn about using restful web service using spring boot
myweblearner.com
Overview
Aspect Oriented Programming (AOP)는 써야하지만 우아하지 않은 코드, 비지니스 로직들을 줄여주고 가독성을 유지시켜주는데 도움이 된다. 어떤 메소드를 실행하기 전 또는 후에 특정 task를 실행하고 싶다면 AOP를 사용하면 된다. 내부적으로 Spring은 proxy를 사용해서 실제 메소드 실행 클래스를 둘러싼 wrapper를 호출한다
- JointPoint : 대상 메소드
- PointCut : joint point를 매칭하기 위해 사용하는 predicate
- Advice : PointCut call 전/후에 실행할 Code/Task
- Aspect : pointcut과 advice의 묶음
- Weaving : aspect가 실행되는 시점
About Demo
이번 demo에서는 restful webservice에서 Aspect를 사용하는 방법을 보여준다. 전체 package, 특정 class/method처럼 PointCut을 정의하는 방법에는 여러가지가 있다. 여기서는 AOP기반의 @annotation을 사용한다. 2개의 AOP를 만들어서 , 하나는 메소드의 전/후에 로그를 출력하고 다른 하나는 method의 성능을 계산하고 출력한다. DB 대신 List를 사용한다
Prerequisites
- Maven 3.3.9
- Java 8
- STS (or Eclipse)
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
Model
import lombok.Data;
@Data
public class Employee {
private String empId;
private String empName;
private String deptName;
}
RestControler
CRUD를 지원한다
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.budnamu.sample.aop.model.Employee;
import com.budnamu.sample.aop.service.EmployeeService;
@RestController
@RequestMapping("/aop")
public class EmployeeController {
@Autowired
private EmployeeService dummyService;
// Get the employee detail based on employee id
@GetMapping("/employee/{empid}")
public Employee getEmployee(@PathVariable("empid") String empId) {
return dummyService.getEmployee(empId);
}
//post the new employee details to backend
@PostMapping("/employee")
public String newEmployee(@RequestBody Employee employee) {
return dummyService.addEmployee(employee);
}
// update the employee depart using emp id
@PutMapping("/employee/{empid}")
public String updateEmployee(@PathVariable("empid") String empId, @RequestParam("deptname") String deptName) {
return dummyService.updateEmployeeDept(empId, deptName);
}
// Delete the employee
@DeleteMapping("/employee/{empid}")
public String deleteEmployee(@PathVariable("empid") String empId) {
return dummyService.deleteEmployee(empId);
}
}
Custom Annotation
MethodLogger.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.stereotype.Component;
@Component
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodLogger {
}
CalculatePerformance.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.stereotype.Component;
@Component
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CalculatePerformance {
}
Aspect Definition
@Aspect와 @Configuration annotation을 사용해서 aspect를 정의한다.
여기에서는 적용할 대상, 적용시점 그리고 무엇을 실행할 것인가에 대한 정보를 정의한다.
예를 들어 beforeMethodStart 메소드는 메소드를 호출하기 전에 실행하며 그 대상은 MethodLogger annotation이 선언된 메소드를 대상으로 한다.
import java.time.LocalDateTime;
import java.time.LocalTime;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Configuration;
import static java.time.temporal.ChronoUnit.SECONDS;;
@Configuration
@Aspect
public class LoggerAspect {
@Before("@annotation(com.budnamu.sample.aop.aop.MethodLogger)")
public void beforeMethodStart(JoinPoint point) {
System.out.println("Method " + point.getSignature().getName() + " Started at " + LocalDateTime.now());
}
@After("@annotation(com.budnamu.sample.aop.aop.MethodLogger)")
public void afterMethodStart(JoinPoint point) {
System.out.println("Method " + point.getSignature().getName() + " Ended at " + LocalDateTime.now());
}
@Around("@annotation(com.budnamu.sample.aop.aop.CalculatePerformance)")
public void calculate(ProceedingJoinPoint point) {
LocalTime startTime = LocalTime.now();
try {
point.proceed();
} catch (Throwable e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
LocalTime endTime = LocalTime.now();
System.out.println("Processing time of Method " + point.getSignature().getName() + " -> "
+ SECONDS.between(startTime, endTime));
}
}
}
Service
annotaion을 적용할 메소드에 선언하면 aspect가 알아서 처리해 준다
getEmployee 메소드를 보면 @CalculatePerformance가 선언되어 있는데 이것은 메소드 호출 전후에 적용된다
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import org.springframework.stereotype.Component;
import com.budnamu.sample.aop.aop.CalculatePerformance;
import com.budnamu.sample.aop.aop.MethodLogger;
import com.budnamu.sample.aop.model.Employee;
@Component
public class EmployeeServiceImpl implements EmployeeService {
private static List<Employee> employeeLst = new ArrayList<>();
@Override
@CalculatePerformance
public Employee getEmployee(String empId) {
Stream<Employee> empStream = employeeLst.stream().filter(emp -> {
return emp.getEmpId().equalsIgnoreCase(empId);
});
sleepForSeconds(5000L);
return empStream.findAny().get();
}
@Override
@MethodLogger
public String addEmployee(Employee e) {
employeeLst.add(e);
sleepForSeconds(3000L);
return "Success";
}
@Override
@MethodLogger
public String updateEmployeeDept(String empId, String deptName) {
employeeLst.stream().forEach(emp -> {
if (emp.getEmpId().equalsIgnoreCase(empId)) {
emp.setDeptName(deptName);
}
});
sleepForSeconds(2000L);
return "SUCCESS";
}
@Override
@MethodLogger
public String deleteEmployee(String empId) {
System.out.println("Employee Id -->" + empId);
if (employeeLst.removeIf(emp -> emp.getEmpId().equalsIgnoreCase(empId)))
return "SUCCESS";
else
return "FAIURE";
}
public void sleepForSeconds(Long period) {
try {
Thread.sleep(period);
} catch (Exception e) {
e.printStackTrace();
}
}
static {
Employee emp1 = new Employee();
Employee emp2 = new Employee();
// employee 1
emp1.setEmpId("A1234");
emp1.setEmpName("Sam");
emp1.setDeptName("IT");
// employee 2
emp2.setEmpId("B1234");
emp2.setEmpName("Tom");
emp2.setDeptName("Finance");
employeeLst.add(emp1);
employeeLst.add(emp2);
}
}
등록
로그