四.基于注解配置Spring AOP
我还是用之前的项目来演示,把applicationContext的bean和AOP都去掉。然后通过注解来配置,然后再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" 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/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"> <!--开启组件注解扫描--> <context:component-scan base-package="com.haiexijun"/> <!--启用Spring IoC的注解模式--> <aop:aspectj-autoproxy/> </beans>
为dao和service增加注解:
UserDao.java
package com.haiexijun.dao; import org.springframework.stereotype.Repository; /** *用户表Dao */ @Repository public class UserDao { public void insert(){ System.out.println("新增用户数据"); } }
EmployeeDao.java
package com.haiexijun.dao; import org.springframework.stereotype.Repository; /** * 员工表Dao */ @Repository public class EmployeeDao { public void insert(){ System.out.println("新增员工数据"); } }
EmployeeService.java
package com.haiexijun.service; import com.haiexijun.dao.EmployeeDao; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * 员工服务 */ @Service public class EmployeeService { @Resource private EmployeeDao employeeDao; public void entry(){ System.out.println("执行员工入职业务逻辑"); employeeDao.insert(); } public EmployeeDao getEmployeeDao() { return employeeDao; } public void setEmployeeDao(EmployeeDao employeeDao) { this.employeeDao = employeeDao; } }
UserService.java
package com.haiexijun.service; import com.haiexijun.dao.UserDao; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * 用户服务 */ @Service public class UserService { @Resource private UserDao userDao; 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 "abcdeffdasf"; } public UserDao getUserDao() { return userDao; } public void setUserDao(UserDao userDao) { this.userDao = userDao; } }
上面都是IoC的配置,下面来进行AOP的配置
打开切面类,进行如下的配置:
package com.haiexijun.aspect; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import java.text.SimpleDateFormat; import java.util.Date; /** * 切面类 */ @Component//标记当前类为一个IoC的组件 @Aspect //说明当前类是一个切面类 public class MethodAspect { /** * 环绕通知 * @param pjp ProceedingJoinPoint是一个特殊的连接点。是JoinPoint的升级版,在原有 * 功能外,还可以控制目标方法是否执行。 */ //环绕通知,参数为PointCut切点表达式 @Around("execution(public * com.haiexijun..*.*(..))") public Object printExecutionTime(ProceedingJoinPoint pjp) throws Throwable { try { //得到起始时间 Long startTime=new Date().getTime(); Object obj= pjp.proceed();//执行目标方法 //得到结束时间 Long endTime=new Date().getTime(); //执行时间 long runTime=endTime-startTime; //如果目标方法的运行时间超过了1秒,就输出日志 if (runTime>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+" ( "+runTime+" ms) ====== "); } return obj; } catch (Throwable e) { //环绕通知,它通常只会去捕获对应异常的产生 //但如果目标方法产生了异常,作为产生的异常 //大多数情况下,它会向外抛出去。 //把异常抛出去是因为在我们当前系统中,未来运行时可能 //并不只有一个通知,那如果在当前的环绕通知中,对这个异常进行了消化 //那就意味着其他后序的处理都不会捕捉到这个异常,就可能会产生一些意料之外的问题 System.out.println("Exception message:"+e.getMessage()); throw e; } } }
当然,还有其他的通知的注解,比如@After、@Before、@AfterReturning等。这里不做演示了。
最后,我们打开程序入口类,运行一下,没有问题:
五.AOP中的代理模式的应用
1.Spring AOP的实现原理
在未来,我们去找工作的时候,有一个问题,面试官会经常提起。那就是请你给我讲一下Spring AOP底层的实现原理是什么?
这个问题实在是太常见了,那作为Spring AOP底层的实现原理是什么呢?如下:
Spring是基于代理模式实现功能动态拓展,包含以下两种形式:
第一种是如果目标类实现了接口,则会通过JDK动态代理实现功能拓展。第二种是如果目标类没有实现接口,则Spring AOP底层通过CGLib这个第三方的组件实现功能的扩展。
此时就涉及到一个核心的问题,什么是代理模式?
2.静态代理
代理模式是指通过代理对象对原对象实现功能拓展。
那代理对象又是什么呢?其实在我们日常生活中随处可见,比如你去到一个全新的城市,希望租一套房子。这时你会怎么做,难道是我到处从电线杆上看这些求租的信息吗?肯定不会,大多数人的第一选择是找到中介公司,通过中介系统的数据库,去查询附近有哪些符合我要求的房子,看价钱是否合适。如果觉得还OK,那就由中介带着我去实地考察,看一下我是否满意,如果满意,那就成交,不满意的话,就继续找。而反过来,房东也依赖于中介。因为房东往往也有自己的工作,不可能天天拿着钥匙给租房的人开门去吧?这时房东就可以委托中介,房东把钥匙给中介,让中介的人带租房的人去看房子。可以看到中介的办事人员就是一个典型的代理人。这个案例放到我们程序中就称之为代理模式。
所谓代理模式,其核心的理念是我们要去创建一个代理类,在代理类中,持有最原始的委托类。作为代理类和委托类,他们要共同实现相同的接口。而客户则是则是通过代理类,完成客户所需要的功能。按照刚才的例子,那个客户类呢,就是租房的人。代理类就是中介的办事人员,而委托类是房东。作为中介和房东,他们的目的是一致的。都是要把房子给租出去。正是因为有着相同的目的,所以他们就实现了共同的接口,这个接口中提供了一个租房的方法,代理类和委托类都去实现租房的这个逻辑。作为代理类,它内部持有了委托类的对象,所以在代理类被实例化以后,也就是代理对象执行的过程中,可以对原始的逻辑产生额外的行为。比如说这个中介代理类,在为客户看完房子以后,除了交付给原始的房东原始的租金以外,它还要向客户收取代理费,这就是额外的扩展逻辑了。放在程序中也是一样的。
那作为代理模式,我们如何去实现呢?下面通过代码来演示一下。
下面来创建一个新的Maven工程:
增加一个service包,在service包中增加一个接口。刚才强调过,无论是代理类和委托类他们都要实现相同的接口,这个接口名为UserService。我们模拟一下现实的环境,在这个用户服务接口中,提供一个createUser()方法,所有实现类都要去实现这个方法。那与此同时,在service包下再创建一个新的实现类,名为UserServiceImpl,它实现UserService接口和里面的方法。我们示意性书写一下。
UserService.java
package com.haiexijun.service; public interface UserService { public void createUser(); }
UserServiceImpl.java
package com.haiexijun.service; public class UserServiceImpl implements UserService { @Override public void createUser() { System.out.println("执行创建用户的业务逻辑"); } }
作为这段代码,我们调用其实非常简单,新增加一个Application类,在类中增加Main方法,然后编写代码:
package com.haiexijun.service; public class Application { public static void main(String[] args) { UserService userService=new UserServiceImpl(); userService.createUser(); } }
然后运行,当然没问题。
但是我提出新的要求,希望将这个方法执行的时间打印出来,该这么办呢?这个需求我们之前在学习Spring AOP的时候已经遇到过了。但是是通过开发切面类来完成的。但是,如果放在我们代理模式中如何做呢?如果需要实现这个功能的扩展,就必须基于UserService接口创建对应的代理类。同时在代理类中去持有与之对应的具体实现。
下面来看一下具体做法:
在service包下面创建一个全新的类,名为UserServiceProxy 。代理的英文单词就是proxy。作为当前的代理类,其核心特点就是持有委托类的对象。定义一个私有的UserService类型的属性。接下来,关键的地方来了,这里定义一个带参的构造方法,参数为UserService。这个参数是在我们代理类实例化的时候,从外侧传入进来的,同时对内部的UserService来进行赋值。这样是不是就相当于在我们创建代理对象的时候,通过外侧传入的某个UserService的实现类,为内部的这个类的UserService赋值,相当于持有委托类的对象了。于此同时,不要忘记作为代理类和委托类都要实现相同的接口,也就是UserService,然后实现createUser()方法。在当前的代理类方法中,因为之前已经持有了委托类的对象,我们可以在createUser方法中发起委托类具体的职责,比如所createUser。同时,在这个方法执行前,我们还可以扩展其他的代码,比如对当前时间运行时的时间。这是不是就是功能的拓展啊?
UserServiceProxy.java
package com.haiexijun.service; import java.text.SimpleDateFormat; import java.util.Date; public class UserServiceProxy implements UserService { //持有委托类的对象 private UserService userService; public UserServiceProxy(UserService userService){ this.userService=userService; } @Override public void createUser() { System.out.println("======"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date())); userService.createUser(); } }
与此同时,作为我们的客户,也就是使用者来说,不在直接面向UserServiceImpl,取而代之的是UserServiceProxy ,去面向这个代理类来调用。那么在代理类调用时,需要传入一个具体的UserServiceImpl。
application.java
package com.haiexijun.service; public class Application { public static void main(String[] args) { UserService userService=new UserServiceProxy(new UserServiceImpl()); userService.createUser(); } }
运行结果,这就是通过代理类实现功能的额外扩展
代理模式可以嵌套使用的。
再打个比方,你手不是见过这样的情况,某一个租户,他把房子整栋租下来,然后再把房间租给很多其他的租客,这种形式在北京上海深圳这种房价高的地方,是很常见的。我们也称这种租客叫二房东。
那在代理模式中也是支持的。因为委托类和代理类都实现了相同的接口。同时,在创建对象的时候,又允许传入对应接口的实现类。因此,我们可以再创建一个全新的代理UserServiceProxy1,具体的做法和之前是一样的,实现UserService接口和方法。然后我们可以为create内扩展一些其他的业务代码,在UserService的createUser方法后拓展执行一个输出语句。这时,系统中就出现了两个代理类,一个是在createUser方法前执行,一个是在createUser方法后执行拓展
package com.haiexijun.service; public class UserServiceProxy1 implements UserService{ private UserService userService; public UserServiceProxy1(UserService userService){ this.userService=userService; } @Override public void createUser() { userService.createUser(); System.out.println("======后置扩展功能====="); } }
那租客肯定是要面向二房东来执行的,下面就是new UserServiceProxy1了,里面传一房东UserServiceProxy。感觉有一点套娃。
package com.haiexijun.service; public class Application { public static void main(String[] args) { UserService userService=new UserServiceProxy1(new UserServiceProxy(new UserServiceImpl())); userService.createUser(); } }
运行后,发现前置和后置的额外拓展功能都打印出来了。
这就是代理模式的精妙之处。可以实现对功能的无限层次的拓展。但在这里,我们每进行一次功能的拓展,都要自己来创建一个代理类啊,这样有一个缺点,随着我们的功能的不断地扩大,每一个具体的实现类,他都要至少拥有一个代理类。而这个代理类是要按照这个规则来自己进行书写的。这样呢,假如我们系统中,有成百上千个具体的业务实现类,那就意味着,也有成百上千个具体的代理类来为具体实现类实现扩展职责。这会让我们系统变得无比的臃肿。对于这种必须要手动创建代理类的使用方式,我们称之为静态代理。 静态代理是最简单的一种代理模式的使用方式,但是也是最麻烦的一种使用方式。
那说起手动创建,就有与之对应的自动创建。在JDK1.2以后,由于反射机制的引入,为我们自动创建代理类提供了可能。那下面就来学习与静态代理对应的动态代理。
3.AOP底层原理—JDK动态代理
下面新创建一个Maven工程,因为JDK动态代理的功能不用到其他第三方组件,所以我们不用在Maven中添加任何依赖。然后,把上一个案例的UserService接口和UserServiceImpl实现类拿过来。
要想实现基于JDK动态代理来实现UserServiceImpl的功能扩展,首先要在service包下再额外的创建一个类ProxyInvocationHandler ,这个类要实现一个至关重要的接口InvocationHandler 。我们要实现它的invoke方法。这个invoke是不是在那里遇见过呢?没错,是在之前学习反射中遇到过,通过invoke来调用目标方法。这里的invoke也是一样的道理。
ProxyInvocationHandler实现InvocationHandler,它的职能非常明确。InvocationHandler是JDK提供的反射类,用于在JDK动态代理中对目标方法进行增强。 InvocationHandler实现类与Spring AOP的切面类的环绕通知类似。我们在invoke方法里面对目标方法进行增强。其中,invoke方法包含了3个参数。第一个参数Object代表了代理类对象,作为这个代理类对象,通常是由我们JDK动态代理自动生成的。第二个参数Method是目标方法对象,说明了目标方法的信息,包括方法名等。而第三个参数是一个Object数组,表示目标方法的实参。该方法返回一个Object,代表目标方法运行后的返回值。而最后抛出Throwable表示抛出目标方法异常。
package haiexijun.service; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.Date; public class ProxyInvocationHandler implements InvocationHandler { private Object target;//目标对象 public 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);//调用目标方法 return ret; } }
然后在Application类中编写代码使用:
package haiexijun.service; import java.lang.reflect.Proxy; public class Application { public static void main(String[] args) { //UserService为目标对象 UserService userService=new UserServiceImpl(); ProxyInvocationHandler invocationHandler= new ProxyInvocationHandler(userService); //invoke方法要从传入代理类Proxy //创建动态代理类, // 通过newProxyInstance方法来创建,传入类加载器,类要实现的接口,还有对目标方法进行扩展的InvocationHandler UserService userServiceProxy = (UserService)Proxy.newProxyInstance(userService.getClass().getClassLoader(),userService.getClass().getInterfaces(),invocationHandler); userServiceProxy.createUser(); } }
但是,动态代理必须要实现一个实现类的接口才能够运行,如果没有实现接口,反射过程必然会报错。可是在我们实际情况下,有着大量的类都没有实现接口,该怎么做呢?这时候,Spring又为我们提供了另外一种解决方案。依赖于spring的第三方组件CGLib实现对类的增强。
4.CGLib实现代理类
CGLib是运行时字节码增强技术。全名为Code Generation Library
当我们某一个类它没有实现接口的时候, Spring AOP 会在运行时生成目标继承类字节码的方式进行扩展。
下面来具体逻辑一下所谓生成目标继承类字节码:
上面有一个Service类,里面有一个findById的按id号查询的方法。里面写入具体的业务代码。可以看到里面的Service是没有实现任何接口的,那显然Jdk动态代理无法对其进行扩展。Spring看到这个类没有实现接口,则自动会使用CGLib通过继承的方式来对类进行扩展。这个继承类是在JVM运行过程中自动生成的,他的生成规则是,前面是类的原始名字,后面增加两个$$符号,然后加EnhancerByCGLIB(spring5以后是EnhancerBySpringCGLIB)。然后继承自Service父类。可以对findById方法进行重写,方法里面通过super指向父类的业务代码,并可以添加拓展的前置代码和后置代码等。客户端在调用的时候,面向的是这个增强的子类。
总结:
要增强的目标类实现了接口时,AOP底层调用的是JDK动态代理,没有实现接口时,AOP底层调用的是CGLib代理