spring-AOP和springboot开发中日志的使用

一.AOP概念
1.什么是aop
(1)面向切面编程(方面),利用aop可以对业务的各个部分进行隔离,从而使得业务逻辑各个部分之间的耦合度降低,提高程序的可重用性,同时提高开发的效率。
(2)通俗理解:不通过修改源代码的方式,在主干功能(方法)内部添加新功能
(3)横向抽取机制(同样是达到代码的复用性)(AOP思想):
在这里插入图片描述

二: AOP应用场景
场景一: 记录日志
场景二: 监控方法运行时间 (监控性能)
场景三: 权限控制
场景四: 缓存优化 (第一次调用查询数据库,将查询结果放入内存对象, 第二次调用, 直接从内存对象返回,不需要查询数据库 )
场景五: 事务管理 (调用方法前开启事务, 调用方法后提交关闭事务 )
三:AOP的实现原理
那Spring中AOP是怎么实现的呢?Spring中AOP的有两种实现方式:
1、JDK动态代理
2、Cglib动态代理
这里先用图片解释:
在这里插入图片描述
这里是我自己的一点理解:
在这里插入图片描述

然后进行代码底层去理解动态代理的运行原理:(jdk动态代理)
首先要知道一个类:Proxy(代理)和这个类中的一个方法newProxyInstance方法(后面用到再细说)。
编写jdk动态代理代码开始:
(1)创建接口,定义方法(业务中的一些增删改查等方法,可以是controller中的方法,这里用dao层接口演示)

package aspect;
//创建接口,定义方法
public interface UserDao {
    public int add(int a,int b);
    public String update(String id);
}

(2)创建接口的实现类(需要增强的类,被代理类

package aspect;
//创建接口的实现类
public class UserDaoImpl implements  UserDao {
    //实现类重写接口中的抽象方法。
    public int add(int a, int b) {
        return a+b;
    }

    public String update(String id) {
        return id;
    }
}

(3)创建一个单独的增强类(生产接口实现类的代理对象,并增强被代理类中的一些方法)并在增强类中使用Proxy类创建接口实现类的代理对象
这里可以总结为:3+3(三个东西(要增强的接口,接口的实现类,代理类/增强类),三个参数(代理类中newProxyInstance有三个参数))

package aspect;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Date;

//在此类中创建  接口的实现类   的 代理对象
public class UserDaoImplProxy {
    public static void main(String[] args) {
    /*
    Proxy.newProxyInstance(三个参数)
    第一个参数: 增强类的类加载器
    第二个参数:实现类实现的接口的数组,支持多个接口(要增强的接口)
    第三个参数:实现这个接口InvocationHandler,创建代理对象,里面写增强的逻辑部分
    * */
        //第一个参数:增强类的类加载器,UserDaoImplProxy.class.getClassLoader()
        //第二个参数:InvocationHandler,实现类实现的接口的数组,interfaces
        Class[] interfaces = {UserDao.class};
        //第三个参数:我们需要一个实现类,类中传一个要代理的对象(接口的实现类对象)
        UserDaoImpl userDaoImpl = new UserDaoImpl();
        //生成实现类的代理对象,然后返回成接口的形式,向上转型
        UserDao userDao= (UserDao)Proxy.newProxyInstance(UserDaoImplProxy.class.getClassLoader(),interfaces,new MyInvocationHandler(userDaoImpl));

        //然后我们在这里模拟调用代理对象的方法
        int res = userDao.add(1, 2);
        System.out.println("相加结果:"+res);

    }
}
/*本文件的另一个类,实现了InvocationHandler接口,创建代理对象的代码:*/
class MyInvocationHandler implements InvocationHandler{
	//invoke方法是当代理对象被创建的时候,就自动调用。

   //(1.1)把创建的是谁的代理对象(这里是实现类),把谁传递过去
   //类似于生活中,代理方一定要明确帮谁(被代理方)代理做事,中介帮委托人办事等
    private Object obj;//obj是委托人/被代理人
 
    //(1.2)有参构造传递被代理对象
    public MyInvocationHandler(Object obj) {
        this.obj = obj;
    }
    //(2)增强的逻辑部分/代理过程。会将这部分代码横向插入
    //到我们写号的代码中(自动插入),而不需要改变源代码。
	//代理过程细节需要:参数 proxy 指代理类,method表示被代理的方法,args为 代理方法中的参数数组
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //(2.1).方法之前,这里做一个性能时间统计,,// 开始时间
        long stime = System.currentTimeMillis();
        Thread.sleep(100);//耗时代码
        System.out.println("方法之前执行。。。"+method.getName()+"我们调用的方法的传递的参数:"+ Arrays.toString(args));

        //(2.2)放行增强的方法去执行。(invoke类似一个拦截器,方法调用的时候,就被拦截)
        //obj为真实对象/被代理对象;去调用自己类中的方法即method,args为方法参数。类似于对象.方法(参数);
        Object invoke = method.invoke(obj, args);

        //(2.3).方法之后,// 结束时间
        long etime = System.currentTimeMillis();
        System.out.println("方法之后执行..."+obj+"执行时长:"+(etime - stime)+"毫秒");
        //不要忘了返回invoke,不然相加的返回值不能得到。即不能返回一些结果
        return invoke;
    }
}

