Java静态代理和动态代理总结

Java静态代理和动态代理总结

1.前景铺垫

1.0 名词约定

在这里插入图片描述

1.1为什么需要代理?

  客户端不能或是不宜访问目标对象时,通过间接访问代理对象来访问目标对象。生活举例:

e.g.不能:普通用户不能向厂家直接购买散装的货品,只有中间商才能拿到厂家的货,然后以散装的形式发放给用户。

e.g.不宜:张三喜欢王翠花,但是张三信心不足,于是请求媒人【七大姑】【八大姨】代为向王翠花表达心意。

1.2代理的作用

  1. 控制访问/信息隐藏

    目标对象需要对客户端隐藏信息,所以通过代理对象来进行信息隐藏/控制访问

  2. 功能增强

    客户端访问目标对象时,希望有额外的诸如打印日志,事务提交的处理,这些处理不是目标对象的功能,所以交由代理对象处理,典型的如Spring核心思想之一AOP(面向切面编程)。

1.3代理的实现方式

  1. 静态代理
  2. 动态代理
    • jdk动态代理
    • cglib动态代理

2.静态代理

2.1基本概念

静态代理的要求:明确知道需要代理的目标对象。

静态代理的特点:

  1. 程序编译之后,代理对象即创建(静态代理由来)。
  2. 简单,直观(通过代理对象访问目标对象)

静态代理的适用场景

  1. 目标类明确

  2. 目标方法较少

    因为目标类的方法一旦增多,代理类的方法也会成倍增加。

静态代理使用步骤

  1. 创建接口,定义核心方法(IOP面向接口编程)

  2. 创建目标类,实现核心方法

  3. 创建代理类,调用目标类的核心方法,并可以做适当的处理,如日志记录等

2.2实战演练

基本思路:

接口定义两个方法,一个方法用于登录,一个方法用于修改密码。

目标类实现这两个方法

代理类在登录方法上,增加日志记录,在修改密码方法上,增加事务提交。

User

封装用户名和密码信息

public class User {

    private String username;
    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getUsername() {
        return username;
    }
    public String getPassword() {
        return password;
    }
}

UserService

定义login和updatePassword方法

public interface UserService {

    User logIn(String username, String password);
    int  updatePassword(String username,String newPassword);
}

UserServiceTarget

实现userService接口

public class UserServiceTarget implements UserService {
    /**
     *  下面代码仅模拟登陆和修改密码
     *  代码无实际作用
     */
    @Override
    public User logIn(String username, String password) {
        /**
         *  数据库查询操作,此处省略
         */

        //打印测试
        System.out.println("UserServiceTarget--> logIn is called");

        //假设数据库的确存在此用户
        return new User(username,password);
    }

    @Override
    public int updatePassword(String username, String newPassword) {
        /**
         *  数据库更新操作,此处省略
         */
        //假设更新密码成功,影响行数为1
        int changeRows  = 1;

        //打印测试
        System.out.println("UserServiceTarget--> updatePassword is called");

        return changeRows;
    }
}

UserServiceProxy

在登录方法上,增加日志记录,在修改密码上,增加事务提交。

public class UserServiceProxy  implements UserService {


    private UserService userServiceTarget;

    public UserServiceProxy(UserService userServiceTarget) {
        this.userServiceTarget = userServiceTarget;
    }

    public UserServiceProxy(){
    }

    @Override
    public User logIn(String username, String password) {

        //功能增强-->日志记录
        MyAspect.doLog();

        //打印测试
        System.out.println("UserServiceProxy--->  login  is called");

        return userServiceTarget.logIn(username,password);
    }

    @Override
    public int updatePassword(String username, String newPassword) {
        //打印测试
        System.out.println("UserServiceProxy--->  updatePassword  is called");

        int changeRows = userServiceTarget.updatePassword(username, newPassword);

        //功能增强 -->事务提交
        MyAspect.doTrans();

        return changeRows;
    }
}

MyAspect

辅助类,撰写日志记录和事务提交等非核心方法。

public class MyAspect {
    public static void doLog(){
        System.out.println("doLog is done at "+ DateFormat.getDateTimeInstance().format(new Date()));
    }

    public static void doTrans(){
        System.out.println("Transaction is commit ");
    }
}

测试代码

@Test
public void testStaticProxy(){
    UserService userServiceTarget = new UserServiceTarget();

    //代理对象明确知道目标对象
    UserService userServiceProxy  = new UserServiceProxy(userServiceTarget);

    userServiceProxy.logIn("123","123");
    System.out.println("-------------------分割线-------------------");
    userServiceProxy.updatePassword("123","123");

}

测试结果

doLog is done at 2020-8-6 16:33:17
UserServiceProxy—> login is called
UserServiceTarget–> logIn is called
-------------------分割线-------------------
UserServiceProxy—> updatePassword is called
UserServiceTarget–> updatePassword is called
Transaction is commit

3.动态代理

3.1基本概念

