Backend Development 16 min read

Implementing Business Operation Logging with Spring AOP in a Java Backend

This article explains how to design and implement a business operation logging feature in a Spring Boot application using custom annotations and Spring AOP, covering requirement analysis, pitfalls of a naïve implementation, AOP design, code examples, and testing procedures.

Top Architect
Top Architect
Top Architect
Implementing Business Operation Logging with Spring AOP in a Java Backend

Introduction

The author, a senior architect, shares a long‑awaited tutorial on building a business operation log management module that records who performed which operation, when, and the request/response payloads, with optional rollback capability.

Requirement Analysis

Clients need a simple feature: record user actions (operator, time, function, log type, description, request and response messages) and provide a visual page for querying and rolling back critical operations.

Bad (Anti‑Pattern) Implementation

Initially each controller method directly writes log code and catches exceptions, leading to heavy coupling between business logic and logging, duplicated code, and the need to modify every existing endpoint when the logging logic changes.

@RestController
@Slf4j
@BusLog(name = "人员管理")
@RequestMapping("/person")
public class PersonController2 {
    @Autowired
    private IPersonService personService;
    @Autowired
    private IBusLogService busLogService;
    // Add person
    @PostMapping
    public Person add(@RequestBody Person person) {
        try {
            Person result = this.personService.registe(person);
            this.saveLog(person);
            log.info("//增加person执行完成");
        } catch (Exception e) {
            this.saveExceptionLog(e);
        }
        return result;
    }
}

This approach tightly couples logging with business code and is hard to maintain.

Design Idea – Use AOP

By defining a custom @BusLog annotation and an AOP aspect, logging can be woven around annotated methods without touching the core business logic.

Why Not Filter or HandlerInterceptor?

Filters work only in servlet containers and cannot modify request/response data.

HandlerInterceptor is limited to Spring MVC and shares the same constraints as filters.

Spring AOP can be applied to any layer (controller, service) and offers finer‑grained advice types (before, after, around, etc.).

Implementation Steps

Environment

JDK 1.8

IntelliJ IDEA 2020.1

Spring Boot 2.3.9.RELEASE

mybatis-spring-boot-starter 2.1.4

Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Database Table

create table if not exists bus_log (
    id bigint auto_increment comment '自增id' primary key,
    bus_name varchar(100) null comment '业务名称',
    bus_descrip varchar(255) null comment '业务操作描述',
    oper_person varchar(100) null comment '操作人',
    oper_time datetime null comment '操作时间',
    ip_from varchar(50) null comment '操作来源ip',
    param_file varchar(255) null comment '操作参数报文文件'
) comment '业务操作日志' default charset='utf8';

Custom Annotation

/**
 * Business log annotation
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BusLog {
    /** Function name */
    String name() default "";
    /** Function description */
    String descrip() default "";
}

Controller Example

@RestController
@Slf4j
@BusLog(name = "人员管理")
@RequestMapping("/person")
public class PersonController {
    @Autowired
    private IPersonService personService;
    private Integer maxCount = 100;

    @PostMapping
    @NeedEncrypt
    @BusLog(descrip = "添加单条人员信息")
    public Person add(@RequestBody Person person) {
        Person result = this.personService.registe(person);
        log.info("//增加person执行完成");
        return result;
    }

    @PostMapping("/batch")
    @BusLog(descrip = "批量添加人员信息")
    public String addBatch(@RequestBody List
personList) {
        this.personService.addBatch(personList);
        return String.valueOf(System.currentTimeMillis());
    }

    @GetMapping
    @NeedDecrypt
    @BusLog(descrip = "人员信息列表查询")
    public PageInfo
list(Integer page, Integer limit, String searchValue) {
        PageInfo
pageInfo = this.personService.getPersonList(page, limit, searchValue);
        log.info("//查询person列表执行完成");
        return pageInfo;
    }
    // other CRUD methods omitted for brevity
}

AOP Aspect

@Component
@Aspect
@Slf4j
public class BusLogAop implements Ordered {
    @Autowired
    private BusLogDao busLogDao;

    /** Pointcut for methods annotated with @BusLog */
    @Pointcut("@annotation(com.fanfu.anno.BusLog)")
    public void pointcut() {}

    /** Around advice */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) {
        log.info("----BusAop 环绕通知 start");
        Object result = null;
        try {
            result = proceedingJoinPoint.proceed();
        } catch (Throwable t) {
            t.printStackTrace();
        }
        // Retrieve annotation data
        Object target = proceedingJoinPoint.getTarget();
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        BusLog classAnno = target.getClass().getAnnotation(BusLog.class);
        BusLog methodAnno = signature.getMethod().getAnnotation(BusLog.class);
        BusLogBean bean = new BusLogBean();
        bean.setBusName(classAnno.name());
        bean.setBusDescrip(methodAnno.descrip());
        bean.setOperPerson("fanfu");
        bean.setOperTime(new Date());
        // Serialize arguments to JSON and write to file
        JsonMapper mapper = new JsonMapper();
        String json = null;
        try { json = mapper.writeValueAsString(proceedingJoinPoint.getArgs()); } catch (JsonProcessingException e) { e.printStackTrace(); }
        String paramFilePath = System.getProperty("user.dir") + File.separator +
                DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN) + ".log";
        try (OutputStream os = new FileOutputStream(paramFilePath)) {
            os.write(json.getBytes(StandardCharsets.UTF_8));
            bean.setParamFile(paramFilePath);
        } catch (IOException e) { e.printStackTrace(); }
        // Persist log record
        this.busLogDao.insert(bean);
        log.info("----BusAop 环绕通知 end");
        return result;
    }

    @Override
    public int getOrder() { return 1; }
}

Testing

The author recommends using IntelliJ IDEA’s built‑in HTTP client as an alternative to Postman, showing how to send requests and view responses directly from the IDE.

Conclusion

The logging solution records function name, description, operator, timestamp, and request payloads (stored as files for rollback scenarios). By separating logging concerns with AOP, the code stays clean, reusable, and easy to maintain.

JavaBackend DevelopmentMySQLSpring AOPAspect-Oriented ProgrammingBusiness Logging
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.