被覆盖的
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {…} 方法,要求三个参数。“参数 proxy 指代理类,method表示被代理的方法,args为 method 中的参数数组,返回值Object为代理实例的方法调用返回的值。这个抽象方法在代理类中动态实现”。
然后直接运行增强类的main方法,测试结果。

总结jdk动态代理:
三个东西加三个参数:第一个是接口,第二个是此接口的实现类。第三个是增强类:其中增强类中主要核心是UserDao userDao= (UserDao)Proxy.newProxyInstance(UserDaoImplProxy.class.getClassLoader(),interfaces,new MyInvocationHandler(userDaoImpl));用于生成代理对象。然后用代理对象调用接口中的方法。

在这里插入图片描述

四:AOP术语:

针对方法的:
1.连接点:(可 连接/增强 的 点/方法)
类里面哪些方法可以被增强,这些方法
2.切入点:实际被真正增强的方法
针对部分代码逻辑的:
3.通知(增强)
(1)实际增强的逻辑部分称为通知(增强)
(2)5种通知:
前置通知:@Before 在增强的方法前执行
后置通知:@After 在增强的方法后执行
环绕通知:@Around 在增强的方法前后都执行
异常通知:@AfterThowing 当发生异常时候 执行
最终通知:@AfterReturnning 返回值之后执行。
针对动作:
4切面:把通知应用到切入点的过程
在这里插入图片描述

五:AOP的实操

spring框架一般都基于AspectJ实现AOP操作:有两种方式:基于xml(不推荐),基于注解方式(推荐,springboot也用)
第一步:导包/引入依赖(springboot)

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

或者导入spring-aop的jar和aspectj.weaver的jar.spring-aspect,aoplliance.jar等.
第二步:
切入点表达式作用:知道对哪个类里面的哪个方法进行增强
语法:execution([权限修饰符][返回类型]【类全路径】【方法名称】([参数列表]))
举例:

切入点表达式可以参考此链接:
https://blog.csdn.net/qq_42764468/article/details/102767947

//切入点:@Pointcut
//第一个*表示匹配任意的方法返回值, ..(两个点)表示零个或多个,第一个..表示controller包及其子包,
//第二个*表示所有类, 第三个*表示所有方法,第二个..表示方法的任意参数个数
    @Pointcut("execution(* com.fan.controller..*.*(..))")
    public void log() {
    }

第三步:相同切入点抽取

@Pointcut(“execution(* com.fan.controller….(…))”)
public void log() {
}
然后再其他通知中调用,如:

 @Before( value = "log()")
    public void before(){
        log.info("-----before-----");
    }

第四步:有多个增强类对同一个方法进行增强的时候,要设置增强优先级,
@Order(1),数字越小,等级越高,如:

@Slf4j/*此注解使用后,类上面添加@Sl4j注解,然后使用log打印日志, log.info("方法式拦截规则:" + method.getName());*/
@Aspect
@Component
@Order(1)//
public class LogAspect {
//增强的一些逻辑
}

演示springboot利用aop记录日志:

页面和需要增强的类和方法准备:(我这里要增强的是controler类中的所有方法)
在controller包下编写一个IndexController类

package com.fan.controller;

import com.fan.NotFoundException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@Controller
public class IndexController {
    @GetMapping("/{id}/{name}")
    public String index(@PathVariable Integer id,@PathVariable String name){
        //int i = 8/0;
       /* String blog = null;
        if (blog == null){
            throw   new NotFoundException("测试异常通知");
        }*/
        System.out.println("-----方法执行中的index------");
        return "testAoplog";
    }

}

另外要有一个testAoplog.html页面放到templates下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
测试aoplog
</body>
</html>

配置文件准备:
然后我们需要在开发环境下的配置文件application-dev.yml种配置日志的一些信息(在application.yml配置文件激活此文件profile.active),如下:

logging:
  level:
    root: info
    #分包配置级别,即不同的目录下可以使用不同的级别
    com.fan: debug
    #file.name生效了,用于指定日志的位置和名称
  file:
  #这样直接写,是在当前目录下生成这个文件名,也可以前面具体加文件路径d:/springboot.log
    name: springboot.log

