从头开始学Java架构之设计模式4:代理模式

代理模式-指为其他对象提供一种代理,以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介作用,属于结构型设计模式。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。

简而言之代理模式就是对目标对象做个封装,以起到隐藏目标对象(保护目标对象),和在目标对象进行的逻辑处理之外,增加一些逻辑处理的功能(增强目标对象)。

主要分为静态代理和动态代理!其中,动态代理又有JDKProxy和CGLib两种主要的动态代理方式

静态代理实例--

显式声明被代理对象

当今社会,年轻人一般都没有时间去找对象,那么心急的父母就会作为孩子的代理,去帮孩子找对象,在这里,孩子就是被代理对象。

首先创建一个Person接口,里面定义了一个findLove方法:

public interface Person {
    public void findLove();

}
知识兔

再创建一个Son类,它去 实现Person接口:

public class Son implements Person{
    
    @Override
    public void findLove(){
        System.out.println("儿子要求:肤白、貌美大长腿");
    }
    
}
知识兔

再创建一个Father类,在它里面显式声明了被代理对象(即Son类的对象),并显式的定义了一个带参构造函数,然后也定义一个findLove方法,并在里面调用Son的findLove方法:

public class Father{
    
    //显示声明被代理对象
    private Person person;
    
    public Father(Person person){
        this.person = person;
    }
    
    public void findLove(){
        System.out.println("父亲物色对象");
        this.person.findLove();
        System.out.println("双方父母同意,确立关系");
    }

}
知识兔

来看测试类FatherProxyTest:

public class FatherProxyTest {
    
    public static void main(String[] args) {
        //只能帮儿子找对象,这就是静态代理
        Father father = new Father(new Son());
        father.findLove();
    }

}
知识兔

在测试类里面,通过Father类的构造函数,得到了一个Son类的对象,并且执行了Son类里的findLove方法,这就是静态代理模式的一个简单例子。

再看一个实际的业务场景,在分布式业务场景中,我们通常会对数据库进行分库分表,分库分表之后使用Java操作时,就可能需要配置多个数据源,我们可以实现根据订单创建时间来动态切换数据源(不是动态代理)。

首先来创建Order订单实体:

//创建order订单实体
public class Order {
    private Object orderinfo;
    //订单创建时间按年分库
    private long createTime;
    private String id;
    
    public Object getOrderinfo() {
        return orderinfo;
    }
    public void setOrderinfo(Object orderinfo) {
        this.orderinfo = orderinfo;
    }
    
    public long getCreateTime() {
        return createTime;
    }
    public void setCreateTime(long createTime) {
        this.createTime = createTime;
    }
    
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }

}
知识兔

创建OrderDao持久层操作,里面就是一些逻辑操作:

//创建OrderDao持久层操作
public class OrderDao {
    public int insert(Order order){
        System.out.println("OrderDao创建order成功");
        return 1;
    }

}
知识兔

创建IOrderService接口:

//创建IOrderService接口
public interface IOrderService {
    int createOrder(Order order);

}
知识兔

创建OrderService实现类:

