springboot面向切面编程

功能

演示如何使用aop面向切面编程对http请求的日志进行记录。

注:这里使用的是spring aop和aspectj混合编程,如果不使用注解的方式(@Aspect)而使用xml方式,则不需要引入aspectj包

参考文档https://www.xzhongwei.com/post/308

新建项目

  • 开发工具:idea
  • Create New Project -> Spring Initializr ->next-> 添加项目信息
  • 添加依赖 web -> spring web

添加依赖

        <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>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.1.19</version>
        </dependency>
        <!-- 解析 UserAgent 信息 -->
        <dependency>
            <groupId>eu.bitwalker</groupId>
            <artifactId>UserAgentUtils</artifactId>
            <version>1.21</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>

controller

新建controller目录并在该目录下新建XzwController类

package indi.xzw.springboot_aop.controller;

import cn.hutool.core.lang.Dict;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@Slf4j
@RestController
public class XzwController {

    @GetMapping("/xzw")
    public Dict xzw(String who){
        return Dict.create().set("who", who.isBlank() ? "my name is xiazhongwei" : who);
    }

    @PostMapping("testJson")
    public Dict testJson(@RequestBody Map<String,Object> map){
        final String jsonStr = JSONUtil.toJsonStr(map);
        log.info(jsonStr);
        return Dict.create().set("json", map);
    }
}

运行

分别调用这两个接口

98

99

下面我们使用aop,给这些请求加上日志。

aop

新建目录aop并在该目录下新建AopLog类

package indi.xzw.springboot_aop.aop;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.json.JSONUtil;
import com.google.common.collect.Maps;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;

@Aspect
@Component
@Slf4j
public class AopLog {

    //切入点
    @Pointcut("execution(* indi.xzw.springboot_aop.controller.*XzwController.*(..))")
    public void myLog(){

    }

    /*
    * 环绕操作
     * @param point 切入点
     * @return 原方法返回值
     * @throws Throwable 异常信息
    * */
    @Around("myLog()")
    public Object aroundLog(ProceedingJoinPoint point) throws Throwable{
        //开始请求打印日志
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();

        // 打印请求相关参数
        long startTime = System.currentTimeMillis();
        Object result = point.proceed();
        String header = request.getHeader("User-Agent");
        UserAgent userAgent = UserAgent.parseUserAgentString(header);

        final Log l =Log.builder()
                .threadId(Long.toString(Thread.currentThread().getId()))
                .threadName(Thread.currentThread().getName())
                .ip(getIp(request))
                .url(request.getRequestURL().toString())
                .classMethod(String.format("%s.%s", point.getSignature().getDeclaringTypeName(),
                        point.getSignature().getName()))
                .httpMethod(request.getMethod())
                .requestParams(getNameAndValue(point))
                .result(result)
                .timeCost(System.currentTimeMillis() - startTime)
                .userAgent(header)
                .browser(userAgent.getBrowser().toString())
                .os(userAgent.getOperatingSystem().toString()).build();

        log.info("Request Log Info : {}", JSONUtil.toJsonStr(l));

        return result;
    }

    @Before("myLog()")
    public void beforeLog(JoinPoint joinPoint) {
        log.info("Before request Log Info : {}",joinPoint.toString());
    }

    /**
     *  获取方法参数名和参数值
     * @param joinPoint
     * @return
     */
    private Map<String, Object> getNameAndValue(ProceedingJoinPoint joinPoint) {

        final Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        final String[] names = methodSignature.getParameterNames();
        final Object[] args = joinPoint.getArgs();

        if (ArrayUtil.isEmpty(names) || ArrayUtil.isEmpty(args)) {
            return Collections.emptyMap();
        }
        if (names.length != args.length) {
            log.warn("{}方法参数名和参数值数量不一致", methodSignature.getName());
            return Collections.emptyMap();
        }
        Map<String, Object> map = Maps.newHashMap();
        for (int i = 0; i < names.length; i++) {
            map.put(names[i], args[i]);
        }
        return map;
    }

    private static final String UNKNOWN = "unknown";

    /**
     * 获取ip地址
     */
    public static String getIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        String comma = ",";
        String localhost = "127.0.0.1";
        if (ip.contains(comma)) {
            ip = ip.split(",")[0];
        }
        if (localhost.equals(ip)) {
            // 获取本机真正的ip地址
            try {
                ip = InetAddress.getLocalHost().getHostAddress();
            } catch (UnknownHostException e) {
                log.error(e.getMessage(), e);
            }
        }
        return ip;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class Log {
        // 线程id
        private String threadId;
        // 线程名称
        private String threadName;
        // ip
        private String ip;
        // url
        private String url;
        // http方法 GET POST PUT DELETE PATCH
        private String httpMethod;
        // 类方法
        private String classMethod;
        // 请求参数
        private Object requestParams;
        // 返回参数
        private Object result;
        // 接口耗时
        private Long timeCost;
        // 操作系统
        private String os;
        // 浏览器
        private String browser;
        // user-agent
        private String userAgent;
    }
}

运行

跟上面一样再次发送请求就会有打印信息,这里我们运行http://127.0.0.1:8080/xzw?who=zhongweixia

输出结果:

2020-12-30 11:19:47.993  INFO 15560 --- [nio-8080-exec-2] indi.xzw.springboot_aop.aop.AopLog       : Before request Log Info : execution(Dict indi.xzw.springboot_aop.controller.XzwController.xzw(String))
2020-12-30 11:19:47.996  INFO 15560 --- [nio-8080-exec-2] indi.xzw.springboot_aop.aop.AopLog       : Request Log Info : {"threadId":"30","classMethod":"indi.xzw.springboot_aop.controller.XzwController.xzw","result":{"who":"zhongweixia"},"os":"UNKNOWN","ip":"169.254.186.178","browser":"UNKNOWN","requestParams":{"who":"zhongweixia"},"userAgent":"PostmanRuntime/7.26.8","httpMethod":"GET","timeCost":2,"threadName":"http-nio-8080-exec-2","url":"http://127.0.0.1:8080/xzw"}

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×