Spring系列之静态代理、动态代理、cglib代理与Spring AOP的处理

本章内容

  • 代理的概念及理解
  • 如何实现静态代理
  • 如何实现动态代理
  • 静态代理与动态代理有什么区别与优缺点
  • JDK动态代理如何实现
  • cglib动态代理如何实现
  • JDK动态代理与cglib动态代理的区别
  • SpringAOP中关键概念有哪些
  • 如何实现Spring中的AOP

一、代理的概述

1.1 什么是代理?代理的好处?

代理(Proxy)是一种设计模式, 提供了对目标对象另外的访问方式;即通过代理访问目标对象。

这样做的好处: 可以在目标对象实现的基础上,增强额外的功能操作。(扩展目标对象的功能)。

用通俗的语言来说,代理其实和现实世界中,明星的经纪人很类似。真正接受工作,要去表演的其实还是明星,但是明星一个人不可能直接和你对接,我们一般找到的都是明星的经纪人。右经济人帮明星筛选接收工作。也就是明星真正做事之前和做事之后的工作都由经纪人帮忙完成了。

  1. 经纪人接收工作(洽谈工作,签合约,安排时间,前期宣传等等...)
  2. 明星演戏唱歌
  3. 经纪人收尾工作(收尾款,后续宣传等等...)

经纪人就是代理,实际上台唱歌、表演的还是明星

代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。

代理模式的UML图

 为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。

更通俗的说,代理解决的问题当两个类需要通信时,引入第三方代理类,将两个类的关系解耦,让我们只了解代理类即可,而且代理的出现还可以让我们完成与另一个类之间的关系的统一管理,但是切记,代理类和委托类要实现相同的接口,因为代理真正调用的还是委托类的方法。

1.2 使用场合

如果需要委托类处理某一业务,那么我们就可以先在代理类中统一处理然后在调用具体实现类

1.3 分类

按照代理的创建时期,代理类可以分为两种: 

静态:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。

动态:在程序运行时运用反射机制动态创建而成。

二、静态代理

比如已经有一个我们常写的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里面就不用在每个方法中都写入事务这些重复的代码了

三、JDK动态代理

3.1 分析利弊

静态代理的不足:

  • 如果接口改了,代理的也要跟着改...这样很不方便
  • 因为代理对象,需要与目标对象实现一样的接口。所以会有很多代理类,反而显得很不方便。

动态代理比静态代理好的地方:

  • 代理对象,不需要实现接口,这样就不会有太多的代理类了
  • 代理对象的生成,是利用JDKAPI, 动态地在内存中构建代理对象

Java提供了一个Proxy类,调用它的newInstance方法可以生成某个对象的代理对象,该方法需要三个参数:

@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException

其中

  • loader表示生成代理对象使用哪个类装载器
  • interfaces表示生成哪个对象的代理对象,通过接口指定,指定要代理类的接口
  • h表示生成的代理对象的方法里干什么事,实现handler接口

在编写动态代理之前,要明确几个概念:

  • 代理对象拥有目标对象相同的方法,因为参数二指定了对象的接口
  • 用户调用代理对象的什么方法,都是在调用处理器的invoke方法。
  • 使用JDK动态代理必须要有接口

3.2 JDK动态代理实例

Person.java接口

public interface Person {
    void sing(String name);
    void dance(String name);
}

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);
    }
}

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代理

4.1 概念

由于静态代理需要实现目标对象的相同接口,那么可能会导致代理类会非常非常多....不好维护,因此出现了动态代理

动态代理也有个约束:目标对象一定是要有接口的,没有接口就不能实现动态代理.....因此出现了cglib代理

cglib代理也叫子类代理,从内存中构建出一个子类来扩展目标对象的功能!

CGLIB是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口。它广泛的被许多AOP的框架使用,例如Spring AOP,为他们提供方法的interception(拦截)

4.2 实现cglib代理

怎么写cglib代理:

  • 需要引入cglib.jar文件, 但是spring的核心包中已经包括了cglib功能,所以直接引入spring-core-xxxx.jar即可。
  • 引入功能包后,就可以在内存中动态构建子类
  • 代理的类不能为final,否则报错
  • 目标对象的方法如果为final/static, 那么就不会被拦截,即不会执行目标对象额外的业务方法。

比如有下面的代码:

UserDaoImpl.java类,注意这里并没有接口

public class UserDaoImpl {
    public void save(){
        System.out.println("正在保存");
    }
}

cglib实现代理ProxyFactory.java类:

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;
    }
}

测试类

public class Test {
    @org.junit.Test
    public void cglibProxy(){
        UserDaoImpl userDaoImpl = new UserDaoImpl();
        UserDaoImpl factory = (UserDaoImpl) new ProxyFactory(userDaoImpl).getProxyInstance();
        factory.save();
    }
}

五、Spring AOP

5.1 基本概念

什么是AOP?

  1. Aop: aspect object programming 面向切面编程 
  2. 功能: 让关注点代码与业务代码分离!
  3.  面向切面编程就是指: 对很多功能都有的重复的代码抽取,再在运行的时候往业务方法上动态植入“切面类代码”。
  4.  关注点:重复代码就叫做关注点。

举例说明

// 保存一个用户
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("==========关闭事务==========");
    }
}

切入点:

  • 执行目标对象方法,动态植入切面代码
  • 可以通过切入点表达式,指定拦截哪些类的哪些方法; 给指定的类在运行的时候植入切面类代码
  • 指定哪些类的哪些方法被拦截

5.2 Spring 注解方式实现AOP编程

我们之前手动的实现AOP编程是需要自己来编写代理工厂的,现在有了Spring,就不需要我们自己写代理工厂了。Spring内部会帮我们创建代理工厂。

也就是说,不用我们自己写代理对象了。

因此,我们只要关心切面类、切入点、编写切入表达式指定拦截什么方法就可以了!

第一步:导入相应的依赖包

<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();
}

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值