代理模式

代理模式的定义:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。

通俗点说,就是一个中介,比如有一个广州人,是个本地人,有两套房,他要租出去收租,但是除了收租,他还要去找租客,带租客看房,还要准备租房合同,核算水电费等等,很麻烦。这个本地人他也不想这么折腾,他只想完成他的核心业务(收钱),其他杂七杂八的事情就不想管,但是总要有人去做,那就找租房中介,也就是二手房东。二手房东就代理这个广州本地人把房子租给租客。这个道理就是这么简单。

他们这些在广州有房子的本地人都可以找中介公司去代理租房是一样的。因为很多广州本地人都有这个需求,干脆就搞一个中介公司来专门去做租房子的事情。

代理模式,运用在编程里,也是这个道理,有一些非核心业务的代码,在很多地方都需要用到的逻辑,可以交给代理对象完成,程序员只需要关心核心业务的逻辑即可。

实现代理模式的三种方式

项目就基于上一篇模板模式的文章的项目进行试验。

静态代理

假设原来有一个接口UserService,controller层调用userServicegetAllUser()方法。如下所示:

public interface UserService {
    /**
     * 获取所有用户信息
     *
     * @return List
     * @author Ye hongzhi
     * @date 2020/4/12
     */
    List<User> getAllUser() throws Exception;
}
@RestController
@RequestMapping("/xiaoniu")
public class UserController {

    @Resource(name = "userService")
    private UserService userService;

    @RequestMapping("/getAllUser")
    public List<User> getAllUser()throws Exception{
        return userService.getAllUser();
    }
}

如果用静态代理实现记录日志信息,怎么记录呢?

首先创建一个代理类UserServiceProxy,实现UserService接口,然后在UserServiceProxy里面创建一个成员变量userService,再写一个有参构造器来初始化userService。代码如下:

public class UserServiceProxy implements UserService {

    private UserService userService;

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

    @Override
    public List<User> getAllUser() throws Exception {
        System.out.println("记录日志:执行getAllUser()方法前");
        List<User> userList = userService.getAllUser();
        System.out.println(userList);
        System.out.println("记录日志:执行getAllUser()方法后");
        return userList;
    }
}

所以在controller层调用的方式就要改一下,是用代理类UserServiceProxy调用getAllUser()方法。如下:

@RestController
@RequestMapping("/xiaoniu")
public class UserController {
    
    @Resource(name = "userService")
    private UserService userService;

    @RequestMapping("/getAllUser")
    public List<User> getAllUser()throws Exception{
        return new UserServiceProxy(userService).getAllUser();
    }
}

然后启动项目,调用一下接口,就可以看到控制台打印如下日志:

/*
记录日志:执行getAllUser()方法前
[User{id=1, name='大司马', age=36, job='厨师'}, User{id=2, name='朴老师', age=36, job='主播'}, User{id=3, name='王刚', age=30, job='厨师'}, User{id=4, name='大sao', age=32, job='美食up主'}, User{id=5, name='姚大秋', age=35, job='主持人'}]
记录日志:执行getAllUser()方法后
*/

这就是静态代理的实现思路,很简单。但是一般我们肯定是不用这种方式。因为这种方式太笨了,很容易就可以看出几个缺点。

1.要实现接口,也就是目标的方法要定义一个接口方法,实际上是运用了java多态的特性

2.第一点还不是致命的,因为JDK动态代理也是必须要定义接口;致命的是每一个你想代理的接口你都要去创建一个代理类去实现,假设有很多要代理的接口,那就创建很多代理类,这样显得很臃肿

假设还是不理解为什么要动态代理,不妨我们再多加一个支付接口PayService,这个支付接口我们也要加上日志记录。

用静态代理怎么做?很简单呀,再创建一个PayServiceProxy类不就完了吗,如果还有OrderService(订单),

WarehouseService(仓库)等等。那就要创建很多XXXServiceProxy类。如果使用动态代理,就没必要创建这么多代理类,创建一个代理类就够了!

动态代理就是为了解决静态代理的这个缺点产生的。

JDK动态代理

JDK本身就带有动态代理,必须要满足一个条件,就是要有接口。原理其实和静态代理是一样的,也是用代理类去实现接口,但是代理类不是一开始就写好的,而是在程序运行时通过反射创建字节码文件然后加载到JVM。也就是动态生成的代理类对象。

下面就是用JDK动态代理实现代理模式。

public class LogRecordProxy<T> implements InvocationHandler {

    private T target;

    public LogRecordProxy(T t) {
        this.target = t;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("记录日志:执行" + method.getName() + "方法前");
        Object result = method.invoke(target, args);
        System.out.println(result);
        System.out.println("记录日志:执行" + method.getName() + "方法后");
        return result;
    }
    
