Xây dựng custom annotation để lưu log Rest API trong ứng dụng Spring Boot dựa trên kỹ thuật AOP
Chúng mình có tạo Group cho các bạn cùng chia sẻ và học hỏi về thiết kế hệ thống nha 😄😄😄
Các bạn tham gia để gây dựng cộng đồng System Design Việt Nam thật lớn mạnh nhé 😍😍😍
Cộng Đồng System Design Việt Nam: https://www.facebook.com/groups/sydexa
Kênh TikTok: https://www.tiktok.com/@sydexa.com
Chắc hẳn khi lập trình java thì các bạn đã từng nghe tới khái niệm annotation, đặc biệt khi làm việc với Spring Boot thì việc sử dụng chúng là rất thường xuyên. Chúng ta có thể kể đến các annotation như @Component, @Service, @Controller,... Bạn đã bao giờ có ý định tự định nghĩa riêng một annotation để sử dụng cho mục đích của mình? Nghe thật thú vị đúng không, hôm nay chúng ta sẽ cùng bắt tay vào xây dựng custom annotation để lưu log các Rest API trong ứng dụng Spring Boot nhé
Chuẩn bị project demo
Trước tiên chúng ta cần khởi tạo một project spring boot mới để demo cho bài viết này. Truy cập Spring Initializr và khởi tạo project với các option và dependencies như sau:
Nhấn Generate
và tiến hành giải nén thư mục vừa tải về chúng ta được cấu trúc project như sau:
Với mục đích chính là xây dựng annotation lưu log nên mình xin phép đi nhanh qua các phần xây dựng cái rest API, cũng như lưu trữ cục bộ dữ liệu trong ứng dụng thay vì lưu vào db nhé. Chúng ta tiến hành tạo các package sau:
Package model
gồm 2 class:
BookDTO
package com.example.DemoLogAnnotation.model;
public class BooktDTO {
private long id;
private String name;
private String author;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
LogDTO
package com.example.DemoLogAnnotation.model;
import java.time.Instant;
public class LogDTO {
private Instant time;
private String message;
public Instant getTime() {
return time;
}
public void setTime(Instant time) {
this.time = time;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Package repository
gồm 2 class:
BookRepository
package com.example.DemoLogAnnotation.repository;
import com.example.DemoLogAnnotation.model.BooktDTO;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
@Repository
public class BookRepository {
private static final List<BooktDTO> books = new ArrayList<>();
public List<BooktDTO> getBooks() {
return books;
}
public void addBook(BooktDTO book) {
books.add(book);
}
}
LogRepository
package com.example.DemoLogAnnotation.repository;
import com.example.DemoLogAnnotation.model.LogDTO;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
@Repository
public class LogRepository {
private static final List<LogDTO> logs = new ArrayList<>();
public List<LogDTO> getLogs() {
return logs;
}
}
Package controller
và định nghĩa các API thêm Book, lấy danh sách Book và xem log hệ thống:
BookController
package com.example.DemoLogAnnotation.controller;
import com.example.DemoLogAnnotation.model.BooktDTO;
import com.example.DemoLogAnnotation.repository.BookRepository;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookRepository bookRepository;
public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@PostMapping
public BooktDTO createBook(@RequestBody BooktDTO booktDTO) {
bookRepository.addBook(booktDTO);
return booktDTO;
}
@GetMapping
public List<BooktDTO> getBooks() {
return bookRepository.getBooks();
}
}
LogController
package com.example.DemoLogAnnotation.controller;
import com.example.DemoLogAnnotation.model.LogDTO;
import com.example.DemoLogAnnotation.repository.LogRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/logs")
public class LogConrtoller {
private final LogRepository logRepository;
public LogConrtoller(LogRepository logRepository) {
this.logRepository = logRepository;
}
@GetMapping
public List<LogDTO> getLogs() {
return logRepository.getLogs();
}
}
Cấu trúc project lúc này sẽ như sau:
Như vậy là đã xong phần chuẩn bị, chúng ta cùng chuyển tới phần chính của bài viết thôi nào ^^
Tiến hành thêm thư viện Spring AOP
Aspect Oriented Programming (AOP) là 1 kỹ thuật lập trình dùng để tách logic chương trình thành các phần riêng biệt. Trong Spring AOP, có 4 loại advice được hỗ trợ:
- Before advice: chạy trước khi method được thực thi
- After returning advice: Chạy sau khi method trả về một kết quả
- After throwing adivce: Chạy khi method ném ra một exception
- Around advice: Chạy khi method được thực thi (Bao gồm cả 3 loại advice trên)
Các bạn có thể tìm hiểu thêm trên google đã có rất nhiều bài viết nói về khái niệm này. Ở đây chúng ta sẽ áp
dụng After returning advice
để xử lý lưu log của API sau khi hàm xử lý API đã trả ra kết quả.
Để thêm Spring AOP vào ứng dụng spring boot, ta tiến hành thêm vào phần dependencies trong file build.gradle
như sau:
dependencies {
/*
...
*/
implementation 'org.springframework.boot:spring-boot-starter-aop'
}
Sau đó tiến hành thêm @EnableAspectJAutoProxy
vào class main
@EnableAspectJAutoProxy
@SpringBootApplication
public class DemoLogAnnotationApplication {
public static void main(String[] args) {
SpringApplication.run(DemoLogAnnotationApplication.class, args);
}
}
Định nghĩa annotation, viết hàm xử lý và sử dụng nó
Tạo package annotation
và tiến hành khai báo SaveLog
annotation:
package com.example.DemoLogAnnotation.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SaveLog {
@AliasFor("value")
String message();
@AliasFor("message")
String value();
}
@Retention(RetentionPolicy.RUNTIME)
cho biết annotation có thể được truy cập qua reflection tại thời điểm runtime, nếu bạn không khai báo nó khi định nghĩa một annotation bạn sẽ không thể truy cập nó tại thời điểm runtime.@Target(ElementType.METHOD)
cho biết việc bạn chỉ có thể sử dụng annotation này trên các method, ở đây mục đích của chúng ta là sẽ khai báo annotation này trên các method trong controller nên ta sử dụng thuộc tính này
Tiến hành đánh @SaveLog lên các rest API chúng ta cần lưu log, ở đây mình cần lưu log tạo sách và lấy danh sách nên mình
thêm vào 2 chỗ sau trong class BookController
:
@SaveLog(message = "Tạo mới sách, với id = ${#booktDTO.getId()}")
@PostMapping
public BooktDTO createBook(@RequestBody BooktDTO booktDTO) {
bookRepository.addBook(booktDTO);
return booktDTO;
}
@SaveLog(message = "Lấy danh sách toàn bộ sách")
@GetMapping
public List<BooktDTO> getBooks() {
return bookRepository.getBooks();
}
Ở đây sử dụng một tính năng khác của Spring là Spring powerful expressions (SpEL) để có thể đọc được giá trị của các
đối số truyền vào bên trong method và sử dụng nó trong annotation của chúng ta: ${#booktDTO.getId()}
Giờ là đến phần quan trọng nhất của bài viết: Viết hàm xử lý lưu log
Tiến hành khai báo package aop
và tạo class SaveLogAspect như sau:
package com.example.DemoLogAnnotation.aop;
import com.example.DemoLogAnnotation.annotation.SaveLog;
import com.example.DemoLogAnnotation.model.LogDTO;
import com.example.DemoLogAnnotation.repository.LogRepository;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.Instant;
@Aspect
@Component
public class SaveLogAspect {
private static final TemplateParserContext TEMPLATE_PARSER_CONTEXT = new TemplateParserContext("${", "}");
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private final LogRepository logRepository;
public SaveLogAspect(LogRepository logRepository) {
this.logRepository = logRepository;
}
// Khai báo pointcut cho @SaveLog
@Pointcut("@annotation(com.example.DemoLogAnnotation.annotation.SaveLog)")
private void saveLogPointcut() {
}
// Xử lý sau khi hàm được chú thích bởi @SaveLog trả về kết quả
@AfterReturning("saveLogPointcut()")
public void afterReturningSaveLog(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Object[] args = joinPoint.getArgs();
Method targetMethod = signature.getMethod();
String[] parameterNames = signature.getParameterNames();
SaveLog saveLog = targetMethod.getAnnotation(SaveLog.class);
EvaluationContext evaluationContext = new StandardEvaluationContext();
for (int i = 0; i < args.length; i++) {
evaluationContext.setVariable(parameterNames[i], args[i]);
}
String message = EXPRESSION_PARSER.parseExpression(saveLog.message(), TEMPLATE_PARSER_CONTEXT).getValue(evaluationContext, String.class);
LogDTO logDTO = new LogDTO();
logDTO.setTime(Instant.now());
logDTO.setMessage(message);
logRepository.addLog(logDTO);
}
}
Tận hưởng thành quả
Giờ chúng ta khởi chạy project và sử dụng postman để test kết quả thôi
- Đầu tiên gọi POST
/api/books
để thêm sách
- Sau đó gọi GET
/api/books
để lấy danh sách:
- Giờ là phần đáng mong chờ nhất, gọi GET
/api/logs
để kiểm chứng @SaveLog của chúng ta hoạt động như mong đợi:
Tổng kết
Như vậy mình đã xây dựng thành công một custom annotation @SaveLog
để có thể đánh trên các method rest API mà chúng ta
muốn lưu log. Hy vọng bài viết này sẽ có ích với các bạn. Có nhiều phần trong bài viết mình trình bày ngắn gọn vắn tắt
để tránh làm cho bài viết dài dòng, các bạn có góp ý thì cứ để lại bình luận đóng góp nhé. Cảm ơn các bạn đã theo dõi
bài viết .
Lời nhắn
Chúng mình có tạo Group cho các bạn cùng chia sẻ và học hỏi về thiết kế hệ thống nha 😄😄😄
Các bạn tham gia để gây dựng cộng đồng System Design Việt Nam thật lớn mạnh nhé 😍😍😍
Cộng Đồng System Design Việt Nam: https://www.facebook.com/groups/sydexa
Kênh TikTok: https://www.tiktok.com/@sydexa.com
All rights reserved