动态代理的实现方式

  1. jdk动态代理【掌握】

    要求目标类必须实现接口,底层原理反射机制,相关核心类或接口Method,Proxy,InvocationHandler。

  2. cglib动态代理【了解】

    要求目标类必须可以继承,cglib在许多框架应用广泛,比如Spring框架,运行效率高于jdk动态代理,但是实际开发中运用jdk动态代理较多。

动态代理的特点

  1. 代理类在程序运行时,才动态创建(动态代理由来)
  2. 使用相比静态代理更为复杂

动态代理适用场景

  1. 方法比较多,代理类的方法比较多时。

3.2JDK动态代理

  JDK动态代理的底层原理是反射机制,核心类是Method,Proxy,InvocationHandler。代理类是程序在运行时动态生成的。

3.2.0难点

  代理类是动态生成的,即代理类的底层实现,不像静态代理那样直观,但是事实上也没必要去深究代理类到底是怎样生成的,暂时知道怎样用就可以了。

在这里插入图片描述

  使用流程简介:

  1. 客户端即程序通过Proxy类创建代理对象。
  2. 代理对象调用核心功能方法时,都会经过Invocationhandler处理。
  3. InvocationHandler处理可包含下面两个
    • 调用目标对象方法:通过Method类,调用目标对象的方法,以达到访问目标对象方法的目的。
    • 进行功能增强:在InvocationHandler中进行如日志,事务的功能增强。
3.2.1关于Proxy、Invocationhandler、Method的介绍

  三者同属于package java.lang.reflect,即反射包中。由于反射比较复杂,简单解释如何使用jdk动态代理。

Proxy类

该类newProxyInstance方法用于动态产生代理对象

【核心方法newProxyInstance的源码】

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    Objects.requireNonNull(h);

    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    /*
     * Look up or generate the designated proxy class.
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }

        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        return cons.newInstance(new Object[]{h});
    } catch (IllegalAccessException|InstantiationException e) {
        throw new InternalError(e.toString(), e);
    } catch (InvocationTargetException e) {
        Throwable t = e.getCause();
        if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        } else {
            throw new InternalError(t.toString(), t);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString(), e);
    }
}

【源码方法头解释】

代码辅助理解

        //目标对象
        UserService target = new UserServiceImpl();

        //创建调用处理器
        InvocationHandler userServiceInvocationHandler = new UserServiceInvocationHandler(target);

        //生成代理对象
        UserService proxy = (UserService)Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),userServiceInvocationHandler);

​ 如果想生成代理对象,那么肯定需要知道目标对象的有关信息代理对象在调用方法时需要做怎样的处理

  1. ClassLoader loader即类加载器,使用target.getClass().getClassLoader()获取userService的类加载器,它的作用是向内存中加载对象(了解)。
  2. Class<?>[] interfaces即目标对象实现的接口,使用target.getClass().getInterfaces()获得。
  3. InvocationHandler h,即调用处理器,利用InvocationHandler实现相关的处理,参见下文有详解。
  4. 方法体不用去看,需要的时候再去看。

Method类

该类invoke方法用于目标对象方法的调用。

【核心方法invoke的方法头】

public Object invoke(Object obj, Object... args)
    throws IllegalAccessException, IllegalArgumentException,
       InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, obj, modifiers);
        }
    }
    MethodAccessor ma = methodAccessor;             // read volatile
    if (ma == null) {
        ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
}

【源码方法头解释】

以两行代码辅助理解

UserService userService = new UserServiceImpl();
userService.login("lordbao","123")//login返回User对象
  1. Object obj即方法的调用者,对应login方法调用者的userService。

  2. Object... args参数列表,对应login方法的实际参数 lordbao,123

  3. 返回对象Object,对应login方法的返回对象User

  4. 方法体不用去看,需要的时候再去看。

通过Method类,将上面代码改造如下,将产生同样效果

UserService userService = new UserServiceImpl();

//利用反射拿到logIn 方法
Method logIn = userService.getClass().getMethod("logIn", String.class, String.class);
logIn.invoke(userService,"lordbao","123");

也就是说,如果能拿到login方法 对应的 Method 实体对象 login ,那么通过login对象就能调用userService的login方法。

InvocationHandler接口

调用处理器,当代理对象进行方法调用时,都会经由InvocationHandler的invoke方法处理。

【核心invoke方法源码】

public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable;

【源码方法头解释】

以代码辅助理解

public class UserServiceInvocationHandler implements InvocationHandler {

    //目标对象    用户业务类
    private UserService target;

    //构造方法传入目标对象
    public UserServiceInvocationHandler(UserService target) {
        this.target = target;
    }

    /**
     *     假设
     *     登录时,需要记录日志
     *     修改密码时,需要提交事务
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        if (method.getName().equals("logIn")){
            MyAspect.doLog();
        }

        //调用核心业务功能方法
        Object result=  method.invoke(target,args);

        if (method.getName().equals("updatePassword")){
            MyAspect.doTrans();
        }

        return result;
    }
  1. proxy即代理对象,当代理对象调用方法时,会动态地传入代理对象proxy。
  2. method即目标对象的方法,通过method就可以实现目标方法的调用。
  3. args即代理对象调用方法时,传入的实际参数。

正如上文所提到到,通过Invocationhandler可以实现目标对象的方法调用和功能增强。

那么?

Q:目标对象如何得到?

A:利用构造方法,将目标对象传入。

Q:为什么通过Invocationhandler,就能实现目标对象的方法调用和功能增强呢?

A:当代理对象调用方法时,都会经由InvocationHandler的invoke方法处理,可以通过method对象来进行判断,以实现对不同目标对象方法的增强。

3.2.2实战演练

基本思路

接口定义两个方法,一个方法用于登录,一个方法用于修改密码。

目标类实现这两个方法

实现InvocationHandler,重写invoke方法,并新建构造方法以实现目标对象的传入

在InvocationHandler的实现类中,进行功能增强和目标方法调用

创建代理对象,调用相关方法

User

封装用户名和密码信息

public class User {

    private String username;
    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getUsername() {
        return username;
    }
    public String getPassword() {
        return password;
    }
}

UserService

定义login和updatePassword方法

public interface UserService {

    User logIn(String username, String password);
    int  updatePassword(String username,String newPassword);
}

UserServiceImpl

实现userService接口

public class UserServiceImpl implements UserService {

    /**
     *  下面代码仅模拟登陆和修改密码
     *  代码无实际作用
     */
    @Override
    public User logIn(String username, String password) {
        /**
         *  数据库查询操作,此处省略
         */

        System.out.println("logIn is called");
        //假设数据库的确存在此用户
        return new User(username,password);
    }

    @Override
    public int updatePassword(String username, String newPassword) {
        /**
         *  数据库更新操作,此处省略
         */
        //假设更新密码成功,影响行数为1
        int changeRows  = 1;
        System.out.println("updatePassword is called");
        return changeRows;
    }
}

