代理模式--在工作中的实际应用

770 阅读4分钟

代理模式,即在不改变原有功能的基础上,通过增加一个代理类,为原有功能增加新的功能。代理模式侧重点在于解耦,通过对原始功能进行代理从而附加一些额外的功能。类似游戏中的装备附魔,比如一把剑的功能只有砍怪,但是可以通过附魔,让这把剑带着火焰去砍怪,这把带火焰的剑相对于原有的剑就增加了一个额外的火焰功能,后续还可以对之增加光晕、特效等功能。这里要与一个相似的设计模式-装饰器设计模式做一个区分,装饰器设计模式简单理解就是增强原有方法。比如还是这把剑,可以通过锻造将其打造为轻剑、重剑,轻剑速度快,重剑伤害高,这就是对原有的剑的功能--砍怪,做了增强。所以简单说:代理模式是增加功能,装饰器模式是增强功能。

1、背景:

在线上的服务中,为提升任务的处理速度,所以使用了异步线程池进行处理。同时,为了追踪执行的整个过程,使用MDC在日志中增加一个trace_id做任务追踪。这样就导致了一个问题,在主线程中添加了一个trace_id,在异步处理时新建的线程就无法获得主线程中的trace_id,就会导致追踪中断。为此,需要在子线程执行运行之前将主线程中的trace_id获取到并传递到子线程中去,从而达到想要的效果。所以,需要对Runnable接口和Callable接口都做处理,由于增加trace_id相对于整个任务来讲是个新增的功能,负责链路追踪,所以使用代理模式去处理。同时,如果使用静态代理方式,那么需要为Runnable接口和Callable接口都增加一个代理类,而且所做的处理都是相同的,即在执行任务前获取主线程的trace_id,执行完任务后清除MDC,防止内存泄露,导致两个代理类变得很相似,增加了维护的个数,也使得代码变得不够优雅。所以使用JDK动态代理,为Runnable接口和Callable接口动态创建代理对象。

为此,定义一个动态代理类TraceTaskProxy,用来生成Runnable接口和Callable接口的代理对象,在执行子线程任务时,使用代理对象去执行。

2、代码:

public class TraceTaskProxy {
    @SuppressWarnings("unchecked")
    public static <T> T createProxy(T target) {
        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new MDCHandler<>(target));
    }

    private static class MDCHandler<T> implements InvocationHandler {

        private final T t;

        public MDCHandler(T t) {
            this.t = t;
        }

        Map<String, String> previous = MDC.getCopyOfContextMap();

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                // 主线程需要在MDC中设置一个traca_id 否则子线程也不会获取到
                if (previous != null) {
                    MDC.setContextMap(previous);
                }
                return method.invoke(t, args);
            } finally {
                MDC.clear();
            }
        }
    }
}

3、具体使用:

// 异步线程池配置
public class AsyncConfig implements AsyncConfigurer {
    @Override
    @Bean(name = "defaultExecutor")
    public Executor getAsyncExecutor() {
        return initExecutor("defaultExecutor", 10);
    }

    private DecoratedExecutorService initExecutor(String executorServiceName, int maxPoolSizeAvailableProcessors) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutorMdcUtil();
        // 核心线程池数量,方法: 返回可用处理器的Java虚拟机的数量。
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        // 最大线程数量
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * maxPoolSizeAvailableProcessors);
        // 线程池的队列容量
        executor.setQueueCapacity(Runtime.getRuntime().availableProcessors() * 2);
        // 线程名称的前缀
        executor.setThreadNamePrefix(executorServiceName + "-");
        // setRejectedExecutionHandler:当pool已经达到max size的时候,如何处理新任务
        // CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
		// ExecutorServiceMetrics 是包含指标搜集功能的ExecutorService,属于第三方功能
        ExecutorService monitorExecutorService = ExecutorServiceMetrics.monitor(
                Metrics.globalRegistry, executor.getThreadPoolExecutor(), executorServiceName);
        return new DecoratedExecutorService(monitorExecutorService);
    }
	// 省略其它需要重写的方法
}
// 对ExecutorServiceMetrics进行包装,让它使用功能更强的Runnable和Callable对象
public class DecoratedExecutorService implements ExecutorService {

    private final ExecutorService delegate;

    public DecoratedExecutorService(ExecutorService executorService) {
        delegate = executorService;
    }
	
    //...省略其它需要重写的方法
	
    @Override
    public <T> Future<T> submit(Callable<T> task) {
		// 使用TraceTaskProxy创建的代理对象添加将trace_id传递到子线程的功能,又不影响任务的正常执行
        return delegate.submit(TraceTaskProxy.createProxy(task));
    }

    @Override
    public Future<?> submit(Runnable task) {
		// 使用TraceTaskProxy创建的代理对象添加将trace_id传递到子线程的功能,又不影响任务的正常执行
        return delegate.submit(TraceTaskProxy.createProxy(task));
    }

}

4、总结:

代理模式侧重点是解耦,同时也能起到代码复用的效果(动态代理),常在为核心业务增加新的功能需求的场景下使用,比如增加指标搜集,日志监控等上,但也不限于此,它的主要目的是解耦。 同时代理模式有别于装饰者模式,一个是增加,一个是增强。