何为代理设计模式

代理设计模式就是提供一个替身(代理者)或者占位符来控制对这个对象的访问。 使用代理模式创建代表对象。代表对象控制这另一个对象的访问,被代表的对象可以是远程的对象、创建开销大的对象或需要安全控制的对象。

代理模式的类图如下。

代理模式是在生活中特别常见,比如玩的跑跑卡丁车国服版本是由世纪天成公司代理的,世纪天成可以在原有跑跑卡丁车的基础上新加一些属于自己的功能,比如未成年人游戏时长限制控制、游戏内部的广告牌打广告、还有游戏中的默认车牌都显示“世纪天成”字样等等。

实例

我们就以跑跑卡丁车国服游戏的例子来举例,同时结合 JDK 自带的动态代理。JDK 自带的动态代理必须要一个接口和实现需要代理的类,需要首先要一个 KartRider 接口,也就是原始的韩服跑跑卡丁车。

public interface KartRider {

    void setAd(String ad);

    void setCarId(String carId);

    void play();

}

世纪天成代理的跑跑卡丁车国服,“跑跑卡丁车国服”就是需要代理的类。

public class ChineseKartRider implements KartRider {

    private String ad;

    private String carId;

    private Integer age;

    @Override
    public void setAd(String ad) {
        this.ad = ad;
    }

    @Override
    public void setCarId(String carId) {
        this.carId = carId;
    }

    @Override
    public void play() {
        System.out.println(this);
        System.out.println("游戏已启动!");
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "ChineseKartRider{" +
                "ad='" + ad + '\'' +
                ", carId='" + carId + '\'' +
                ", age=" + age +
                '}';
    }
}

创建一个代理类(“也就是世纪天成了”)并实现 InvocationHandler 接口,其中 Invoke 方法就是用于控制创建的 ChineseKartRider 对象实例的方法访问,实现未成年人游戏时长限制控制。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;

public class TianCityInvocationHandler implements InvocationHandler {

    private KartRider kartRider;

    public TianCityInvocationHandler(KartRider kartRider) {
        this.kartRider = kartRider;
    }

    /**
     *
     * @param proxy 调用该方法的代理实例
     *
     * @param method 调用其 getName 方法就知道是哪个方法名在使用
     *
     * @param args 调用的方法的参数个数
     *
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException {
        LocalDateTime date = LocalDateTime.now();
        int week = date.getDayOfWeek().getValue();
        int hour = date.getHour();

        try {
            if("play".equals(method.getName())) {

                ChineseKartRider p = (ChineseKartRider) kartRider;

                boolean req1 = week == 5 || week == 6 || week == 0; // 未成年人只能在这三天玩游戏
                boolean req2 = p.getAge() == null || p.getAge() < 18; // 年龄规则
                boolean req3 = hour == 20; // 1小时时长限制

                // 未成年限制
                if (req1 && req2 && req3) {
                    return method.invoke(kartRider, args);
                }
                // 年龄过 18 岁不受此限制
                if (!req2) {
                    return method.invoke(kartRider, args);
                }

                throw new IllegalAccessException("通知要求,严格限制向未成年人提供网络游戏服务的时间, 所有网络游戏企业仅可在周五、周六、周日和法定节假日每日20时至21时向未成年人提供1小时服务!");

            } else if (method.getName().startsWith("set")) {
                return method.invoke(kartRider, args);
            }
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }

}

运行类(18岁以上不受限制,18岁以下在规定的时间就会提示)。

public class ProxyTest {

    public static void main(String[] args) {
        KartRider proxy = getProxy();
        proxy.setAd("跑跑卡丁车 - 突破");
        proxy.setCarId("世纪天成");
        proxy.play();


    }

    public static KartRider getProxy() {

        ChineseKartRider kartRider = new ChineseKartRider();
        kartRider.setAge(18);

        return (KartRider) Proxy.newProxyInstance(
                kartRider.getClass().getClassLoader(),
                kartRider.getClass().getInterfaces(),
                new TianCityInvocationHandler(kartRider));
    }

}

运行结果(18岁不受限制):

ChineseKartRider{ad='跑跑卡丁车 - 突破', carId='世纪天成', age=18}
游戏已启动!

把年龄设置为 10 岁,抛出了异常。

Exception in thread "main" java.lang.reflect.UndeclaredThrowableException
	at com.sun.proxy.$Proxy0.play(Unknown Source)
	at com.example.demo_springboot.designpattern.proxy.ProxyTest.main(ProxyTest.java:11)
Caused by: java.lang.IllegalAccessException: 通知要求,严格限制向未成年人提供网络游戏服务的时间, 所有网络游戏企业仅可在周五、周六、周日和法定节假日每日20时至21时向未成年人提供1小时服务!
	at com.example.demo_springboot.designpattern.proxy.ChineseInvocationHandler.invoke(ChineseInvocationHandler.java:51)
	... 2 more

随处可见的代理模式

除了本例,Java RMI、Spring AOP、Spring Transaction Manager 等等也涉及到代理设计模式,面向切面编程的本质就是代理模式。

并且知道代理模式之后,也就明白为什么在被代理的对象(ChineseKartRider)内部调用方法时不受控制,若不确定该对象是被代理过的,可以通过 Proxy.isProxyClass() 方法来判断。