一.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;