    /**
     * 获取代理对象的方法
     * */
    @SuppressWarnings("unchecked")
    public <T> T getProxy() throws Exception {
        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
}

在controller层,就要改成这样。

@RestController
@RequestMapping("/xiaoniu")
public class UserController {

    @Resource
    private UserService userService;

    @RequestMapping("/getAllUser")
    public List<User> getAllUser() throws Exception {
        //获取代理对象
        UserService userServiceProxy = new LogRecordProxy<>(userService).getProxy();
        return userServiceProxy.getAllUser();
    }
}

假设有一个PayService也要做日志记录,就可以直接使用。

@Resource(name = "payService")
    private PayService payService;	

	@RequestMapping("/pay")
    public String pay(@RequestParam(name = "channel") String channel,
                      @RequestParam(name = "amount") String amount
    )throws Exception{
        //获取代理对象,实际上就在构造器上改一下传入的参数即可
        PayService payServiceProxy = new LogRecordProxy<>(payService).getProxy();
        return payServiceProxy.pay(channel,amount);
    }

很多文章给的例子都不带泛型,也可以,就是获取的代理对象需要强转一下,强转成对应的接口类。

注意:这里一定要用接口接收代理对象,不能用实现类!

因为返回的对象已经不是实现类的对象,而是和实现类有共同的接口类的代理类对象,所以当然只能用接口类去接收。

这也是为什么一再强调要面向接口编程的原因,因为面向接口编程可以做更多的扩展。假设是面向实现类去编程,那就不能用JDK动态代理去扩展了!

CGLB动态代理

那如果有些场景真的没有接口呢,我们怎么运用代理模式?

首先引入maven配置

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>

然后创建一个方法拦截器LogRecordInterceptor,要实现MethodInterceptor类,如下:

public class LogRecordInterceptor implements MethodInterceptor {

    private Object target;

    public LogRecordInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("记录日志:执行" + method.getName() + "方法前,参数:" + Arrays.toString(args));
        Object result = method.invoke(target, args);
        System.out.println(result);
        System.out.println("记录日志:执行" + method.getName() + "方法后,参数:" + Arrays.toString(args));
        return result;
    }
}

然后再创建一个工厂类InterceptorFactory,用于创建代理对象。

public class InterceptorFactory {

    @SuppressWarnings("unchecked")
    public static <T> T getInterceptor(Class<T> clazz, MethodInterceptor methodInterceptor) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(methodInterceptor);
        return (T) enhancer.create();
    }
}

接着我们就可以创建一个没有接口的类,我这里就创建一个数学工具类进行测试

public class MathUtil {
    /**
     * 获取一个数的平方
     * */
    public String getSquare(int num) {
        return String.valueOf(num * num);
    }
}

然后在controller层定义一个接口来测试

@RequestMapping("/getSquare")
    public String getSquare(@RequestParam(name = "num") Integer num) throws Exception {
        MathUtil mathUtil = InterceptorFactory.getInterceptor(MathUtil.class, new LogRecordInterceptor(new MathUtil()));
        return mathUtil.getSquare(num);
    }

用浏览器或者POSTMAN工具调用接口,就可以在控制台看到以下输出:

/*
记录日志:执行getSquare方法前,参数:[2]
4
记录日志:执行getSquare方法后,参数:[2]
*/

这样就实现没有定义接口也可以实现动态代理!

实际上,定义接口的也可以用这种方法来进行扩展,比如上面的userService接口

@RestController
@RequestMapping("/xiaoniu")
public class UserController {

    @Resource
    private UserService userService;

    @RequestMapping("/getAllUser")
    public List<User> getAllUser() throws Exception {
        UserServiceImpl userServiceProxy = InterceptorFactory
            .getInterceptor(UserServiceImpl.class,
                            new LogRecordInterceptor(userService));
        return userServiceProxy.getAllUser();
    }
}

调用接口我们在控制台也是可以看到以下输出日志:

/*
记录日志:执行getAllUser方法前,参数:[]
[User{id=1, name='大司马', age=36, job='厨师'}, User{id=2, name='朴老师', age=36, job='主播'}, User{id=3, name='王刚', age=30, job='厨师'}, User{id=4, name='大sao', age=32, job='美食up主'}, User{id=5, name='姚大秋', age=35, job='主持人'}]
记录日志:执行getAllUser方法后,参数:[]
*/

总结

以上就是代理模式的一些通俗的解释,还有三种实现的方式的学习

多说几句,我们都知道Spring框架有两个核心技术,一个叫控制反转IOC,另一个叫切面编程AOP。切面编程大家都很熟悉,用的就是代理模式,那么AOP实现的代理模式用的是JDK动态代理还是CLB动态代理

答曰:两个都用!