学习教程框架Spring入门-03
sinarcsinxSpring入门-03
AOP
概念
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程。他是一种可以在不修改原来的核心代码的情况下给程序动态统一进行增强的一种技术
SpringAOP: 批量对Spring容器中bean的方法做增强,并且这种增强不会与原来方法中的代码耦合。
快速入门
需求
要求让SpringAOP模块中service包下所有类的所有方法在调用前都输出:方法被调用了
准备工作
添加依赖
需要添加SpringIOC相关依赖和AOP相关依赖。
1 2 3 4 5 6 7 8 9 10 11 12
| <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.26</version> </dependency>
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.7</version> </dependency>
|
相关bean要注入容器中
开启组件扫描
1
| <context:component-scan base-package="com.sinarcsinx"></context:component-scan>
|
加@Service注解
1 2 3 4 5 6
| @Service public class PhoneService { public void deleteAll(){ System.out.println("PhoneService中deleteAll的核心代码"); } }
|
1 2 3 4 5 6
| @Service public class UserService { public void deleteAll(){ System.out.println("UserService中deleteAll的核心代码"); } }
|
实现AOP
开启AOP注解支持
使用aop:aspectj-autoproxy标签
1 2 3 4 5 6 7 8 9 10 11
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.sinarcsinx"></context:component-scan> <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
|
创建切面类
创建一个类,在类上加上@Component和@Aspect
使用@Pointcut注解来指定要被增强的方法
使用@Before注解来给我们的增强代码所在的方法进行标识,并且指定了增强代码是在被增强方法执行之前执行的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Component @Aspect public class MyAspect { @Pointcut("execution(* com.sinarcsinx.service.*.*(..))") public void pt(){}
@Before("pt()") public void methodbefore(){ System.out.println("方法被调用了"); } }
|
测试
1 2 3 4 5 6
| public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); PhoneService phoneService = applicationContext.getBean(PhoneService.class); UserService userService = applicationContext.getBean(UserService.class); phoneService.deleteAll(); }
|
AOP核心概念
Joinpoint(连接点):所谓连接点是指那些可以被增强到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点
Pointcut(切入点):所谓切入点是指被增强的连接点(方法)
Advice(通知/ 增强):所谓通知是指具体增强的代码
Target(目标对象):被增强的对象就是目标对象
Aspect(切面):是切入点和通知(引介)的结合
Proxy (代理):一个类被 AOP 增强后,就产生一个结果代理类
切点确定
切点表达式
可以使用切点表达式来表示要对哪些方法进行增强
写法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))
- 访问修饰符可以省略,大部分情况下省略
- 返回值类型、包名、类名、方法名可以使用星号
*
代表任意
- 包名与类名之间一个点
.
代表当前包下的类,两个点 ..
表示当前包及其子包下的类
- 参数列表可以使用两个点
..
表示任意个数,任意类型的参数列表
例如:
1 2 3 4 5 6 7
| execution(* com.sinarcsinx.service.*.*(..)) 表示com.sinarcsinx.service包下任意类,方法名任意,参数列表任意,返回值类型任意 execution(* com.sinarcsinx.service..*.*(..)) 表示com.sinarcsinx.service包及其子包下任意类,方法名任意,参数列表任意,返回值类型任意 execution(* com.sinarcsinx.service.*.*()) 表示com.sinarcsinx.service包下任意类,方法名任意,要求方法不能有参数,返回值类型任意 execution(* com.sinarcsinx.service.*.delete*(..)) 表示com.sinarcsinx.service包下任意类,要求方法不能有参数,返回值类型任意,方法名要求已delete开头
|
切点函数@annotation
我们也可以在要增强的方法上加上注解。然后使用@annotation来表示对加了什么注解的方法进行增强
写法:**@annotation(注解的全类名)**
例如:
定义注解如下
1 2 3 4
| @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface InvokeLog { }
|
给需要增强的方法增加注解
1 2 3 4 5 6 7 8
| @Service public class PhoneService {
@InvokeLog public void deleteAll(){ System.out.println("PhoneService中deleteAll的核心代码"); } }
|
切面类中使用@annotation来确定要增强的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Component @Aspect public class MyAspect {
@Pointcut("@annotation(com.sinarcsinx.aspect.InvokeLog)") public void pt(){}
@Before("pt()") public void methodbefore(){ System.out.println("方法被调用了"); } }
|
通知分类
@Before:前置通知,在目标方法执行前执行
@AfterReturning: 返回后通知,在目标方法执行后执行,如果出现异常不会执行
@After:后置通知,在目标方法之后执行,无论是否出现异常都会执行
@AfterThrowing:异常通知,在目标方法抛出异常后执行
@Around:环绕通知,围绕着目标方法执行
注意:下面的伪代码是用来理解单个通知的执行时机的,不能用来理解多个通知情况下的执行顺序。如果需要配置多个通知我们会选择使用Around通知,更加的清晰并且好用
1 2 3 4 5 6 7 8 9 10 11 12 13
| public Object test() { before(); try { Object ret = 目标方法(); afterReturing(); } catch (Throwable throwable) { throwable.printStackTrace(); afterThrowing(); }finally { after(); } return ret; }
|
环绕通知非常特殊,它可以对目标方法进行全方位的增强
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Around("pt()") public void around(ProceedingJoinPoint pjp){ System.out.println("目标方法前"); try { pjp.proceed(); System.out.println("目标方法后"); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("目标方法出现异常"); }finally { System.out.println("finally中进行增强"); } }
|
获取被增强方法相关信息
我们实际对方法进行增强时往往还需要获取到被增强代码的相关信息,比如方法名,参数,返回值,异常对象等
我们可以在除了环绕通知外的所有通知方法中增加一个JoinPoint类型的参数。这个参数封装了被增强方法的相关信息。
我们可以通过这个参数获取到除了异常对象和返回值之外的所有信息。
例如:
1 2 3 4 5 6 7
| @Before("pt()") public void methodbefore(JoinPoint jp){ Object[] args = jp.getArgs(); Object target = jp.getTarget(); MethodSignature signature = (MethodSignature) jp.getSignature(); System.out.println("Before方法被调用了"); }
|
案例:要求让所有service包下类的所有方法被调用前都输出全类名,方法名,以及调用时传入的参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Component @Aspect public class PrintLogAspect {
@Pointcut("execution(* com.sinarcsinx.service..*.*(..))") public void pt(){}
@Before("pt()") public void printLog(JoinPoint joinPoint){ MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String className = signature.getDeclaringTypeName(); String methodName = signature.getName(); Object[] args = joinPoint.getArgs();
System.out.println(className+"=="+methodName+"======"+ Arrays.toString(args)); } }
|
如果需要获取被增强方法中的异常对象或者返回值则需要在方法参数上增加一个对应类型的参数,并且使用注解的属性进行配置。这样Spring会把你想获取的数据赋值给对应的方法参数
例如:
1 2 3 4
| @AfterReturning(value = "pt()",returning = "ret") public void AfterReturning(JoinPoint jp,Object ret){ System.out.println("AfterReturning方法被调用了"); }
|
1 2 3 4
| @AfterThrowing(value = "pt()",throwing = "t") public void AfterThrowing(JoinPoint jp,Throwable t){ System.out.println("AfterReturning方法被调用了"); }
|
如果觉得上面的获取方式特别的麻烦难以理解可以使用下面这种万能的方法
直接在环绕通知方法中增加一个ProceedingJoinPoint类型的参数。这个参数封装了被增强方法的相关信息
该参数的proceed()方法被调用相当于被增强方法被执行,调用后的返回值就相当于被增强方法的返回值
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Around(value = "pt()") public Object around(ProceedingJoinPoint pjp) { Object[] args = pjp.getArgs(); Object target = pjp.getTarget(); MethodSignature signature = (MethodSignature) pjp.getSignature(); Object ret = null; try { ret = pjp.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } return ret; }
|
AOP应用案例
需求
现有AI核心功能代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class AIController { public String getAnswer(String question){ String str = question.replace("吗", ""); str = str.replace("?","!"); return str; }
public String fortuneTelling(String name){ String[] strs = {"女犯伤官把夫克,旱地莲花栽不活,不是吃上两家饭,也要刷上三家锅。","一朵鲜花头上戴,一年四季也不开,一心想要花开时,采花之人没到来。","此命生来脾气暴,上来一阵双脚跳,对你脾气啥都好,经常与人吵和闹。"}; int index = name.hashCode() % 3;
return strs[index]; } }
|
现在为了保证数据的安全性,要求调用方法时fortuneTelling传入的姓名是经过加密的。我们需要对传入的参数进行解密后才能使用,并且要对该方法的返回值进行加密后返回
PS:后期也可能让其他方法进行相应的加密处理。
字符串加密解密直接使用下面的工具类即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.SecureRandom;
public class CryptUtil { private static final String AES = "AES";
private static int keysizeAES = 128;
private static String charset = "utf-8";
public static String parseByte2HexStr(final byte buf[]) { final StringBuffer sb = new StringBuffer(); for (int i = 0; i < buf.length; i++) { String hex = Integer.toHexString(buf[i] & 0xFF); if (hex.length() == 1) { hex = '0' + hex; } sb.append(hex.toUpperCase()); } return sb.toString(); }
public static byte[] parseHexStr2Byte(final String hexStr) { if (hexStr.length() < 1) return null; final byte[] result = new byte[hexStr.length() / 2]; for (int i = 0;i< hexStr.length()/2; i++) { int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16); int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16); result[i] = (byte) (high * 16 + low); } return result; }
private static String keyGeneratorES(final String res, final String algorithm, final String key, final Integer keysize, final Boolean bEncode) { try { final KeyGenerator g = KeyGenerator.getInstance(algorithm); if (keysize == 0) { byte[] keyBytes = charset == null ? key.getBytes() : key.getBytes(charset); g.init(new SecureRandom(keyBytes)); } else if (key == null) { g.init(keysize); } else { byte[] keyBytes = charset == null ? key.getBytes() : key.getBytes(charset); SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.setSeed(keyBytes); g.init(keysize, random); } final SecretKey sk = g.generateKey(); final SecretKeySpec sks = new SecretKeySpec(sk.getEncoded(), algorithm); final Cipher cipher = Cipher.getInstance(algorithm); if (bEncode) { cipher.init(Cipher.ENCRYPT_MODE, sks); final byte[] resBytes = charset == null? res.getBytes() : res.getBytes(charset); return parseByte2HexStr(cipher.doFinal(resBytes)); } else { cipher.init(Cipher.DECRYPT_MODE, sks); return new String(cipher.doFinal(parseHexStr2Byte(res))); } } catch (Exception e) { e.printStackTrace(); } return null; }
public static String AESencode(final String res) { return keyGeneratorES(res, AES, "aA11*-%", keysizeAES, true); }
public static String AESdecode(final String res) { return keyGeneratorES(res, AES, "aA11*-%", keysizeAES, false); }
public static void main(String[] args) { System.out.println( "加密后:" + AESencode("将要加密的明文") ); System.out.println( "解密后:" + AESdecode("730CAE52D85B372FB161B39D0A908B8CC6EF6DA2F7D4E595D35402134C3E18AB") ); } }
|
实现
导入依赖
1 2 3 4 5 6 7 8 9 10 11 12
| <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.26</version> </dependency>
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.7</version> </dependency>
|
开启AOP注解支持
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.sinarcsinx"></context:component-scan> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>
|
自定义注解
1 2 3 4 5 6 7 8 9 10 11 12
| package com.sinarcsinx.aspect;
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Crypt {
}
|
在目标方法上增加注解
注意:目标对象一定要记得注入Spring容器中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Controller public class AIController {
@Crypt public String fortuneTelling(String name){ System.out.println(name); String[] strs = {"女犯伤官把夫克,旱地莲花栽不活,不是吃上两家饭,也要刷上三家锅。","一朵鲜花头上戴,一年四季也不开,一心想要花开时,采花之人没到来。","此命生来脾气暴,上来一阵双脚跳,对你脾气啥都好,经常与人吵和闹。"}; int index = name.hashCode() % 3;
return strs[index]; } }
|
定义切面类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package com.sinarcsinx.aspect;
import com.sinarcsinx.util.CryptUtil; 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.springframework.stereotype.Component;
@Component @Aspect public class CryptAspect {
@Pointcut("@annotation(com.sinarcsinx.aspect.Crypt)") public void pt(){
}
@Around("pt()") public Object crypt(ProceedingJoinPoint pjp) { Object[] args = pjp.getArgs(); String arg = (String) args[0]; String s = CryptUtil.AESdecode(arg); args[0] = s; Object proceed = null; String ret = null; try { proceed = pjp.proceed(args); ret = (String) proceed; ret = CryptUtil.AESencode(ret); } catch (Throwable throwable) { throwable.printStackTrace(); } return ret; } }
|
xml配置AOP
定义切面类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public class MyAspect {
public void before(JoinPoint joinPoint){ System.out.println("before"); }
public void afterReturning(JoinPoint joinPoint,Object ret){ System.out.println("afterReturning:"+ret); }
public void after(JoinPoint joinPoint){ System.out.println("after"); }
public void afterThrowing(JoinPoint joinPoint,Throwable e){ String message = e.getMessage(); System.out.println("afterThrowing:"+message); }
public Object around(ProceedingJoinPoint pjp){ Object[] args = pjp.getArgs(); MethodSignature signature = (MethodSignature) pjp.getSignature(); Object target = pjp.getTarget(); Object ret = null; try { ret = pjp.proceed(); System.out.println(ret); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println(throwable.getMessage()); }
return ret; } }
|
目标类和切面类注入容器
在切面类和目标类上加是对应的注解。注入如果是使用注解的方式注入容器要记得开启组件扫描
当然你也可以在xml中使用bean标签的方式注入容器
1 2 3 4
| @Component public class MyAspect { }
|
1 2 3 4
| @Service public class UserService { }
|
配置AOP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.sinarcsinx"></context:component-scan>
<aop:config> <aop:pointcut id="pt1" expression="execution(* com.sinarcsinx.service..*.*(..))"></aop:pointcut> <aop:pointcut id="pt2" expression="@annotation(com.sinarcsinx.aspect.InvokeLog)"></aop:pointcut> <aop:aspect ref="myAspect"> <aop:before method="before" pointcut-ref="pt1"></aop:before> <aop:after method="after" pointcut-ref="pt1"></aop:after> <aop:after-returning method="afterReturning" pointcut-ref="pt1" returning="ret"></aop:after-returning> <aop:after-throwing method="afterThrowing" pointcut-ref="pt2" throwing="e"></aop:after-throwing> </aop:aspect> </aop:config> </beans>
|
多切面顺序问题
在实际项目中我们可能会存在配置了多个切面的情况。这种情况下我们很可能需要控制切面的顺序
在默认情况下Spring有它自己的排序规则(按照类名排序)
默认排序规则往往不符合我们的要求,我们需要进行特殊控制
如果是注解方式配置的AOP可以在切面类上加**@Order注解来控制顺序。@Order中的属性越小优先级越高**
如果是XML方式配置的AOP,可以通过调整配置顺序来控
例如:
下面这种配置方式就会先使用CryptAspect里面的增强,在使用APrintLogAspect里的增强
1 2 3 4 5 6 7 8 9 10 11 12
| @Component @Aspect @Order(2) public class APrintLogAspect { } @Component @Aspect @Order(1) public class CryptAspect { }
|
AOP原理-动态代理
实际上Spring的AOP其实底层就是使用动态代理来完成的。并且使用了两种动态代理分别是JDK的动态代理和Cglib动态代理
所以我们接下去来学习下这两种动态代理,理解下它们的不同点
JDK动态代理
JDK的动态代理使用的java.lang.reflect.Proxy这个类来进行实现的。要求被代理(被增强)的类需要实现了接口,并且JDK动态代理也只能对接口中的方法进行增强
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public static void main(String[] args) { AIControllerImpl aiController = new AIControllerImpl(); ClassLoader cl = Demo.class.getClassLoader(); Class<?>[] interfaces = AIControllerImpl.class.getInterfaces(); AIController proxy = (AIController) Proxy.newProxyInstance(cl, interfaces, new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if(method.getName().equals("getAnswer")){ System.out.println("增强"); } Object ret = method.invoke(aiController, args); return ret; } }); String answer = proxy.getAnswer("?"); System.out.println(answer); }
|
Cglib动态代理
使用的是org.springframework.cglib.proxy.Enhancer类进行实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class CglibDemo { public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(AIControllerImpl.class); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { if ("getAnswer".equals(method.getName())){ System.out.println("被增强了"); } Object ret = methodProxy.invokeSuper(o, objects); return ret; } }); AIControllerImpl proxy = (AIControllerImpl) enhancer.create();
System.out.println(proxy.fortuneTelling("你好吗?")); } }
|
总结
JDK动态代理要求被代理(被增强)的类必须要实现接口,生成的代理对象相当于是被代理对象的兄弟
Cglib的动态代理不要求被代理(被增强)的类要实现接口,生成的代理对象相当于被代理对象的子类对象
Spring的AOP默认情况下优先使用的是JDK的动态代理,如果使用不了JDK的动态代理才会使用Cglib的动态代理
切换默认动态代理方式
有的时候我们需要修改AOP的代理方式
我们可以使用以下方式修改:
如果我们是采用注解方式配置AOP的话:
设置aop:aspectj-autoproxy标签的proxy-target-class属性为true,代理方式就会修改成Cglib
1
| <aop:aspectj-autoproxy proxy-target-class="true"/>
|
如果我们是采用xml方式配置AOP的话:
设置aop:config标签的proxy-target-class属性为true,代理方式就会修改成Cglib
1 2
| <aop:config proxy-target-class="true"> </aop:config>
|