一、动态代理
代理模式就是创造一个代理对象来代替真实对象的访问,目的是在不改变原始对象代码的前提下,对其功能进行增强、修改等操作。
代理必须要实现的步骤是:
1、建立代理对象和真实对象的代理关系
2、实现代理对象的逻辑方法
1.1 JDK动态代理
JDK动态代理是java.lang.reflect.*下提供的,他必须借助一个借口才能产生代理对象,所以先定义一个接口:
public interface HelloWorld{
public void sayHello();
}
然后提供一个实现类:
public class HelloWorldImpl implements HelloWorld{
@Override
public void sayHello(){
System.out.println("hello world");
}
}
上述的一个接口,一个实现类已经有了,下面开始动态代理,建立关系和实现代理逻辑。在JDK动态代理中,实现代理逻辑的类必须实现接口java.lang.reflect.InvocationHandler接口,里面定义了一个invoke方法,并提供接口组来下挂代理对象,下面定义一个代理类:
public class JdkProxy implements InvocationHandler{
//真实对象
private Object target = null;
/**
*建立代理关系并返回代理对象
*/
public Object bind(Object target){
this.target = target;
return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
}
/**
*代理方法逻辑
*@param proxy 代理对象
*@param method 当前调度的方法
*@param args 当前方法的参数
*/
@Override
public Object invoke(Object proxy,Method method,Object [] args) throw Throwable{
System.out.println("进入代理方法");
System.out.println("调度真实方法之前的逻辑");
Objetc obj = method.invoke(target,args);//这句就是调度原始对象的sayhello方法
System.out.println("调度真实方法之后的逻辑");
return obj;
}
}
下面解析步骤:
建立代理和真实对象的关系:通过代理类的bind方法来建立,其中的Proxy.newProxyInstance( … )是生成代理对象的方法,其中的参数解释如下:
- 第一个是类加载器,采用target本身的类加载器。
- 第二个是把代理对象下挂到哪些接口下,上述例子是挂在HelloWorld下,因此可以这样声明,HelloWorld proxy = xxxx;
- 第三个是定义实现方法逻辑的代理类,this表示当前对象,这个代理类必须实现InvocationHandler接口。
实现代理逻辑方法:其中的invoke方法即可实现,解释一下三个参数: - proxy:bind方法生成的代理对象
- method:当前调度的方法
- args:方法需要的参数
当我们使用proxy调用其下挂接口HelloWorld的sayHello方法后,会进入上述代理类的invoke方法。这就是JDK动态代理的简要概述。
1.2 CGLIB动态代理
相比于JDK,这个代理技术不需要借助接口,而是需要一个非抽象类即可。
下面使用CGLIB技术写一个代理类:
public class CglibProxy implements MethodIntercepter {
/**
*生成CGLIB代理对象
*@param cls ——class类
*@return Class类的CGLIB代理对象
*/
public Object getProxy(Class cls){
//CGLIB enhancer增强类对象
Enhancer enhancer = new Enhancer();
//设置增强类型
enhancer.setSuperclass(cls);
//定义代理逻辑对象为当前对象,要求当前对象实现MethodInterceptor方法
enhancer.setCallBack(this);
//生成并返回代理对象
return enhancer.create();
}
/**
*代理逻辑方法
*@param proxy 代理对象
*@param method 方法
*@param args 参数
*@param methodProxy 方法代理
*@return 代理逻辑返回
*/
@Override
public Object intercept(Object proxy,Method method,Object [] args,MethodProxy methodProxy) throws Throwable{
System.out.println("调用真实对象前");
// CGLIB 反射调用真实对象方法
Object result = methodProxy.invokeSuper(proxy,args);
System.out.println("调用真实对象后");
return reslt;
}
}
上述的CGLIB技术要求代理对象实现接口MethodInterceptor的方法intercept,此时intercept方法就是代理逻辑方法,下面看下如何使用:
public test(){
CglibProxy cpe = new CglibProxy();
ServiceImpl obj = (ServiceImpl)cpe.getProxy(ServiceImpl.class);
obj.sayHello("张三");
}
上述的obj.sayHello( )会调用代理的intercept方法进行增强功能。注意CGLIB的增强类enhancer的使用。
二、AOP
AOP是一种约定优于配置的编程方式,将我们自己的代码织入到约定好的流程中,Spring支持多种配置方式,如xml和注解等,我们就用SpringBoot的注解方式介绍。
2.1 为什么使用AOP
AOP最典型的应用就是数据库事务,可以将一些重复且繁琐的内容通过约定处理,这样我们就不必为繁琐的try-catch和jdbc等代码感到啰嗦了,可以将创建数据库连接、开启事务、关闭连接等操作织入到一个默认的流程中,几乎所有的事务都需要走这样的流程。
2.1.1 AOP术语和流程
- 连接点:对应的是具体被拦截的对象,在Spring中指一个类的方法,AOP会使用动态代理将其织入到流程。
- 切点:通过正则等方式去适配多个连接点,这就是切点的功能。
- 通知:根据约定的流程,分为前置通知、后置通知等。
- 目标对象:被代理的对象。
- 引入:引入新的类或方法来增强现有的bean。
- 织入:一种通过动态代理的技术生成代理对象,将切点匹配到连接点,并按照约定将各类通知织入到流程中。
- 切面:是一个可以定义切点、各类通知和引入的内容。
上面是一个流程图,简要的介绍了AOP的流程。
2.2 AOP开发详解
下面使用@AspectJ的方式开发AOP,因为SpringAOP只支持对方法进行拦截,因此首先要确定拦截什么方法。
2.2.1 确定连接点
首先准备一个接口和一个实现类:
public interface UserService{
public void printUser(User user);
}
public class UserServiceImpl implements UserService{
@Override
public void printUser(User user){
if(user == null){
throw new RuntimeException("用户参数为空");
}
System.out.println(user.getUid());
}
}
我们将以printUser( )为连接点开发切面。
2.2.2 开发切面
有了上述的连接点后,还需要一个切面来描述AOP的信息,用于描述AOP流程的织入。
@AspectJ
public class MyAspect{
@Before("execution(*com.springboot.service.impl.UserServiceImpl.printUser(..))")
public void before(){
System.out.pringln("before....");
}
@After("execution(*com.springboot.service.impl.UserServiceImpl.printUser(..))")
public void after(){
System.out.pringln("after....");
}
@AfterReturning("execution(*com.springboot.service.impl.UserServiceImpl.printUser(..))")
public void afterReturning(){
System.out.pringln("afterReturning....");
}
@AfterThrowing("execution(*com.springboot.service.impl.UserServiceImpl.printUser(..))")
public void afterThrowing(){
System.out.pringln("afterThrowing....");
}
}
上述是一个切面的案例,其中需要关注的是@AspectJ注解,这个注解标识的类就会被认为是一个切面,而@Before等是作为通知,会根据注解里的execution表达式去匹配连接点并最后织入到流程中。
各个通知的顺序如上述流程图的顺序。
2.2.3 切点
可以看到上述的通知注解的连接点都是重复的,因此SpringAOP提供了切点的概念,切点向Spring描述哪些类的哪些方法需要开启AOP。而代码可以修改如下:
@AspectJ
public class MyAspect{
@Pointcut("execution(*com.springboot.service.impl.UserServiceImpl.printUser(..))")
public void pointCut(){
}
@Before("pointCut()")
public void before(){
System.out.pringln("before....");
}
@After("pointCut()")
public void after(){
System.out.pringln("after....");
}
@AfterReturning("pointCut()")
public void afterReturning(){
System.out.pringln("afterReturning....");
}
@AfterThrowing("pointCut()")
public void afterThrowing(){
System.out.pringln("afterThrowing....");
}
}
这时我们可以在通知注解上引用被@Pointcut标注的方法来描述连接点,下面来介绍一下连接点正则式:
execution(*com.springboot.service.impl.UserServiceImpl.printUser(…))
其中:
- execution:表示在执行的时候拦截里面正则匹配的方法
- ‘*’:*表示任意返回类型
- com.springboot.service.impl.UserServiceImpl:指向目标对象的全限定名称
- printUser:指定目标对象的方法
- ( ):表示任意参数进行匹配,注意一定只有两个小数点
对于正则式,还可以使用Aspect的指示器:
项目类型 | 描述 |
---|---|
arg( ) | 限定连接点方法参数 |
@args( ) | 通过连接点方法参数上的注解进行限定 |
execution( ) | 用于匹配是连接点的执行方法 |
this( ) | 限制连接点匹配AOP代理Bean引用为指定的类型 |
target | 目标对象 |
@target( ) | 限制目标对象的配置了指定的注解 |
within | 限制连接点匹配指定的类型 |
@within( ) | 限制连接点带有匹配注解的类型 |
@annotation( ) | 限制带有指定注解的连接点 |
2.2.4 环绕通知
环绕通知最为强大,且难以控制,除非是大幅度的修改原有的方法,否则不建议使用,他是取代原有方法的通知,同时也能调度原有的方法。下面在原来的切面中加入环绕通知:
@Around("pointCut()")
public void around(ProceedingJoinPoint jp) throws Throwable{
...
//这是回调原始方法,如果不需要完全可以去掉
jp.proceed();
...
}
这个通知有一个参数ProceedingJoinPoint,这个参数对象有一个procced方法用来调用连接点原有的方法。
下面来测试一下上述AOP:
//定义控制器
@Controller
//定义请求路径
@RequestMapping("/user")
public class UserController{
// 注入用户服务
@Autowired
private UserService userService = null;
//定义请求
@RequestMapping("/print")
//转换为json
@ResponseBody
public User printUser(Long id,String userName){
User user = new User();
user.setId(id);
user.setUserName(userName);
userService.printUser(user);//如果为null则抛出异常
}
}
2.2.5 引入
假设这样,之前的被代理的类只有打印功能,如果我们需要校验这个user对象是否为空,如果空则不打印,而且这个原有的类不可以修改,此时我们只能新定义一个接口来引入相关的校验功能:
//用户校验接口
public interface UserValidator{
public boolean validate(User user);
}
//实现类
public class UserValidatorImpl implements UserValidator{
@Override
public boolean validate(User user){
return user != null;
}
}
这样我们能够通过AOP的引入来增强UserService接口了,修改原来的切面
@AspectJ
public class MyAspect{
@DeclareParents(value="com.springboot.service.impl.UserServiceImpl+",defaultImpl=UserValidatorImpl.class)
public UserValidator userValidator;
...
}
这里我们看到一个注解@DeclareParents,他的作用是引入新的类来增强服务,必须配置两个属性,value和defaultImpl。
- value:指向要增强的目标对象
- defaultImpl:引入增强的类
下面在之前的测试代码加入如下方法:
@RequestMapping("/vp")
@ResponseBody
public User volidateAndPrint(Long id,String userName){
User user = new User();
user.setId(id);
user.setUserName(userName);
//强制转换
UserValidator userVolidator = (UserValidator)userService;
//校验用户是否为空
if(userVolidator.validate(user)){
userService.printUser(user);
}
return user;
}
可以看到,增强了功能,那么是如何实现的呢,就是绑定代理关系的方法的第二个参数,他是一个接口数组,代理对象可以下挂到这些接口,因此可以通过强制转换来达到不同的功能。
2.2.6 通知获取参数
上述的通知中都没有传递参数,我们可以传递参数进入,对于非环绕通知,还可以使用一个连接点(joinpoint)类型的参数,看如下修改:
@Before("pointCut() && args(user)")
public void before(JoinPoint point,User user){
Object[] args = point.getArgs();
System.out.println("before....");
}
其中,正则式的pointCut代表继续使用原来定义的切点规则,并约定将连接点名为user的参数传递进来,但对于环绕通知,可以使用ProceedingJoinPoint类型的参数,通过这个参数可以得到原始连接点的参数。
2.2.7 织入
织入是一个生成动态代理对象并且将切入点和目标对象方法编织为约定的流程的过程,在这个过程中,如果目标对象有接口,SpringAOP会默认使用JDK动态代理,否则使用CGLIB动态代理。对于多个切面,默认执行顺序是混乱的,可以使用注解@Order或将切面实现接口Ordered,可以定义多个切面的执行顺序。
三、总结
根据上述的介绍,相信对动态代理和AOP有了一定的认识,AOP的应用很多,比如业务层的拦截器,数据库事务等,至于这些就需要我们从工作实践中去体会了。