UserServiceInvocationHandler

实现InvocationHandler接口

public class UserServiceInvocationHandler implements InvocationHandler {

    //目标对象    用户业务类
    private UserService target;

    //构造方法传入目标对象
    public UserServiceInvocationHandler(UserService target) {
        this.target = target;
    }

    /**
     *     假设
     *     登录时,需要记录日志
     *     修改密码时,需要提交事务
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {


        if (method.getName().equals("logIn")){
            MyAspect.doLog();
        }

        //调用核心业务功能方法
        Object result=  method.invoke(target,args);

        if (method.getName().equals("updatePassword")){
            MyAspect.doTrans();
        }

        return result;
    }
}

其中:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 

proxy即代理对象,基本用不到,你可以打印它的Class玩玩。

method即目标方法

args即实际参数

if (method.getName().equals("logIn")){
            MyAspect.doLog();
}

 if (method.getName().equals("updatePassword")){
            MyAspect.doTrans();
}

可以通过method的方法名来对不同的方法进行增强

       //调用核心业务功能方法
        Object result=  method.invoke(target,args);

  即为调用目标对象的方法,不过需要注意,这个method对象传入的参数必须是目标对象,这也是为什么要用构造方法传入目标对象的原因,只有这样才能实现目标对象方法的调用,args即方法的实际参数。

 return result;

返回方法调用结果。

MyAspect

辅助类,撰写日志记录和事务提交等非核心方法。

public class MyAspect {
    public static void doLog(){
        System.out.println("doLog is done at "+ DateFormat.getDateTimeInstance().format(new Date()));
    }

    public static void doTrans(){
        System.out.println("Transaction is commit ");
    }
}

测试代码

@Test
public void testDynamicProxy(){

    //目标对象
    UserService target = new UserServiceImpl();
    System.out.println(target.getClass().getName());

    //创建调用处理器,并传入目标对象
    InvocationHandler userServiceInvocationHandler = new UserServiceInvocationHandler(target);

    //生成代理对象
    UserService proxy = (UserService)Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),userServiceInvocationHandler);
    System.out.println(proxy.getClass().getName());

    proxy.logIn(null,null);
    System.out.println("...........................................");
    proxy.updatePassword(null,null);

}

效果

com.lordbao.service.impl.UserServiceImpl
com.sun.proxy.$Proxy4
doLog is done at 2020-8-6 20:46:38
logIn is called

updatePassword is called
Transaction is commit

3.3cglib动态代理

  请移步别人的博客https://cloud.tencent.com/developer/article/1429932

  总结,代理的作用主要是为了控制访问和功能增强,其中控制访问在代码演练中没提到,基本思路是可以通过method对象和实际参数args来实现控制访问,比如只有管理者root时,才执行method.invoke方法。

  静态代理和动态代理的适用场景各有不同,杀鸡就用小刀,杀牛就用大刀。

  动态代理的jdk动态代理的难点说白了就是不熟悉,多敲代码然后看原理,看完原理然后敲代码,如此折腾,终会搞懂的。

写完收工!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值