+8

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:

img.png

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:

img_1.png

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:

img_2.png

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

img_3.png

  • Sau đó gọi GET /api/books để lấy danh sách:

img_4.png

  • 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:

img_5.png

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

Viblo
Let's register a Viblo Account to get more interesting posts.