一、Spring中的可插拔组件技术
二、Spring AOP
Spring AOP - Aspect Oriented Programming面向切面编程
AOP的做法是将通用、与业务无关的功能抽象封装为切面类
切面可配置在目标方法的执行前、后运行,真正做到即插即用
最终目的:在不修改源码的情况下,对程序行为进行扩展
简单的spring AOP
配置文件,依赖AspectJ
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<!--aspectjweaver是Spring AOP的底层依赖-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
</dependencies>
定义Advice,pointcut
<bean id="userDao" class="com.imooc.spring.aop.dao.UserDao"/>
<bean id="employeeDao" class="com.imooc.spring.aop.dao.EmployeeDao"/>
<bean id="userService" class="com.imooc.spring.aop.service.UserService">
<property name="userDao" ref="userDao"/>
</bean>
<bean id="employeeService" class="com.imooc.spring.aop.service.EmployeeService">
<property name="employeeDao" ref="employeeDao"/>
</bean>
<!-- AOP配置-->
<bean id="methodAspect" class="com.imooc.spring.aop.aspect.MethodAspect"></bean>
<aop:config>
<!--PointCut切点,使用execution表达式描述切面的作用范围-->
<!--execution(public * com.imooc..*.*(..))说明切面作用在com.imooc包下的所有类的所有方法上-->
<aop:pointcut id="pointcut" expression="execution(public * com.imooc..*.*(..))"/>
<!--定义切面类-->
<aop:aspect ref="methodAspect">
<!--before通知,代表在目标方法上运行前执行methodAspect.printExecutionTime()-->
<aop:before method="printExecutionTime" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
实现切面类/方法
在aop文件下创建aspect文件创建MethodAspect.java文件
//切面类
public class MethodAspect {
public void printExecutionTime(JoinPoint joinPoint){
// 切面方法,用于扩展额外功能
// JoinPoint连接点,通过连接点可以获取目标类、方法的信息
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String now = sdf.format(new Date());
String className = joinPoint.getTarget().getClass().getName();//获取目标对象的类的名称
String methodName = joinPoint.getSignature().getName();//获取目标方法名称
System.out.println("---->"+ now + ":" + className + "." + methodName);
}
}
这样很好的去捕获时间,随时插入使用,不会影响源代码
三、AOP关键概念
Spring AOP与AspectJ的关系
Eclipse AspectJ,一种基于java平台的面向切面编程的语言
Spring AOP使用AspectJWeaver实现类与方法匹配
Spring AOP利用动态代理的模式实现对对象运行时功能扩展
几个关键概念
AOP配置的过程
依赖AspectJ
实现切面类/方法
配置Aspect Bean
定义PointCut
配置Advice
JoinPoint对象
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String now = sdf.format(new Date());
String className = joinPoint.getTarget().getClass().getName();//获取目标对象的类的名称
String methodName = joinPoint.getSignature().getName();//获取目标方法名称
System.out.println("---->"+ now + ":" + className + "." + methodName);
Object[] arge = joinPoint.getArgs();
System.out.println("---->参数个数:"+arge.length);
for (Object arg:arge) {
System.out.println("---->参数:" + arg);
}
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.createUser();
userService.generateRandomPassword("MD5",16);
运行结果:
---->2023-02-24 16:21:15 676:com.imooc.spring.aop.service.UserService.createUser
---->参数个数:0
执行员工入职业务逻辑
---->2023-02-24 16:21:15 694:com.imooc.spring.aop.dao.UserDao.insert
---->参数个数:0
新增用户数据
---->2023-02-24 16:21:15 700:com.imooc.spring.aop.service.UserService.generateRandomPassword
---->参数个数:2
---->参数:MD5
---->参数:16
按MD5方式生成16位随机密码
PointCut切点表达式
public * com.imooc … service .(…))
- 这里*service指符合xxxservice名的类的范围。
- public可以省去
- *可以替换成void,string等类型
- .create*代表createxxx的方法
- 参数方法的捕获:无参数的捕获(),捕获两个参数的方法(String,)这里的可以替换
通知
五类通知
特殊的“通知”-引介增强
引介增强(IntroductionInterceptor)
是对类的增强,而非方法,其他五类通知是作用在方法上
允许在运行时为目标类增加新属性或方法,在程序运行时,这些类被加载到JVM中,引介增强再对这些类进行动态调整
允许在运行时改变类的行为,让类随运行环境动态变更,在程序运行过程中,假设环境参数是这个样子的引介增强可以让我们目标要执行的类动态改变这个类的运行逻辑去适应现在的环境,如果是另外的运行环境,引介增强还是可以对这个类进行动态调整来适应当前的环境
这些通知执行的顺序是由配置的顺序来执行的
前四种通知的简单了解
<aop:before method="printExecutionTime" pointcut-ref="pointcut"/>
<aop:after method="doAfter" pointcut-ref="pointcut"/>
<aop:after-returning method="doAfterReturn" returning="ret" pointcut-ref="pointcut"/>
<aop:after-throwing method="doAfterThrowing" throwing="th" pointcut-ref="pointcut"/>
public void printExecutionTime(JoinPoint joinPoint){
System.out.println("<--触发前置通知");
}
public void doAfter(JoinPoint joinPoint){
System.out.println("<--触发后置通知");
}
public void doAfterReturn(JoinPoint joinPoint,Object ret){
System.out.println("<--返回后通知:"+ret);
}
public void doAfterThrowing(JoinPoint joinPoint,Throwable th){
System.out.println("<---异常通知:"+th.getMessage());
}
详解环绕通知
利用AOP进行方法性能筛查
aop:config
<aop:pointcut id=“pointcut” expression=“execution(* com.imooc….(…))”/>
<aop:aspect ref=“methodChecker”>
<aop:around method=“check” pointcut-ref=“pointcut”/>
</aop:aspect>
</aop:config>
// ProceedingJoinPoint是JoinPoint的升级版,在原有功能外,还可以控制目标方法是否执行
public Object check(ProceedingJoinPoint pjp) throws Throwable {
try {
long starTime = new Date().getTime();
Object ret = pjp.proceed();//执行目标方法
long endTime = new Date().getTime();
long duration = endTime - starTime;
if (duration >= 1000){
String className = pjp.getTarget().getClass().getName();
String methodName = pjp.getSignature().getName();
SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss SSS”);
String now = sdf.format(new Date());
System.out.println(“=======” + now + “:” + className + “.” + methodName + “(” + duration + “)”);
}
return ret;
} catch (Throwable throwable) {
System.out.println(“Exception message” + throwable.getMessage());
throw throwable;
}
}
环绕通知从整体来看,包含了四种全部通知
环绕通知和其他通知不同的是:
切面类中方法声明上,return object将目标方法的返回值进行返回,重点是ProceedingJoinpoint的使用
catch中将throwable抛出异常,是因为在当前系统中,在未来运行时中可能不止一个通知,如果在当前环绕通知中对这一个异常进行消化,后续其他后续处理都不会捕捉到这个异常,会产生意料之外的问题
基于注解开发Spring AOP
<?xml version="1.0" encoding="UTF-8"?>
<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"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--初始化IOC容器-->
<context:component-scan base-package="com.imooc"/>
<!--启用Spring AOP注解模式-->
<aop:aspectj-autoproxy/>
</beans>
@Repository
public class EmployeeDao {
public void insert(){
System.out.println("新增员工数据");
}
}
@Service
public class UserService {
private UserDao userDao;
@Resource
public void createUser(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行员工入职业务逻辑");
userDao.insert();
}
public String generateRandomPassword(String type , Integer length){
System.out.println("按" + type + "方式生成"+ length + "位随机密码");
return "Zxcquei1";
}
public UserDao getUserDao() {
return userDao;
}
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
@Component//标记当前类为组件
@Aspect//说明当前类是切面类
public class MethodChecker {
//环绕通知,参数为PointCut
@Around("execution(* com.imooc..*Service.*(..))")
//ProceedingJoinPoint是JoinPoint的升级版,在原有功能外,还可以控制目标方法是否执行
public Object check(ProceedingJoinPoint pjp) throws Throwable {
try {
long startTime = new Date().getTime();
Object ret = pjp.proceed();//执行目标方法
long endTime = new Date().getTime();
long duration = endTime - startTime; //执行时长
if(duration >= 1000){
String className = pjp.getTarget().getClass().getName();
String methodName = pjp.getSignature().getName();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String now = sdf.format(new Date());
System.out.println("=======" + now + ":" + className + "." + methodName + "(" + duration + "ms)======");
}
return ret;
} catch (Throwable throwable) {
System.out.println("Exception message:" + throwable.getMessage());
throw throwable;
}
}
}
Spring AOP实现原理
Spring基于代理模式实现功能动态扩展,包含两种形式:
目标类拥有接口,通过JDK动态代理实现功能扩展
目标类没有接口,通过CGLib组件实现功能扩展
代理模式
代理模式通过代理对象对原对象的实现功能扩展
静态代理
接口
public interface UserService {
public void crateUser();
}
委托类
public class UserServicelmpl implements UserService{
@Override
public void crateUser() {
System.out.println("执行创建用户业务逻辑");
}
}
代理类
public class UserServiceProxy1 implements UserService{
private UserService userService;
public UserServiceProxy1(UserService userService){
this.userService = userService;
}
@Override
public void crateUser() {
userService.crateUser();
System.out.println("======后置扩展功能=======");
}
}
//静态代理是指必须手动创建代理类的代理模式使用方式
public class UserServiceProxy implements UserService{
//持有委托类的对象
private UserService userService;
public UserServiceProxy(UserService userService){
this.userService = userService;
}
@Override
public void crateUser() {
System.out.println("====="+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date()) +"=========");
userService.crateUser();
}
}
客户类
public class Application {
public static void main(String[] args) {
UserService userService =new UserServiceProxy1( new UserServiceProxy(new UserServicelmpl()));
userService.crateUser();
}
}
运行结果
=====2023-02-27 15:47:55 769=========
执行创建用户业务逻辑
======后置扩展功能=======
无论代理类还是委托类都要实现相同的接口,在代理类中要持有委托类的对象
AOP底层原理-JDK动态代理
新问题:每一个委托类都要创建一个代理类,如果系统中有大量类,就需要手动创建大量代理类
解决:在JDK1.2中引入了反射机制,根据要实现的接口,按照接口的结构去自动生成相应的代理类,完成目标方法的拓展工作,因为代理类是通过反射自动在运行时生成的,所以叫动态代理
静态代理与动态代理的区别
静态代理需要手写代理,而动态代理是通过接口结构自动在运行时生成的
接口
public interface UserService {
public void crateUser();
}
public interface EmployeeService {
public void createEmployee();
}
委托类
public class UserServicelmpl implements UserService{
@Override
public void crateUser() {
System.out.println("执行创建用户业务逻辑");
}
}
public class EmployeeServicelmpl implements EmployeeService{
@Override
public void createEmployee() {
System.out.println("执行创建员工业务逻辑");
}
}
利用反射动态创建代理类
/**
* InvocationHandler是JDK提供的反射类,用于在JDK动态代理中对目标方法进行增强
* InvocationHandler实现类与切面类的环绕通知类似
*/
public class ProxyInvocationHandler implements InvocationHandler {
private Object target;//目标对象
private ProxyInvocationHandler(Object target){
this.target = target;
}
/**
* 在invoke()方法对目标方法进行增强
* @param proxy 代理类对象
* @param method 目标方法对象
* @param args 目标方法实参
* @return 目标方法运行后返回值
* @throws Throwable 目标方法抛出的异常
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("===="+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date()) +"======");
Object ret = method.invoke(target, args);//调用目标方法,ProceedingJoinPoint.proceed()
return ret;
}
public static void main(String[] args) {
UserService userService = new UserServicelmpl();
ProxyInvocationHandler invocationHandler = new ProxyInvocationHandler(userService);
/*
动态创建代理类
Proxy是反射包提供的方法newProxyInstance
userService.getClass().getClassLoader()传入目标接口的类加载器
userService.getClass().getInterfaces()传入的是类要实现的接口
invocationHandler对目标方法的扩展
*/
UserService userServiceProxy = (UserService)Proxy.newProxyInstance(userService.getClass().getClassLoader(),
userService.getClass().getInterfaces(),
invocationHandler);
userServiceProxy.crateUser();
//动态代理,必须实现接口才可以运行
EmployeeService employeeService = new EmployeeServicelmpl();
EmployeeService employeeServiceProxy = (EmployeeService)Proxy.newProxyInstance(employeeService.getClass().getClassLoader(),
employeeService.getClass().getInterfaces(),
new ProxyInvocationHandler(employeeService));
employeeServiceProxy.createEmployee();
}
}
JDK动态代理解析
UserService userServiceProxy = Proxy.newProxyInstance():这句话执行分三个步骤:
第一步:newProxyInstance会在本地硬盘中创建.Class的字节码文件,然后确定包名
第二步:确定类名,在没有重复的情况下,类名是$Proxy0的默认形式
第三步:生成代理类
Class字节码文件如何被JVM识别
newProxyInstance()方法中执行defineClass0,而这个defineClass0是指将class字节码文件通过被代理类的ClassLoder(类加载器)去载入到JVM的方法区中,保存的是我们字节码解析以后的这些与类描述的定义信息,类加载完以后,通过newProxyInstance()方法来完成实例化的工作=new $Proxy,而这个对象放在JVM内存中的堆当中,因为代理对象需要持有被代理类,所有在内存中代理对象会持有被代理(UserServiceImpl)的引用,当这个对象创建并保存到到这个堆内存当中时,newProxyInstance()这个方法就完成了。
AOP底层原理-CGLib实现代理类
CDLib是运行时字节码增强技术
Spring AOP扩展无接口类使用CGLib,属于第三方组件
AOP会运行时生成目标继承类字节码的方式进行行为扩展
动态代理,必须实现接口才可以运行,而CGLib是无需接口的
Spring AOP的实现原理
分两种情况,第一种情况:如果我们的目标类实现了接口,则spring优先底层使用JDK动态代理来生成目标类的代理,从而实现功能的扩展,
第二种情况:如果目标类没有实现接口,自动使用CGLib来通过继承的方式对目标类进行扩展