public class OrderService implements IOrderService {
//静态代理的标志,显式声明被代理对象
private OrderDao orderDao; public OrderService(){ //如果使用spring应该是自动注入的 //我们为了使用方便,在构造方法中直接将OrderDao初始化了 orderDao = new OrderDao(); } @Override public int createOrder(Order order) { System.out.println("OrderService调用orderDao创建订单"); // TODO Auto-generated method stub return orderDao.insert(order); } }
知识兔

然后使用静态代理,根据订单创建时间自动按年进行分库。

先创建数据源路由对象,使用ThreadLocal单例实现,DynamicDataSourceEntry类:

//动态切换数据源
public class DynamicDataSourceEntity {
    
    //默认数据源
    public final static String DEFAULE_SOURCE = null;
    
    private final static ThreadLocal<String> local = new ThreadLocal<String>();
    
    //构造方法私有化,即单例模式
    private DynamicDataSourceEntity(){}
    
    //清空数据源
    public static void clear(){
        local.remove();
    }
    
    //获取当前正在使用的数据源名字
    public static String get(){
        return local.get();
    }
    
    //还原当前切面的数据源
    public static void restore(){
        local.set(DEFAULE_SOURCE);
    }
    
    //DB_2018
    //DB_2019
    //设置已知名字的数据源
    public static void set(String source){
        local.set(source);
    }
    
    //根据名字动态设置数据源
    public static void set(int year){
        local.set("DB_" + year);
    }

}
知识兔

创建切换数据源的代理OrderServiceStaicProxy类,这里的被代理类就是IOrderService接口:

//代理数据源切换的逻辑
public class OrderServiceStaticProxy implements IOrderService {
    private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
    
    //因为只能代理OrderService的对象,所以是静态代理。
    //静态代理要求你的对象已经创建好了
    private IOrderService orderService;
    
    public OrderServiceStaticProxy(IOrderService orderService){
        this.orderService = orderService;
    }
    
    public int createOrder(Order order){
        Long time = order.getCreateTime();
        Integer dbRouter = Integer.valueOf(yearFormat.format(new Date(time)));
        System.out.println("静态代理类自动分配到【DB_" +  dbRouter + "】数据源处理数据");
        DynamicDataSourceEntity.set(dbRouter);
        
        this.orderService.createOrder(order);
        DynamicDataSourceEntity.restore();
        
        return 0;
    }

}
知识兔

测试类,这里OrderServiceStaticProxy代理类的参数是OrderService类的对象,因为接口的对象只能通过其实现类实现!:

public class DbRouteProxyTest {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try{
            Order order = new Order();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
            Date date = sdf.parse("2017/02/01");
            order.setCreateTime(date.getTime());
            
            //接口的对象是只能通过其实现类实现,其对象调用的方法也是其实现类的方法。
            //通过静态代理分配数据源。
            IOrderService orderService = new OrderServiceStaticProxy(new OrderService());
            orderService.createOrder(order);
        }catch(Exception e){
            e.printStackTrace();
        }

    }

}
知识兔

动态代理示例--

动态代理和静态代理基本思路一致,但是动态代理功能更强大,随着业务的扩展适应性更强。即动态代理把代理对象的限制取消,可以代理任何对象!

还以找对象为例,静态代理是父母给儿子找对象,而且父母只会给自己的儿子找对象,但是动态代理把找对象发展成一个产业,相当于媒婆、婚介所之类的,可以给任何有需求的人找对象。

动态配置和替换被代理对象。

动态代理主要有两种流行的实现方式:JDK动态代理和CGLib动态代理

先看动态代理的例子

首先,创建一个Girl类,也让它去实现Person接口:

//对于被代理对象来说,它也必须要有一个接口!
public class Girl implements Person {

    @Override
    public void findLove() {
        // TODO Auto-generated method stub
        System.out.println("高富帅");
        System.out.println("身高180厘米");
        System.out.println("有6块腹肌");

    }

}
知识兔

为什么要实现这个接口?其实静态代理是不必实现这个接口的,但是如果使用JDK动态代理的话,就必须要实现一个接口!原因会在下面讲。

再创建一个JDKMeipo类,它作为代理类去代理Girl对象:

//必须实现InvocationHandler接口
public class JDKMeipo implements InvocationHandler {

    //要持有被代理对象的引用
    private Object target;
    //提供一个注入的方法
    public Object getInstance(Object target) throws Exception{
        this.target = target;
        //把类保存下来
        Class<?> clazz = target.getClass();
        //jdk的写法
        return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // TODO Auto-generated method stub
        before();
        //用到了java反射
        Object obj = method.invoke(this.target,args);
        after();
        return obj;
    }
    
    private void before(){
        System.out.println("我是媒婆,我要给你找对象,现在已经确认你的需求");
        System.out.println("开始物色");
    }
    
