一、AOP简介
AOP,Aspect Oriented Programming,面向切面编程,是对面向对象编程0OP的升华。OOP是纵向对一个事物的抽象,一个对象包括静态的属性信息,包括动态的方法信息等。而AOP是横向的对不同事物的抽象,属性与属性、方法与方法、对象与对象都可以组成一个切面, 而用这种思维去设计编程的方式叫做面向切面编程。
二、AOP思想的实现方案
动态代理技术,在运行期间,对目标对象的方法进行增强,代理对象同名方法内可以执行原有逻辑的同时嵌入执行其他增强逻辑或其他对象的方法。
例如:
A对象为我们要增强的目标对象,B对象为增强方法所在的对象。在动态代理后,我们会获得一个A对象的
接下来我们通过这种方式来增强spring容器中的一个Bean
(项目创建成功之后的目录)不要忘了,每个bean都需要被spring容器管理,所以需要在类上添加注解@Component或者@Service
① 首先创建一个项目,创建Userservice接口,并实现它,这个UserServiceImpl对象就相当于A对象
public interface UserService {public void show1();public void show2();
}
@Service
public class UserServiceImpl implements UserService {@Overridepublic void show1() {System.out.println("UserServiceImpl show1");}@Overridepublic void show2() {System.out.println("UserServiceImpl show2");}
}
②然后创建增强类Advice,相当于B对象
@Component
public class MyAdvice {public void beforeAdvice(){System.out.println("前置增强...");}public void afterAdvice(){System.out.println("后置增强...");}
}
③最后创建一个Bean的后置处理类,这个后置处理类会返回一个被增强的bean,相当于A对象的Proxy对象
@Component
public class MockAopBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware {private ApplicationContext applicationContext;@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {//目的:对UserServiceImpl中的show1和show2方法进行增强,增强方法存在于MyAdvice中//问题1:筛选service.impl包下的所有方法都可以进行增强,解决方法是if-else//问题2:如果获取MyAdvice,解决方案从Spring容器中获取//加一个条件判断,只有在"com.itheima.service.impl"包下的bean才能够被增强if(bean.getClass().getPackage().getName().equals("com.itheima.service.impl")){//使用Proxy类生成当前获取到的对象的代理对象Object beanProxy = Proxy.newProxyInstance(bean.getClass().getClassLoader(), bean.getClass().getInterfaces(), new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//获取增强方法所在的对象MyAdvice advice = applicationContext.getBean(MyAdvice.class);//执行前置增强方法advice.beforeAdvice();//执行目标方法Object result = method.invoke(bean,args);//执行bean的目标方法,携带参数//执行后置增强方法advice.afterAdvice();return result;}});return beanProxy;}return bean;}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}
}
三、Spring_AOP的相关概念
四、基于xml配置的AOP
1.xml方式AOP快速入门
2.步骤
1.导入AOP相关坐标
2.准备目标类、准备增强类、并配置给Spring管理
3.配置切点表达式(哪些方法被增强)
4.配置织入(切点被哪些方法增强,是前置增强还是后置增强)
五、基于注解的方式配置的AOP
1.AOP的注解介绍
注解 | 作用 |
---|---|
@Aspect | 切面声明 ,标注在类、接口(包括注解类型)或枚举上。 |
@Pointcut | 切入点声明 ,即切入到哪些目标类的目标方法。既可以用 execution 切点表达式, 也可以是annotation 指定拦截拥有指定注解的方法。value 属性指定切入点表达式,默认为 “”,用于被通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式 |
@Before | 前置通知 , 在目标方法(切入点)执行之前执行。value 属性绑定通知的切入点表达式,可以关联切入点声明,也可以直接设置切入点表达式。注意:如果在此回调方法中抛出异常,则目标方法不会再执行,会继续执行后置通知 -> 异常通知。 |
@After | 后置通知 , 在目标方法(切入点)执行之后执行 |
@AfterReturning | 返回通知 , 在目标方法(切入点)返回结果之后执行。pointcut 属性绑定通知的切入点表达式,优先级高于 value,默认为 “” |
@AfterThrowing | 异常通知 , 在方法抛出异常之后执行, 意味着跳过返回通知,pointcut 属性绑定通知的切入点表达式,优先级高于 value,默认为 " " 注意:如果目标方法自己 try-catch 了异常,而没有继续往外抛,则不会进入此回调函数 |
@Around | 环绕通知 :目标方法执行前后分别执行一些代码,类似拦截器,可以控制目标方法是否继续执行。通常用于统计方法耗时,参数校验等等操作。 |
正常流程:【环绕通知-前】-> 【前置通知】-> 【返回通知】-> 【后置通知】->【环绕通知-后】。
2.AOP的切点表达式
切点表达式是配置要对哪些连接点(哪些类的哪些方法)进行通知的增强,语法如下:
execution ( [访问修饰符] 返回值类型 包名.类名.方法名(参数) )
其中:
1.访问修饰符可以省略不写
2.返回值类型、某一级的包名、类名、方法名可以使用*
表示任意
3.包名与类型之间使用单点.
表示该包下的类,使用双点 ..
表示该包及其子包下的所有类
4.参数列表可以使用两个点..
表示任意参数
例如 "execution(* com.itheima.service.impl.*.*(..))"
表示权限修饰符默认、任意返回值类型、com.itheima.service.impl包下、所有类、所有方法、任意个参数
3.注解方式AOP快速入门
4.步骤
1.导入AOP相关坐标
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.准备目标类并配置给Spring管理
@Service
public class UserServiceImpl implements UserService {@Overridepublic void show1() {
// int a = 1/0;System.out.println("UserServiceImpl show1");}@Overridepublic void show2() {System.out.println("UserServiceImpl show2");}
}
3.使用注解配置切点、切面类、通知类型等
@Component
@Aspect
public class MyAdvice2 {@Before("execution(* com.itheima.service.impl.*.*(..))")public void beforeAdvice(JoinPoint joinPoint){System.out.println("当前增强的对象是:"+joinPoint.getTarget());System.out.println("表达式:"+joinPoint.getStaticPart());System.out.println("注解配置的前置增强...");System.out.println("======================");}@AfterReturning("execution(* com.itheima.service.impl.*.*(..))")public void afterReturningAdvice(JoinPoint joinPoint){System.out.println("当前增强的对象是:"+joinPoint.getTarget());System.out.println("表达式:"+joinPoint.getStaticPart());System.out.println("注解配置的后置增强...");System.out.println("======================");}@Around("execution(* com.itheima.service.impl.*.*(..))")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("当前增强的对象是:"+proceedingJoinPoint.getTarget());System.out.println("表达式:"+proceedingJoinPoint.getStaticPart());System.out.println("环绕的前置增强");Object proceed = proceedingJoinPoint.proceed();//执行目标方法System.out.println("环绕的后置增强");System.out.println("======================");return proceed;}@AfterThrowing(pointcut = "execution(* com.itheima.service.impl.*.*(..))",throwing = "e")public void afterThrowingAdvice(Throwable e){System.out.println("当前的异常信息为:"+e);System.out.println("异常抛出通知...报异常才执行");System.out.println("======================");}@After("execution(* com.itheima.service.impl.*.*(..))")public void afterAdvice(){System.out.println("最终的增强");System.out.println("======================");}
}
4.启动类注解@EnableAspectJAutoProxy开启Spring AOP
@SpringBootApplication
@EnableAspectJAutoProxy
public class SpringAopTest01Application {public static void main(String[] args) {SpringApplication.run(SpringAopTest01Application.class, args);}}
在第三步,使用注解配置通知类型和切点时,五种通知类型中都使用了"execution(* com.itheima.service.impl.*.*(..))"
。这样显然是不够优雅的。
我们可以将切点抽取出来:
@Component
@Aspect
public class MyAdvice2 {//抽取切点出来@Pointcut("execution(* com.itheima.service.impl.*.*(..))")public void myPointCut(){}// @Before("execution(* com.itheima.service.impl.*.*(..))")@Before("myPointCut()")public void beforeAdvice(JoinPoint joinPoint){System.out.println("当前增强的对象是:"+joinPoint.getTarget());System.out.println("表达式:"+joinPoint.getStaticPart());System.out.println("注解配置的前置增强...");System.out.println("======================");}// @AfterReturning("execution(* com.itheima.service.impl.*.*(..))")@AfterReturning("myPointCut()")public void afterReturningAdvice(JoinPoint joinPoint){System.out.println("当前增强的对象是:"+joinPoint.getTarget());System.out.println("表达式:"+joinPoint.getStaticPart());System.out.println("注解配置的后置增强...");System.out.println("======================");}// @Around("execution(* com.itheima.service.impl.*.*(..))")@Around("myPointCut()")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("当前增强的对象是:"+proceedingJoinPoint.getTarget());System.out.println("表达式:"+proceedingJoinPoint.getStaticPart());System.out.println("环绕的前置增强");Object proceed = proceedingJoinPoint.proceed();//执行目标方法System.out.println("环绕的后置增强");System.out.println("======================");return proceed;}// @AfterThrowing(pointcut = "execution(* com.itheima.service.impl.*.*(..))",throwing = "e")@AfterThrowing(value = "myPointCut()",throwing = "e")public void afterThrowingAdvice(Throwable e){System.out.println("当前的异常信息为:"+e);System.out.println("异常抛出通知...报异常才执行");System.out.println("======================");}// @After("execution(* com.itheima.service.impl.*.*(..))")@After("myPointCut()")public void afterAdvice(){System.out.println("最终的增强");System.out.println("======================");}
}
这样基于注解方式的AOP就编写完成了,我们测试一下,调用com.itheima.service.impl包下的类的方法
运行结果:
可以看到这些增强方法都执行了,而且我们并没有修改目标方法。一点原代码都没动,直接在切面类上配置需要被增强的方法的签名,就实现了方法增强,这就是AOP。
六、使用AOP实现日志功能
上面我们使用AOP实现了对方法的增强,这里我们继续使用AOP实现项目中常用的日志记录功能。
1.思路
在每次方法的之前记录当前时间,执行之后记录当前时间,获取方法执行耗时。
在方法执行之后记录方法所在的模块
、执行的操作
、方法的名称
、方法的参数
、请求发起的IP地址
。
使用环绕通知类型实现该功能。
2.步骤
- 1.前提准备:一个IP地址获取的工具类、一个获取HttpServletRequest的工具类
package com.itheima.util;import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;/*** IP相关工具类** @author xiegege* @date 2021/02/22 16:08*/
@Slf4j
public class IpUtil {/*** 获取当前网络ip*/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 ("127.0.0.1".equals(ipAddress) || "0:0:0:0:0:0:0:1".equals(ipAddress)) {// 根据网卡取本机配置的IPInetAddress inet = null;try {inet = InetAddress.getLocalHost();} catch (UnknownHostException e) {e.printStackTrace();}ipAddress = inet.getHostAddress();}}//对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 //"***.***.***.***".length() = 15if (ipAddress != null && ipAddress.length() > 15) {if (ipAddress.indexOf(",") > 0) {ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));}}return ipAddress;}/*** 获取真实IP*/public static String getRealIp(HttpServletRequest request) {String ip = request.getHeader("x-forwarded-for");return checkIp(ip) ? ip : (checkIp(ip = request.getHeader("Proxy-Client-IP")) ? ip : (checkIp(ip = request.getHeader("WL-Proxy-Client-IP")) ? ip :request.getRemoteAddr()));}/*** 校验IP*/private static boolean checkIp(String ip) {return !StringUtils.isEmpty(ip) && !"unknown".equalsIgnoreCase(ip);}/*** 获取操作系统,浏览器及浏览器版本信息*/public static Map<String, String> getOsAndBrowserInfo(HttpServletRequest request) {String userAgent = request.getHeader("User-Agent");String user = userAgent.toLowerCase();String os;String browser = "";//=================OS Info=======================if (userAgent.toLowerCase().contains("windows")) {os = "Windows";} else if (userAgent.toLowerCase().contains("mac")) {os = "Mac";} else if (userAgent.toLowerCase().contains("x11")) {os = "Unix";} else if (userAgent.toLowerCase().contains("android")) {os = "Android";} else if (userAgent.toLowerCase().contains("iphone")) {os = "IPhone";} else {os = "UnKnown, More-Info: " + userAgent;}//===============Browser===========================try {if (user.contains("edge")) {browser = (userAgent.substring(userAgent.indexOf("Edge")).split(" ")[0]).replace("/", "-");} else if (user.contains("msie")) {String substring = userAgent.substring(userAgent.indexOf("MSIE")).split(";")[0];browser = substring.split(" ")[0].replace("MSIE", "IE") + "-" + substring.split(" ")[1];} else if (user.contains("safari") && user.contains("version")) {browser = (userAgent.substring(userAgent.indexOf("Safari")).split(" ")[0]).split("/")[0]+ "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1];} else if (user.contains("opr") || user.contains("opera")) {if (user.contains("opera")) {browser = (userAgent.substring(userAgent.indexOf("Opera")).split(" ")[0]).split("/")[0]+ "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1];} else if (user.contains("opr")) {browser = ((userAgent.substring(userAgent.indexOf("OPR")).split(" ")[0]).replace("/", "-")).replace("OPR", "Opera");}} else if (user.contains("chrome")) {browser = (userAgent.substring(userAgent.indexOf("Chrome")).split(" ")[0]).replace("/", "-");} else if ((user.contains("mozilla/7.0")) || (user.contains("netscape6")) ||(user.contains("mozilla/4.7")) || (user.contains("mozilla/4.78")) ||(user.contains("mozilla/4.08")) || (user.contains("mozilla/3"))) {browser = "Netscape-?";} else if (user.contains("firefox")) {browser = (userAgent.substring(userAgent.indexOf("Firefox")).split(" ")[0]).replace("/", "-");} else if (user.contains("rv")) {String ieVersion = (userAgent.substring(userAgent.indexOf("rv")).split(" ")[0]).replace("rv:", "-");browser = "IE" + ieVersion.substring(0, ieVersion.length() - 1);} else {browser = "UnKnown";}} catch (Exception e) {log.error("获取浏览器版本失败");log.error(e.getMessage());browser = "UnKnown";}Map<String, String> result = new HashMap<>(2);result.put("OS", os);result.put("BROWSER", browser);return result;}/*** 判断是否是内网IP*/public static boolean internalIp(String ip) {byte[] addr = textToNumericFormatV4(ip);return internalIp(addr) || "127.0.0.1".equals(ip);}private static boolean internalIp(byte[] addr) {if (StringUtils.isEmpty(addr) || addr.length < 2) {return true;}final byte b0 = addr[0];final byte b1 = addr[1];// 10.x.x.x/8final byte SECTION_1 = 0x0A;// 172.16.x.x/12final byte SECTION_2 = (byte) 0xAC;final byte SECTION_3 = (byte) 0x10;final byte SECTION_4 = (byte) 0x1F;// 192.168.x.x/16final byte SECTION_5 = (byte) 0xC0;final byte SECTION_6 = (byte) 0xA8;switch (b0) {case SECTION_1:return true;case SECTION_2:if (b1 >= SECTION_3 && b1 <= SECTION_4) {return true;}case SECTION_5:if (b1 == SECTION_6) {return true;}default:return false;}}/*** 将IPv4地址转换成字节** @param text IPv4地址* @return byte 字节*/public static byte[] textToNumericFormatV4(String text) {if (text.length() == 0) {return null;}byte[] bytes = new byte[4];String[] elements = text.split("\\.", -1);try {long l;int i;switch (elements.length) {case 1:l = Long.parseLong(elements[0]);if ((l < 0L) || (l > 4294967295L)) {return null;}bytes[0] = (byte) (int) (l >> 24 & 0xFF);bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF);bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);bytes[3] = (byte) (int) (l & 0xFF);break;case 2:l = Integer.parseInt(elements[0]);if ((l < 0L) || (l > 255L)) {return null;}bytes[0] = (byte) (int) (l & 0xFF);l = Integer.parseInt(elements[1]);if ((l < 0L) || (l > 16777215L)) {return null;}bytes[1] = (byte) (int) (l >> 16 & 0xFF);bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);bytes[3] = (byte) (int) (l & 0xFF);break;case 3:for (i = 0; i < 2; ++i) {l = Integer.parseInt(elements[i]);if ((l < 0L) || (l > 255L)) {return null;}bytes[i] = (byte) (int) (l & 0xFF);}l = Integer.parseInt(elements[2]);if ((l < 0L) || (l > 65535L)) {return null;}bytes[2] = (byte) (int) (l >> 8 & 0xFF);bytes[3] = (byte) (int) (l & 0xFF);break;case 4:for (i = 0; i < 4; ++i) {l = Integer.parseInt(elements[i]);if ((l < 0L) || (l > 255L)) {return null;}bytes[i] = (byte) (int) (l & 0xFF);}break;default:return null;}} catch (NumberFormatException e) {return null;}return bytes;}/*** 获取IP*/public static String getHostIp() {try {return InetAddress.getLocalHost().getHostAddress();} catch (UnknownHostException e) {e.printStackTrace();}return "127.0.0.1";}/*** 获取主机名*/public static String getHostName() {try {return InetAddress.getLocalHost().getHostName();} catch (UnknownHostException e) {e.printStackTrace();}return "未知";}
}
这里使用了RequestContextHolder的静态方法来获取HttpServletRequest,RequestContextHolder的获取需要引入javaweb相关的依赖,springboot可以直接引入spring-boot-starter-web
package com.itheima.util;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/*** @author MrBird*/
public class HttpContextUtil {private HttpContextUtil(){}//获取HttpServletRequest请求public static HttpServletRequest getHttpServletRequest() {return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();}
}
- 2.编写自定义注解
该注解用于标记方法,被标记的方法会被增强
package com.itheima.myAnnotation;
import java.lang.annotation.*;/*** @author Watching* * @date 2023/8/14* * Describe:标记 方法需要被记录日志*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {//方法所属模块String module() default "";//方法执行的操作String operation() default "";
}
- 3.编写一个TestLogService接口并实现它,并在实现类的方法上面添加自定义注
TestLogService接口
public interface TestLogService {public void func1();public void func2();public void func3();
}
TestLogServiceImpl实现类
@Service
public class TestLogServiceImpl implements TestLogService {@Override@LogAnnotation(module = "测试模块",operation = "存钱")public void func1() {//TODO 假如这个方法是存钱方法}@Override@LogAnnotation(module = "测试模块",operation = "取钱")public void func2() {//TODO 假如这个方法是取钱方法}@Override@LogAnnotation(module = "测试模块",operation = "查询余额")public void func3() {//TODO 假如这个方法是查看多少钱方法}
}
- 4.编写切面类
package com.itheima.advice;import com.alibaba.fastjson.JSON;
import com.itheima.myAnnotation.LogAnnotation;
import com.itheima.util.HttpContextUtil;
import com.itheima.util.IpUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;/*** @author Watching* * @date 2023/8/14* * Describe:切面类,实现日志记录*/
@Component
@Aspect
@Slf4j
public class MyAdvice3 {//定义切点@Pointcut("@annotation(com.itheima.myAnnotation.LogAnnotation)")public void pt() {}@AfterThrowing(value = "pt()",throwing = "e")public void ifError(Throwable e){System.out.println("当前的异常信息为:"+e);System.out.println("异常抛出通知...报异常才执行");System.out.println("======================");e.printStackTrace();//打印异常的堆栈轨迹信息}//编写环绕通知@Around("pt()")public Object log(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {long start = System.currentTimeMillis();Object proceed = proceedingJoinPoint.proceed();long end = System.currentTimeMillis();long time = end - start;//记录日志recordLog(proceedingJoinPoint, time);return proceed;}//编写日志记录函数private void recordLog(ProceedingJoinPoint joinPoint, long time) {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);log.info("=================log start==========================");//方法所在模块,执行的操作log.info("module: {}", logAnnotation.module());log.info("operation: {}", logAnnotation.operation());//请求的方法名String className = joinPoint.getTarget().getClass().getName();String methodName = signature.getName();log.info(" request method:{}", className + "." + methodName + "()");//请求的参数Object[] args = joinPoint.getArgs();if (args.length == 0) {log.info("params:{}", "null");} else {String params = JSON.toJSONString(args[0]);log.info("params:{}", params);}//获取request设置IP地址HttpServletRequest request = HttpContextUtil.getHttpServletRequest();log.info("ip:{}", IpUtil.getIpAddr(request));log.info("excute time : {} ms", time);log.info("=================log end=======================");}
}
编写完成,写个测试类测试一下
package com.itheima;
import com.itheima.service.TestLogService;
import com.itheima.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
class SpringAopTest01ApplicationTests {@ResourceUserService userService;@ResourceTestLogService logService;
// @Test
// void contextLoads() {
// userService.show1();
// }@Testvoid test(){logService.func1();logService.func2();logService.func3();}
}
测试结果:完成、每个方法执行后都输出了日志,当我们的某个方法出问题时,就可以根据这个日志快速定位