学习Spring AOP必读--静态代理、动态代理、cglib代理与Spring AOP的处理
代理介绍
代理(Proxy)是一种设计模式, 提供了对目标对象另外的访问方式;即通过代理访问目标对象。 这样好处: 可以在目标对象实现的基础上,增强额外的功能操作。(扩展目标对象的功能)。
简单理解代理: 代理其实和现实世界中,明星的经纪人很类似。真正接受工作,要去表演的其实还是明星,但是明星一个人不可能直接和你对接,我们一般找到的都是明星的经纪人。右经济人帮明星筛选接收工作。也就是明星真正做事之前和做事之后的工作都由经纪人帮忙完成了。
经纪人接收工作(洽谈工作,签合约,安排时间,前期宣传等等...)
明星演戏唱歌
经纪人收尾工作(收尾款,后续宣传等等...)
经纪人就是代理,实际上台唱歌、表演的还是明星
这样的例子,我们换成实际的代码写出来,最简单的写法就是静态代理
静态代理
直接用例子来说明,比如已经有一个我们常写的IUserDao接口:
// 接口
public interface IUserDao {
void save();
}
实现类UserDaoImpl:
public class UserDaoImpl implements IUserDao{
@Override
public void save() {
System.out.println("-----保存数据------");
}
}
现在,要在save()方法保存数据前开启事务、保存数据之后关闭事务…当然啦,在业务少的时候,直接在save()方法中介绍事务就可以了
public void save() {
System.out.println("开启事务");
System.out.println("-----保存数据------");
System.out.println("关闭事务");
}
但是,如果我有好多个业务方法都需要开启事务、关闭事务呢?
public void save() {
System.out.println("开启事务");
System.out.println("-----保存数据-----");
System.out.println("关闭事务");
}
public void delete() {
System.out.println("开启事务");
System.out.println("-----删除数据-----");
System.out.println("关闭事务");
}
public void update() {
System.out.println("开启事务");
System.out.println("-----更新数据-----");
System.out.println("关闭事务");
}
public void login() {
System.out.println("开启事务");
System.out.println("-----登录-----");
System.out.println("关闭事务");
}
这样,就有了很多很多的重复代码了…
于是呢,我们就请了一个代理,但是要注意两点:
- 这个代理要和IUserDao有相同的方法
- 代理只是对IUserDao进行增强,真正做事的还是UserDao 因此,代理就要实现IUserDao接口,这样的话,代理就跟IUserDao有相同的方法了。
public class UserDaoProxy implements IUserDao{
//实现IUserDao,保持和IUserDao一样的方法
//并且将IUserDao作为自身的一个属性,这里实际需要的是IUserDao的实现类UserDaoImpl
private IUserDao target;
public UserDaoProxy(IUserDao target) {
this.target = target;
}
@Override
public void save() {
System.out.println("开始事务...");
target.save(); // 执行目标对象的方法
System.out.println("提交事务...");
}
}
外界并不是直接去找UserDaoImpl,而是要通过代理才能找到UserDaoImpl
public static void main(String[] args) {
// 目标对象
IUserDao target = new UserDaoImpl();
// 代理
IUserDao proxy = new UserDaoProxy(target);
proxy.save(); // 执行的是,代理的方法
}
这样在UserDaoImpl里面就不用在每个方法中都写入事务这些重复的代码了。
动态代理
为什么还要动态代理? 首先来看一下静态代理的不足:
- 如果接口改了,代理的也要跟着改...这样很不方便
- 因为代理对象,需要与目标对象实现一样的接口。所以会有很多代理类,反而显得很不方便。
动态代理比静态代理好的地方:
- 代理对象,不需要实现接口,这样就不会有太多的代理类了
- 代理对象的生成,是利用JDKAPI, 动态地在内存中构建代理对象
Java提供了一个Proxy类,调用它的newInstance方法可以生成某个对象的代理对象,该方法需要三个参数:
/**
* Returns an instance of a proxy class for the specified interfaces
* that dispatches method invocations to the specified invocation
* handler.
*
* <p>{@code Proxy.newProxyInstance} throws
* {@code IllegalArgumentException} for the same reasons that
* {@code Proxy.getProxyClass} does.
*
* @param loader the class loader to define the proxy class
* @param interfaces the list of interfaces for the proxy class
* to implement
* @param h the invocation handler to dispatch method invocations to
* @return a proxy instance with the specified invocation handler of a
* proxy class that is defined by the specified class loader
* and that implements the specified interfaces
* @throws IllegalArgumentException if any of the restrictions on the
* parameters that may be passed to {@code getProxyClass}
* are violated
* @throws SecurityException if a security manager, <em>s</em>, is present
* and any of the following conditions is met:
* <ul>
* <li> the given {@code loader} is {@code null} and
* the caller's class loader is not {@code null} and the
* invocation of {@link SecurityManager#checkPermission
* s.checkPermission} with
* {@code RuntimePermission("getClassLoader")} permission
* denies access;</li>
* <li> for each proxy interface, {@code intf},
* the caller's class loader is not the same as or an
* ancestor of the class loader for {@code intf} and
* invocation of {@link SecurityManager#checkPackageAccess
* s.checkPackageAccess()} denies access to {@code intf};</li>
* <li> any of the given proxy interfaces is non-public and the
* caller class is not in the same {@linkplain Package runtime package}
* as the non-public interface and the invocation of
* {@link SecurityManager#checkPermission s.checkPermission} with
* {@code ReflectPermission("newProxyInPackage.{package name}")}
* permission denies access.</li>
* </ul>
* @throws NullPointerException if the {@code interfaces} array
* argument or any of its elements are {@code null}, or
* if the invocation handler, {@code h}, is
* {@code null}
*/
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
- 参数一:生成代理对象使用哪个类装载器
- 参数二:生成哪个对象的代理对象,通过接口指定,指定要代理类的接口
- 参数三:生成的代理对象的方法里干什么事,实现handler接口
在编写动态代理之前,要明确几个概念:
- 代理对象拥有目标对象相同的方法,因为参数二指定了对象的接口
- 用户调用代理对象的什么方法,都是在调用处理器的invoke方法。
- 使用JDK动态代理必须要有接口
比如之前举例的明星例子: Andy Lau是一个明星,实现了人的接口: AndyLau.java对象:
public class AndyLau implements Person {
@Override
public void sing(String name) {
System.out.println("Andy Lau 唱" + name);
}
@Override
public void dance(String name) {
System.out.println("Andy Lau 跳" + name);
}
}
Person.java接口
public interface Person {
void sing(String name);
void dance(String name);
}
AndyLau的代理类AndyLauProxy.java:
public class AndyLauProxy {
//代理只是一个经纪人,实际干活的还是andyLau,于是需要在代理类上维护andyLau这个变量
AndyLau andyLau = new AndyLau();
//返回代理对象
public Person getProxy() {
/**
* 参数一:代理类的类加载器
* 参数二:被代理对象的接口
* 参数三:InvocationHandler实现类
*/
return (Person)Proxy.newProxyInstance(AndyLauProxy.class.getClassLoader(), andyLau.getClass().getInterfaces(), new InvocationHandler() {
/**
* proxy : 把代理对象自己传递进来
* method:把代理对象当前调用的方法传递进来
* args:把方法参数传递进来
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//如果别人想要让AndyLau唱歌
if (method.getName().equals("sing")) {
System.out.println("please pay money $10000000");
//实际上唱歌的还是AndyLau
method.invoke(andyLau, args);
}
return null;
}
});
}
}
测试类:
public static void main(String[] args) {
//外界通过代理才能让Andy Lau唱歌
AndyLauProxy andyLauProxy = new AndyLauProxy();
Person proxy = andyLauProxy.getProxy();
proxy.sing("爱你一万年");
}
cglib代理
由于静态代理需要实现目标对象的相同接口,那么可能会导致代理类会非常非常多....不好维护,因此出现了动态代理
动态代理也有个约束:目标对象一定是要有接口的,没有接口就不能实现动态代理.....因此出现了cglib代理
cglib代理也叫子类代理,从内存中构建出一个子类来扩展目标对象的功能!
CGLIB是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口。它广泛的被许多AOP的框架使用,例如Spring AOP,为他们提供方法的interception(拦截)。
编写cglib代理
接下来我们就讲讲怎么写cglib代理:
- 需要引入cglib.jar文件, 但是spring的核心包中已经包括了cglib功能,所以直接引入spring-core-xxxx.jar即可。
- 引入功能包后,就可以在内存中动态构建子类
- 代理的类不能为final,否则报错
- 目标对象的方法如果为final/static, 那么就不会被拦截,即不会执行目标对象额外的业务方法。
比如有下面的代码:
UserDaoImpl.java类,注意这里并没有接口
package com.yingside.dao;
public class UserDaoImpl {
public void save(){
System.out.println("正在保存");
}
}
cglib实现代理ProxyFactory.java类:
package com.yingside.proxy;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class ProxyFactory implements MethodInterceptor {
// 维护目标对象
private Object target;
public ProxyFactory(Object target){
this.target = target;
}
// 给目标对象创建代理对象
public Object getProxyInstance(){
//1. 工具类
Enhancer en = new Enhancer();
//2. 设置父类
en.setSuperclass(target.getClass());
//3. 设置回调函数
en.setCallback(this);
//4. 创建子类(代理对象)
return en.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("==================开始事务==================");
// 执行目标对象的方法
Object returnValue = method.invoke(target, args);
System.out.println("==================提交事务==================");
return returnValue;
}
}
测试类:
package com.yingside.test;
import com.yingside.dao.UserDaoImpl;
import com.yingside.proxy.ProxyFactory;
public class Test {
@org.junit.Test
public void cglibProxy(){
UserDaoImpl userDaoImpl = new UserDaoImpl();
UserDaoImpl factory = (UserDaoImpl) new ProxyFactory(userDaoImpl).getProxyInstance();
factory.save();
}
}
Spring AOP的理解
重点理解:
Aop: aspect object programming 面向切面编程 功能: 让关注点代码与业务代码分离! 面向切面编程就是指: 对很多功能都有的重复的代码抽取,再在运行的时候往业务方法上动态植入“切面类代码”。 关注点:重复代码就叫做关注点。
下面的代码是使用Hibernate保存用户的片段:
// 保存一个用户
public void add(User user) {
Session session = null;
Transaction trans = null;
try {
session = HibernateSessionFactoryUtils.getSession(); // 【关注点代码】
trans = session.beginTransaction(); // 【关注点代码】
session.save(user); // 核心业务代码
trans.commit(); //【关注点代码】
} catch (Exception e) {
e.printStackTrace();
if(trans != null){
trans.rollback(); //【关注点代码】
}
} finally{
HibernateSessionFactoryUtils.closeSession(session); //【关注点代码】
}
}
什么是切面?
关注点形成的类,就叫切面(类)
public class AOP {
public void begin() {
System.out.println("==========开始事务==========");
}
public void close() {
System.out.println("==========关闭事务==========");
}
}
切入点:
- 执行目标对象方法,动态植入切面代码。
- 可以通过切入点表达式,指定拦截哪些类的哪些方法; 给指定的类在运行的时候植入切面类代码。 切入点表达式:
- 指定哪些类的哪些方法被拦截
Spring 注解方式实现AOP编程
我们之前手动的实现AOP编程是需要自己来编写代理工厂的,现在有了Spring,就不需要我们自己写代理工厂了。Spring内部会帮我们创建代理工厂。
也就是说,不用我们自己写代理对象了。
因此,我们只要关心切面类、切入点、编写切入表达式指定拦截什么方法就可以了!
pom.xml文件 主要是需要导入的包
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>3.2.18.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>3.2.18.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>3.2.18.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.4</version>
</dependency>
spring配置文件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:p="http://www.springframework.org/schema/p"
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
http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 定义包扫描器,指定到哪个包下面找到bean -->
<context:component-scan base-package="com.yingside"/>
<!-- 开启aop注解方式 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
IUserDao接口:
public interface IUserDao {
public void save();
}
UserDaoImpl实现类:
import org.springframework.stereotype.Component;
@Component(value = "userDao")
public class UserDaoImpl implements IUserDao{
public void save(){
System.out.println("正在保存");
}
}
上面的UserDaoImpl.java文件里面的save()方法很明显是我们要执行的方法,现在我们需要的是在save()方法上添加代理,有了Spring管理之后,就不需要我们再像之前人为的编写Proxy代码了。现在直接转变思维,需要关注的是切面类,其实也就是像之前一样需要加到save()方法前后的一些重复代码。只不过把这些提取出来,容易形成一个类,就叫做切面类
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect//指定为切面类
public class AOP {
//里面的值为切入点表达式
@Before("execution(* com.yingside..*.*(..))")
public void begin() {
System.out.println("=====开始事务=====");
}
@After("execution(* com.yingside..*.*(..))")
public void close() {
System.out.println("=====关闭事务=====");
}
}
简单解释一下上面的切入点表达式: 格式:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
括号中各个pattern分别表示:
* 修饰符匹配(modifier-pattern?)
* 返回值匹配(ret-type-pattern)可以为*表示任何返回值,全路径的类名等
* 类路径匹配(declaring-type-pattern?)
* 方法名匹配(name-pattern)可以指定方法名 或者 *代表所有, set* 代表以set开头的所有方法
* 参数匹配((param-pattern))可以指定具体的参数类型,多个参数间用“,”隔开,各个参数也可以用“*”来表示匹配任意类型的参数,如(String)表示匹配一个String参数的方法;(*,String) 表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是String类型;可以用(..)表示零个或多个任意参数
* 异常类型匹配(throws-pattern?)
* 其中后面跟着“?”的是可选项
如果觉得太复杂,下面的解释可能更适合你看:
1)execution(* *(..))
//表示匹配所有方法
2)execution(public * com.yingside.service.UserService.*(..))
//表示匹配com.yingside.server.UserService中所有的公有方法
3)execution(* com.yingside..*.*(..))
//表示匹配com.yingside包及其子包下的所有方法
最后的测试:
@org.junit.Test
public void sprintAop(){
ApplicationContext ac =
new ClassPathXmlApplicationContext("applicationContext.xml");
//这里得到的是代理对象....
IUserDao iUser = (IUserDao) ac.getBean("userDao");
System.out.println(iUser.getClass());
iUser.save();
}
Comments