    private void after(){
        System.out.println("OK的话,准备办事");
    }

}
知识兔

可以看出,要使用JDK动态代理的话,代理类必须实现InvocationHandler接口!并且要提供一个注入的方法getInstance方法,getInstance是不通过构造函数获取对象的一种手段。这个方法里面的代码基本上是一样的,里面通过泛型调用了getInterfaces方法,这里和为什么被代理类一定要实现一个接口有关系。

还重写了Invoke方法,可以看出JDK动态代理是通过Java反射实现的,基本上也是一样的写法。

还实现了before方法和after方法,这两个方法是用来实现目标对象增强的。就是在调用目标对象的逻辑前后执行的逻辑。

调用方法:

public class JDKProxyTest {
    public static void main(String[] args) {
        try{
            //在动态代理中,媒婆可以为任何人找对象
            //这个obj已经是JDK自动生成的类的对象了
            Object obj = new JDKMeipo().getInstance(new Girl());
            
            Method method = obj.getClass().getMethod("findLove", null);
            method.invoke(obj);
            
            //这段代码是输出$Proxy0.class文件
            byte [] bytes = ProxyGenerator.generateProxyClass("$Proxy0",new Class[]{Person.class});
            FileOutputStream os = new FileOutputStream("E://$Proxy0.class");
            os.write(bytes);
            os.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }

}
知识兔

在这段代码中,可以看到不再需要显式声明被代理对象了,直接通过JDKMeipo的getInstance方法,传入被代理Girl类的对象,通过反射去执行findLove方法。但是这里有个疑问,如果仅是如此的话,那么之前的before和after方法就不会被执行,但是事实上,这两个方法的逻辑都被执行了!

可以通过debug啥的看出调用JDKMeipo得到的obj对象并不是某个已知类的对象,反而是一个叫做$Proxy0的类的对象,我们可以通过上面的一段代码去把这个类的class文件输出出来,反编译后可以看到这个类里面也有个findLove方法,事实上,obj其实是这个 类的对象,这就引出了JDKProxy的实现原理——

JDK动态代理,是利用动态生成字节码的原理,在代理中生成它($Proxy0)的源代码,然后编译成class文件,再通过classloader加载它,然后执行,也就是说,Java反射调用的类完全是一个新的类,里面调用了JDKMeipo类里重写的invoke方法,所以before和after方法才会被执行到!

这也是为什么被代理类要实现一个接口,因为需要根据这个接口找到这个接口里 方法,实现并覆盖到被代理类的方法里,并且新生成的方法是不可再次被重写的(final类型的),被代理的对象也不能被二次代理了,所以要把需要代理的方法写到接口里,才能被扫描到!

现在可以用动态代理去实现动态分配数据源了。

创建动态代理的类OrderServiceDynamicProxy:

public class OrderServiceDynamicProxy implements InvocationHandler{
    
    private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
    Object proxyobj;
    //动态代理的对象可以是任何对象了
    public Object getInstance(Object proxyobj){
        
        this.proxyobj = proxyobj;
        Class<?> clazz = proxyobj.getClass();
        return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
    }
    
    //args是参数列表,可以获取被代理对象的参数
    public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{
        before(args[0]);
        Object object = method.invoke(proxyobj, args);
        after();
        return null;
    }
    
    private void before(Object target){
        try{
            //进行数据源的切换
            System.out.println("Proxy before method");
            
            //约定优于配置
            //也就是说接口和实现类必须要有"getCreateTime"方法
            Long time = (Long) target.getClass().getMethod("getCreateTime").invoke(target);
            Integer dbRouter = Integer.valueOf(yearFormat.format(new Date(time)));
            System.out.println("静态代理类自动分配到【DB_" + dbRouter + "】数据源处理数据");
            DynamicDataSourceEntity.set(dbRouter);
        }catch(Exception e){
            e.printStackTrace();
        }
        
    }
    
    private void after(){
        System.out.println("Proxy after method");
        //还原成默认的数据源
        DynamicDataSourceEntity.restore();
    }

}
知识兔

测试代码:

public class DbRouteDynamicProxyTest {
    
