持续学习&持续更新中…
守破离
【Java从零到架构师第③季】【17】Spring-AOP
一、BeanPostProcessor+JDK(CGLib)的不足
-
使用JDK/CGLib生成的代理对象,在默认情况下,会代理所有通过getBean这种方式获取对象的bean(也就是由Ioc容器管理的bean),并且会代理目标对象(也就是这些bean)的所有方法(肯定也会包括Object的toString、hashCode、equals…),无法做到精确控制代理哪些目标对象的哪些方法。而现实开发中,肯定是某些类的某些方法才需要代理增加附加代码。
-
当然了,可以通过手动书写控制代码来实现精确控制代理哪些bean(目标对象)的哪些方法,不过,会很麻烦,很不好维护,具体原因如下。
1、书写麻烦
-
需要考虑使用JDK(
implements
:需要代理的目标对象必须实现了接口)还是使用CGLib(extends
:目标对象无父接口时)来实现动态代理注:即使目标对象有父接口也可以使用CGLib来实现动态代理。
-
不但需要自己实现
BeanPostProcessor
,而且还需要手动在applicationContext.xml中注册
该BeanBeanPostProcessor。注册代码:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 注册BeanPostProcessor --> <!-- <bean class="programmer.lp.processor.LogProcessor"/> --> <bean class="programmer.lp.processor.LogProcessor2"/> <bean id="userService" class="programmer.lp.service.impl.UserServiceImpl"/> <bean id="skillService" class="programmer.lp.service.SkillService"/> </beans>
2、代码复杂
-
在
postProcessAfterInitialization
方法中需要考虑清楚哪些bean需要代理、哪些bean不需要代理,书写相应的控制代码。@Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { // 需要考虑清楚哪些bean需要代理、哪些bean不需要代理 if (!bean.getClass().getName().startsWith("programmer.lp.service.")) { return bean; } Enhancer enhancer = new Enhancer(); enhancer.setClassLoader(getClass().getClassLoader()); enhancer.setSuperclass(bean.getClass()); enhancer.setCallback(new LogMethodInterceptor(bean)); return enhancer.create(); }
-
在
MethodInterceptor
的intercept
(CGLib)或者InvocationHandler
的invoke
(JDK)方法中,需要考虑清楚哪些方法需要代理、哪些方法不需要代理,书写相应的控制代码。private static class LogMethodInterceptor implements MethodInterceptor { Object target; public LogMethodInterceptor(Object target) { this.target = target; } // 以CGLib-MethodInterceptor-intercept方法为例 @Override public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { // 假设现在只想代理 boolean login(String username, String password); 这种方法 String methodName = method.getName(); Class<?> methodReturnType = method.getReturnType(); Class<?>[] methodParameterTypes = method.getParameterTypes(); if ("login".equals(methodName) && methodParameterTypes.length == 2 && methodParameterTypes[0] == String.class && methodParameterTypes[1] == String.class && methodReturnType == Boolean.class) { System.out.println("代理-----------start-----------日志/事务..."); Object result = method.invoke(target, args); System.out.println("代理-----------end-----------日志/事务..."); return result; } return method.invoke(target, args); } }
3、问题解决
- 使用Spring封装好的AOP技术
二、AOP—Aspect Oriented Programming
依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.6</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
三、AOP—动态代理的底层实现
四、AOP—MethodBeforeAdvice
MethodBeforeAdvice:
public class LogAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("MethodBeforeAdvice-LogAdvice-before-------------" + method.getName());
}
}
applicationContext.xml:
<?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"
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">
<!-- 附加代码 -->
<bean id="logAdvice" class="programmer.lp.advice.LogAdvice"/>
<!-- 切面 -->
<aop:config>
<!-- 切入点:给哪些类的哪些方法增加附加代码? -->
<!-- execution(* *(..)) 代表所有bean的所有方法都会被切入 -->
<aop:pointcut id="pc" expression="execution(* *(..))"/>
<!-- 为切入点切入附加代码 -->
<aop:advisor advice-ref="logAdvice" pointcut-ref="pc"/>
</aop:config>
<bean id="userService" class="programmer.lp.service.impl.UserServiceImpl"/>
<bean id="skillService" class="programmer.lp.service.SkillService"/>
</beans>
使用:
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = ctx.getBean("userService", UserService.class);
SkillService skillService = ctx.getBean("skillService", SkillService.class);
userService.login("xxxx", "yyzz");
skillService.delete(10);
五、AOP—MethodInterceptor
MethodInterceptor:
public class LogInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("代理 start ------------------------------------");
Object result = methodInvocation.proceed();
System.out.println("代理 end ------------------------------------");
return result;
}
}
applicationContext.xml:
<!-- 附加代码 -->
<bean id="logInterceptor" class="programmer.lp.advice.LogInterceptor"/>
<!-- 切面 -->
<aop:config>
<!-- 切入点:给哪些类的哪些方法增加附加代码? -->
<!-- execution(* *(..)) 代表所有类的所有方法都会被切入 -->
<aop:pointcut id="pc" expression="execution(* *(..))"/>
<!-- 为切入点切入附加代码 -->
<aop:advisor advice-ref="logInterceptor" pointcut-ref="pc"/>
</aop:config>
六、AOP—切入点表达式
切入点表达式公式:
expression="指示符(描述内容)"
一个完整的切入点表达式:
execution(public boolean programmer.lp.service.UserService.login(String, String))
-
指示符:
execution
、args
、within
、@annotation
-
描述内容:
* *(..)
- 前面的
*
:权限修饰符 返回值
- 后面的
*
:包名.类名.方法名
..
:方法形参类型(多个使用,隔开)
一个完整的描述内容: public boolean programmer.lp.service.UserService.login(String, String)
- 前面的
七、AOP—切入点表达式的使用
使用:
execution(public boolean programmer.lp.service.UserService.login(String, String))
:完整
execution(public boolean programmer.lp.service.UserService.login(..))
:不限制参数execution(public boolean programmer.lp.service.UserService.login())
:限制参数—无参数
-
execution(public * programmer.lp.service.UserService.login(String, String))
:不限制返回值 -
execution(public * *(String, String))
:所有public的形参为(String, String)
的方法 -
execution(public * *(..))
:所有public的的方法
execution(* programmer.lp.service.UserService.login(String, String))
:不限制权限修饰符与返回值(不支持单独不限制权限修饰符)execution(* login(String, String))
:所有login(String, String)
的方法execution(* *(String, String))
:所有形参为(String, String)
的方法
execution(public boolean *.UserService.login(String, String))
:不限制类所在的包
execution(public boolean programmer.lp.service.*.login(String, String))
:programmer.lp.service
包下的所有类的public boolean login(String, String)
方法execution(* programmer.lp.service..*.login(String, String))
:programmer.lp.service
包(包括子包)下的所有类的login(String, String)
方法
execution(* *.login(String, String))
:所有类的login(String, String)
方法execution(* *.login(..))
:所有类的名为login
的方法execution(* login(..))
:所有类的名为login
的方法
execution(* log*(..))
:所有方法名以log开头的方法execution(* *log(..))
:所有方法名以log结尾的方法
args(String, String)
:所有形参为(String, String)
的方法- 相当于,
execution(* *(String, String))
-
execution(* *(..))
:所有类的所有方法 -
args(..)
:所有类的所有方法
注意:
-
java.lang包下的类只需写类的simpleName:
- java.lang.Integer -> Integer(int)
- java.lang.String -> String
- …
-
推荐使用
execution
指示符
八、AOP—切入点指示符为@annotation
-
首先,自定义一个注解:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Log {}
-
然后,给需要代理的方法加上自定义的注解
public class SkillService { @Log public void delete(Integer id) { System.out.println("SkillService-核心业务-delete"); } }
public class UserServiceImpl implements UserService { @Override @Log public boolean login(String username, String password) { // ... // dao等操作 // ... System.out.println("UserService的核心业务-login"); return "lpruoyu".equals(username) && "123456".equals(password); } // ... }
-
applicationContext.xml
<bean id="logInterceptor" class="programmer.lp.advice.LogInterceptor"/> <aop:config> <aop:pointcut id="pc" expression="@annotation(programmer.lp.annotation.Log)"/> <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc"/> </aop:config> <bean id="userService" class="programmer.lp.service.impl.UserServiceImpl"/> <bean id="skillService" class="programmer.lp.service.SkillService"/>
九、AOP—组合切入点表达式
-
可以使用
&&(and)
、||(or)
、!
来组合切入点表达式: -
使用
&&
时需要使用字符实体:&&
-
例1:
<aop:config> <aop:pointcut id="pc" expression="within(programmer.lp.service.impl.UserServiceImpl) and args(String, String)"/> <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc"/> </aop:config>
-
例2:
<bean id="logInterceptor" class="programmer.lp.advice.LogInterceptor"/> <aop:config> <aop:pointcut id="pc" expression= "execution(public boolean programmer.lp.service.UserService.login(String, String)) || execution(* programmer.lp.service.SkillService.delete(..))"/> <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc"/> </aop:config>
-
例3:
<aop:config> <aop:pointcut id="pc" expression="!execution(* logout(..))"/> <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc"/> </aop:config>
十、AOP—细节
问题展现:
目标类:
public class UserServiceImpl implements UserService {
@Override
@Log // 目标方法:需要代理
public boolean login(String username, String password) {
System.out.println("--------------------login--------------------");
// 此处调用了另外一个目标方法
if (register(username, password)) {
return "lpruoyu".equals(username) && "123456".equals(password);
}
return false;
}
@Override
@Log // 目标方法:需要代理
public boolean register(String username, String password) {
System.out.println("--------------------register--------------------");
return username.equals(password);
}
}
applicationContext.xml:
<bean id="logInterceptor" class="programmer.lp.advice.LogInterceptor"/>
<aop:config>
<aop:pointcut id="pc" expression="@annotation(programmer.lp.annotation.Log)"/>
<aop:advisor advice-ref="logInterceptor" pointcut-ref="pc"/>
</aop:config>
<bean id="userService" class="programmer.lp.service.impl.UserServiceImpl"/>
调用:
public class MainTest {
UserService userService;
{
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
userService = ctx.getBean("userService", UserService.class);
}
@Test
public void userService() {
userService.login("xxxx", "yyzz");
}
}
问题解决:
public class UserServiceImpl implements UserService, ApplicationContextAware , BeanNameAware {
private ApplicationContext applicationContext;
private String beanName;
@Override
@Log // 目标方法:需要代理
public boolean login(String username, String password) {
System.out.println("--------------------login--------------------");
// 通过同一个id、在同一个容器下获取到的scope为singleton的对象永远是同一个
// 可以理解为一个ApplicationContext就是一个容器
// 把代理对象拿出来,让代理对象去调用另外一个目标方法
UserService userService = applicationContext.getBean(beanName, UserService.class);
if (userService.register(username, password)) {
return "lpruoyu".equals(username) && "123456".equals(password);
}
return false;
}
@Override
@Log // 目标方法:需要代理
public boolean register(String username, String password) {
System.out.println("--------------------register--------------------");
return username.equals(password);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void setBeanName(String name) {
beanName = name;
}
}
十一、AOP—配置多个pointcut、advisor
<bean id="logAdvice" class="programmer.lp.advice.LogAdvice"/>
<bean id="logInterceptor" class="programmer.lp.advice.LogInterceptor"/>
<aop:config>
<aop:pointcut id="pc1"
expression="execution(public boolean programmer.lp.service.UserService.login(String, String))"/>
<aop:pointcut id="pc2"
expression="execution(* programmer.lp.service.SkillService.delete(..))"/>
<!-- 为pc1这个切入点切入两段附加代码 -->
<aop:advisor advice-ref="logAdvice" pointcut-ref="pc1"/>
<aop:advisor advice-ref="logInterceptor" pointcut-ref="pc1"/>
<aop:advisor advice-ref="logInterceptor" pointcut-ref="pc2"/>
</aop:config>
<bean id="userService" class="programmer.lp.service.impl.UserServiceImpl"/>
<bean id="skillService" class="programmer.lp.service.SkillService"/>
<aop:config>
<aop:pointcut id="pc1"
expression="execution(public boolean programmer.lp.service.UserService.login(String, String))"/>
<aop:advisor advice-ref="logAdvice" pointcut-ref="pc1"/>
<aop:advisor advice-ref="logInterceptor" pointcut-ref="pc1"/>
</aop:config>
<aop:config>
<aop:pointcut id="pc2"
expression="execution(* programmer.lp.service.SkillService.delete(..))"/>
<aop:advisor advice-ref="logInterceptor" pointcut-ref="pc2"/>
</aop:config>
注意和一些细节
- 热爱编程、热爱代码
- 编程是一门艺术,如果爱,请深爱!
- 多敲代码
- 多尝试、敢于尝试
- 善于发现、解决问题
- 多多总结
- 多多思考
-
使用AOP技术生成的代理对象,只能使用接口去获取,不能使用实现类去获取
正确写法: UserService userService = applicationContext.getBean(beanName, UserService.class);
错误写法: UserService userService = applicationContext.getBean(beanName, UserServiceImpl.class);
-
自己使用JDK/CGLib生成的代理对象则无所谓,上面两种写法都可以。
- 一般来说,JDK自带的API(接口、功能)都比第三方库的简洁、高效,因此能用JDK自带的API实现的功能,就尽量使用JDK自带的API来实现。
参考
小码哥-李明杰: Java从0到架构师③进阶互联网架构师.
本文完,感谢您的关注支持!