然后application.yml主配置文件如下:

  spring:  
    profiles:  
      active: dev

然后我们还需要将日志用logback-spring.xml(此文件与application.yml文件平级)配置一些日志输出信息:

<?xml version="1.0" encoding="UTF-8"?>
<configuration  scan="true" scanPeriod="10 seconds">

    <contextName>logback</contextName>
    <!--补充输出sql到日志,logback-spring.xml里面添加如下代码(我的dao所在的包是com.fan.dao)-->
    <logger name="com.fan.dao" level="DEBUG"></logger>
    <!-- 彩色日志 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>


    <!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
    <property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />

    <!-- 定义日志存储的路径,不要配置相对路径 -->
    <property name="FILE_PATH" value="D:/springbootlog/log/spring-log.%d{yyyy-MM-dd}.%i.log" />

    <!-- 控制台输出日志 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 日志级别过滤INFO以下 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <!-- 按照上面配置的LOG_PATTERN来打印日志 -->
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <!--每天生成一个日志文件,保存30天的日志文件。rollingFile用来切分文件的 -->
    <appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

            <fileNamePattern>${FILE_PATH}</fileNamePattern>

            <!-- keep 15 days' worth of history -->
            <maxHistory>30</maxHistory>

            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!-- 日志文件的最大大小 -->
                <maxFileSize>2MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>

            <!-- 超出删除老文件 -->
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>

        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <!-- project default level -->
    <logger name="net.sh.rgface.serive" level="ERROR" />

    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="console" />
        <appender-ref ref="rollingFile" />
    </root>
</configuration>

增强类的准备(增强类中写增强的逻辑)
在我们自己的包下创建一个包aspect,在里面创建增强类com.fan.aspect.LogAspect:代码如下

package com.fan.aspect;
/*import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
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.stereotype.Component;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;*/

import com.alibaba.fastjson.JSONObject;
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.*;
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.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;

/*切面类*/
@Slf4j/*此注解使用后,类上面添加@Sl4j注解,然后使用log打印日志, log.info("方法式拦截规则:" + method.getName());*/
@Aspect
@Component
public class LogAspect {
    //切入点,  @annotation:匹配 执行方法持有指定注解;
    @Pointcut("execution(* com.fan.controller..*.*(..))")
    public void log() {
    }
    @After("log()")
    private Object doAfter(JoinPoint point)throws Throwable{
        long startTime = System.currentTimeMillis();
        Object result = null;
        try {
            /*环绕通知bai ProceedingJoinPoint 执行proceed方法的作用是让目标方法执行,
            这也是环绕通知和前置、后置通知方法的一个最大区别。
            简单理解,环绕通知=前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的*/
            //result = point.proceed();

            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();

            log.info("--- <><><><><><><><><><><><><><> ---");
            log.info("请求源IP:" + getIpAddr(request) );//获取远程id
            log.info("请求URL:" + request.getRequestURL());
            log.info("请求时间:" + new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(new Date()));
            /*point.getSignature().getDeclaringTypeName():获取类名;point.getSignature().getName():获取方法名 */
            log.info("请求方法:" + point.getSignature().getDeclaringTypeName() + "类.方法名:" + point.getSignature().getName());

            /*Object[] args = point.getArgs();
            LinkedList<Object> list = new LinkedList<>();
            for (Object arg : args) {
                list.add(arg);
                //log.info("请求参数:"+arg);
            }
            log.info("请求参数:{}", list);//自己用集合的形式放进去,这种方法比较笨,但是不需要导包/引入坐标*/


           /* JSONObject.toJSONString(Object):通常需要将一个实体对象转换成Json字符串,导入这个fastjson包/引入此坐标。阿里的*/
            log.info("请求参数:{}", JSONObject.toJSONString(point.getArgs()));//用这个工具将数据转成json字符串*/
            log.info("请求处理耗时:" + (System.currentTimeMillis() - startTime) + "毫秒");
            log.info("--- <><><><><><><><><><><><><><> ---");
        } catch (Throwable e) {
            log.info("{} Use time : {} ms with exception : ", point, (System.currentTimeMillis() - startTime), e.getMessage());
            throw e;
        }
        return result;
    }

    /**
     * 获取当前请求网络ip
     *
     * @param request
     * @return
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = request.getHeader("x-forwarded-for");
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
            if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
                InetAddress inet = null;
                try {
                    inet = InetAddress.getLocalHost();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
                ipAddress = inet.getHostAddress();
            }
        }
        if (ipAddress != null && ipAddress.length() > 15) {
            if (ipAddress.indexOf(",") > 0) {
                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
            }
        }
        return ipAddress;
    }
}

然后测试,浏览器输入:http://localhost:8080/1/hehe
查看控制台结果和日志文件结果。ok;

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值