    public static void main(String[] args) {
        try{
            Order order = new Order();
            
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy/mm/dd");
            Date date = sdf.parse("2017/02/01");
            order.setCreateTime(date.getTime());
            
            IOrderService orderService = (IOrderService) new OrderServiceDynamicProxy().getInstance(new OrderService());
            orderService.createOrder(order);
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
知识兔

然后再看看CGLib动态代理的使用--

新建一个Customer类:

public class Customer {
    
    public void findLove(){
        System.out.println("儿子要求:肤白貌美大长腿");
    }

}
知识兔

创建CglibMeipo类:

public class CglibMeipo implements MethodInterceptor {
    
    //把代理的引用传进来
    //为什么要传Class?是因为要通过动态生成源代码去继承传来的这个Class
    public Object getInstance(Class<?> clazz) throws Exception{
        
        //相当于Proxy,代理的工具类
        Enhancer enhancer = new Enhancer();
        
        //设置父类就是传来的这个clazz
        enhancer.setSuperclass(clazz);
        
        //回调函数,回调下面的intercept方法
        enhancer.setCallback(this);
        
        return enhancer.create();
        
    }

    @Override
    //在这个方法里实现代理的增强
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // TODO Auto-generated method stub
        before();
        //生成的类是子类,目的是实现父类的的一些方法
        //在子类中实现父类的增强,调用父类的逻辑
        Object obj = methodProxy.invokeSuper(o, objects);
        after();
        return obj;
    }
    
    private void before(){
        System.out.println("我是媒婆,我要给你找对象,现在已经确认你的需求");
        System.out.println("开始物色");
    }

    private void after(){
        System.out.println("OK的话,准备办事");
    }

}
知识兔

测试类:

public class CglibTest {
    public static void main(String[] args) {
        try{
            //JDK是采用读取接口的信息
            //CGlib是覆盖父类方法
            //目的:都是生成一个新的类,去实现增强代码逻辑的功能
            
            //JDK Proxy对于用户而言,必须要有一个接口实现,目标类相对来说复杂
            //CGlib可以代理任意一个普通的类,没有任何的要求
            
            //CGlib生成代理逻辑更复杂,调用效率更高,生成一个包含了所有逻辑的FastClass,不再需要反射调用
            //JDK Proxy生成代理的逻辑简单,执行效率相对要低,每次都要反射动态调用
            
            //CGlib有个坑,CGlib不能代理final的方法
            
            System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"E://cglib_proxy_classes");
            Customer obj = (Customer) new CglibMeipo().getInstance(Customer.class);
            obj.findLove();
        }catch(Exception e){
            e.printStackTrace();
        }
    }

}
知识兔

可以看出,CGLib代理的对象不需要实现任何接口,它是通过动态继承目标对象实现的动态代理,也是自动生成新的类,不过是会生成三个。

通过上面的System.setProperty方法,可以把新生成的类输出到磁盘上。

但是CGLib不能代理final修饰的方法!因为final类型的方法不能被重写了!

动态代理总结--

可以利用动态代理做日志监听和AOP切面编程。

JDK动态代理的实现原理:

1.拿到被代理类的引用,并且获取它所有的接口(反射获取)。

2.JDK Proxy重新生成一个新的类,实现了被代理类所有接口的方法。

3.动态生成Java代码,把增强逻辑加入到新生代码中。

4.编译生成新的Java代码的class文件。

5.加载并重新运行新的class,得到的类就是全新类。

CGLib和JDK动态代理对比:

1.JDK动态代理是实现了被代理对象的接口,CGLib是继承了被代理对象。

2.JDK和CGLib都是在运行期生成字节码,JDK是直接写Class字节码,CGLib是使用ASM框架写Class字节码,CGLib代理实现更复杂,生成代理类比JDK效率低,

3.JDK调用代理方法,是通过反射机制调用,CGLib是通过FastClass机制直接调用方法,CGLib执行效率更高。

代理模式的优点:

代理模式能将代理对象和真实被调用的对象分离,一定程度上降低了系统的耦合性,易于扩展。可以起到保护目标对象的作用。可以增强目标对象的职责。

代理模式的缺点:

会造成系统中类的数目增多。在客户端和目标对象之间增加了一个代理对象,会造成请求处理速度变慢。增加了系统的复杂度。

Spring中的代理选择原则:

1.当bean有实现接口时,Spring就会用JDK的动态代理。

2.当bean没有实现接口时,Spring选择CGLib。

3.Spring可以通过配置强制使用CGLib,只需要在Spring的配置文件中加入如下代码——<aop:aspectj-autoproxy proxy-target-class="true"/>

计算机