深入理解Spring源码(二):探讨Spring框架中应用的设计模式
往期回顾:
-
细读Spring源码(一)---refresh()方法概览
声明:本文中源码使用的是Spring5.3版本,就是源码中的master分支!!!
一般来说,设计模式分为创建型、结构性和行为性,具体的有以下23种,其中的简单工厂模式其实不算真正意义上的设计模式:
我在看Spring源码的过程中,梳理出了如下几种设计模式:
下面详细介绍每一种设计模式是如何在Spring中使用的,这些知识点不算是阅读源码的前置知识点,但是Spring的源码也不是读一两遍就能完全掌握的,我就是一个方法一个方法地通读了一遍之后,因为读到了很多不理解的写法,然后通过查阅资料才发现是用到了这些设计模式,所以整个下来感受就是:知道这些设计模式,对于通读之后的精读,掌握Springq其他组件,比如SpringBoot等,甚至是在开发过程中对Spring进行扩展,都算是一个前置条件,而且知道了这些,读起源码来,才能自顶向下有一个全局的认识。废话不多说,直接开写吧!
一、创建性模式
在Spring中单例和多例都是用来创建bean的时候使用,现来看一下创建bean的代码:
1.1 单例模式
单例模式详解传送门:http://c.biancheng.net/view/1338.html
Spring中的应用:创建Bean
说明:单例模式就是Spring中定义Bean的时候,可以指定scope,如果是Singleton,就会创建在Spring容器内创建一个全局唯一的单例bean,但需要注意的是,spring中的单例跟设计模式中的单例还是有区别的,在设计模式中,相同的类加载器只能创建一个实例,但是在spring中,是通过bean的id来校验唯一性,就是说,同一个类可以创建多个id不同的实例,spring中创建单例的源代码如下:
1 // Create bean instance. 2 if (mbd.isSingleton()) { 3 //单例模式:创建单例bean 4 sharedInstance = getSingleton(beanName, () -> { 5 try { 6 return createBean(beanName, mbd, args); 7 } catch (BeansException ex) { 8 /*Explicitly remove instance from singleton cache: It might have been put there 9 eagerly by the creation process, to allow for circular reference resolution. 10 Also remove any beans that received a temporary reference to the bean. 11 1、需要从单例缓存中显示地移除:因为为了解决循环引用问题可以在早期创建程序中就已经设置到缓存中了 12 2、也要删除任何指向该bean的临时引用*/ 13 destroySingleton(beanName); 14 throw ex; 15 } 16 }); 17 bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); 18 }
上面的第4~7行就是创建一个单例bean的过程,先看getSingleton方法:
其中的singletonObjects的源码如下:
/** * Cache of singleton objects: bean name to bean instance. * 缓存单例对象:beanName->beanInstance */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
在一个CurrentHashMap中存储beanName到beanInstance的映射关系,其实这就是传说中的一级缓存,从代码可以看出,Spring在创建一个单例bean的时候,会先通过beanName从一级缓存中获取,当获取不到是才会去调用回调函数createBean进行实例创建,在createBean中调用doCreateBean,在doCreateBean中调用instantiateBean方法,而instantiateBean具体创建bean的过程是通过策略模式实现的,这个在策略模式中讲,但在这里就能感觉到源码的魅力了。
1.2 原型模式
原型模式详解传送门:http://c.biancheng.net/view/1343.html
Spring中的应用:创建Bean
说明:同单例模式,如果指定bean的scope为Prototype,就会创建多例bean,即在每次获取时都会创建一个bean对象,Spring中创建多例bean的原源代码如下:
1 if (mbd.isPrototype()) { 2 //多例模式:创建多例bean 3 Object prototypeInstance = null; 4 try { 5 beforePrototypeCreation(beanName); 6 prototypeInstance = createBean(beanName, mbd, args); 7 } finally { 8 afterPrototypeCreation(beanName); 9 } 10 bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); 11 }
可以看到和单例模式不同的地方在于,在调用createBean之前,没有调用getSingleton方法从一级缓存中获取,就是说每次获取都是创建一个新的。下面用一个例子来验证单例和多例:
1、在spring.xml中定义两个bean,一个为单例,一个为多例
1 <bean id="face1" class="com.spring.reading.vo.Face" init-method="init" destroy-method="destroy" scope="singleton"> 2 <property name="eye" value="黑眼睛1"/> 3 </bean> 4 <bean id="face2" class="com.spring.reading.vo.Face" init-method="init" destroy-method="destroy" scope="prototype"> 5 <property name="eye" value="黑眼睛2"/> 6 </bean>
上面第1行定义了一个名称为face1的bean,它的scope为singleton,第4行定义了一个名称为face2的bean,它的scope为prototype
2、获取两个face1和两个face2,并比较两个对象是否相等
1 public class OpenTheDoor { 2 3 public static void main(String[] args) { 4 ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring-lifecycle.xml"); 5 System.out.println("---获取单例Bean---"); 6 Face face11 = (Face) context.getBean("face1"); 7 Face face12 = (Face) context.getBean("face1"); 8 System.out.println("face11=face12:" + (face11 == face12)); 9 System.out.println("face11=face12:" + (face11.equals(face12))); 10 System.out.println("---获取多例Bean---"); 11 Face face21 = (Face) context.getBean("face2"); 12 Face face22 = (Face) context.getBean("face2"); 13 System.out.println("face21=face22:" + (face21 == face22)); 14 System.out.println("face11=face12:" + (face21.equals(face22))); 15 context.close(); 16 } 17 }
上面的第6、7行两次获取名称为face1的Face对象,第11、12行两次获取名称为face2的Face对象,并对两次获取的结果进行比较,同时使用了==和equals方法,因为没有重写equals方法,所以比较的还是对象在内存中的地址。
3、查看比较结果
可以看出同一个类的不同bean,定义成单例时,每次获取的都是同一个对象,定义成单例时,每次获取都是不同的对象,其实这里就能看出Spring中的单例跟设计模式中的单例其实是有区别的,spring中同一个类可以创建多个对象,通过id区分不同实例,通过一个beanName多次获取对象时,每次都能取到同一个,而在单例模式中,同一个类全局只能创建一个实例对象,虽然都叫单例,但Spring中的单例其实并不是真正意义上的单例模式。
总结:
1、Spring创建Bean时默认是单例模式,且非懒加载,即在容器启动过程中就完成创建
2、对于同一个类,可以创建不同的实例,每个实例是不相同的,只是根据同一个beanName多次获取时,得到的实例都是同一个;
3、单例模式中的单例是针对同一个类,全局只能创建一个唯一的实例。
以上就是对Spring中单例模式和多例模式的分析过程。
1.3 工厂模式
工厂模式详解传送门:http://c.biancheng.net/view/1351.html
spring中的工厂模式有两种,一种是工厂方法模式,一种是抽象工厂模式,它们二者的区别在于前者是具体工厂生产具体的产品,而后者是一个工厂中可以生产不同类型的产品,Spring中主要涉及的类和接口如下:
可以看到两者,一个是BeanFactory,一个是BeanFactory,它们两者也有一定的区别,简单来讲就是:BeanFactory是spring容器中用来生产bean的工厂,它负责管理一个Bean的生命周期;而FactoryBean它本身就是一个Bean,同时也可以作为工厂来生产各种自定义的Bean,不支持管理Bean的生命周期,需要开发者手动实现实现管理Bean初始化的接口InitializingBean和销毁的接口DisposableBean,一般用与和第三方集成。
1.4 建造者模式
建造者模式传送门:http://c.biancheng.net/view/1354.html
Spring中的应用:用来在web应用中提供http请求响应结果
说明:建造者模式指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。
建造者(Builder)模式的主要角色如下。
- 产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。
- 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()。
- 具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。
- 指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。
spring中各个角色对应的类如下:
二、结构性模式
2.1 代理模式
代理模式传送门:http://c.biancheng.net/view/1359.html
Spring中的应用:Aop
说明:通过jdk和cglib动态代理方式实现Aop功能
首先来了解一下两种动态代理的区别:
下面先举两种动态代理的实现方式,再看Spring中的源码实现。
2.1.1 jdk动态代理
2.1.1.1 jdk动态代理的实现
jdk动态代理是代理一个接口,并由代理类实现InvocationHandler接口的方式实现
1、创建一个UserService接口类,并定义两个方法
1 package com.spring.reading.proxy.jdk; 2 3 import com.spring.reading.vo.UserVo; 4 5 /** 6 * @author: cyhua 7 * @createTime: 2021/11/26 8 * @description: 9 */ 10 public interface UserService { 11 12 int add(UserVo userVo); 13 14 15 String update(UserVo userVo); 16 }
2、创建一个UserServiceImpl实现类,实现上面的接口
1 package com.spring.reading.proxy.jdk; 2 3 import com.spring.reading.vo.UserVo; 4 5 /** 6 * @author: cyhua 7 * @createTime: 2021/11/26 8 * @description: 9 */ 10 public class UserServiceImpl implements UserService { 11 @Override 12 public int add(UserVo userVo) { 13 System.out.println("add user sueccess" + userVo); 14 return 1; 15 } 16 17 @Override 18 public String update(UserVo userVo) { 19 return null; 20 } 21 }
3、创建一个代理类UserJdkProxy,实现InvocationHandler接口
1 package com.spring.reading.proxy.jdk; 2 3 import java.lang.reflect.InvocationHandler; 4 import java.lang.reflect.Method; 5 6 /** 7 * @author: cyhua 8 * @createTime: 2021/11/26 9 * @description: 10 */ 11 public class UserJdkProxy implements InvocationHandler { 12 13 /** 14 * 被代理的对象 15 */ 16 private Object object; 17 18 public UserJdkProxy(Object object) { 19 this.object = object; 20 } 21 22 @Override 23 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 24 System.out.println("方法之前执行:" + method.getName() + ";参数:" + args.toString()); 25 Object result = method.invoke(object, args); 26 System.out.println("方法之后执行" + method.getName() + ";结果" + result); 27 return result; 28 } 29 30 }
4、通过代理类生成代理对象并调用add接口
1 package com.spring.reading.proxy.jdk; 2 3 import com.spring.reading.vo.UserVo; 4 5 import java.lang.reflect.Proxy; 6 7 /** 8 * @author: cyhua 9 * @createTime: 2021/11/26 10 * @description: 11 */ 12 public class UserJdkProxyCaller { 13 14 public static void main(String[] args) { 15 UserVo userVo = new UserVo(); 16 Class[] interfaces = {UserService.class}; 17 UserService userService = (UserService) Proxy.newProxyInstance(UserJdkProxy.class.getClassLoader(), interfaces, new UserJdkProxy(new UserServiceImpl())); 18 int add = userService.add(userVo); 19 System.out.println("add:" + add); 20 } 21 }
上面第17行代码中的userService对象是通过gdk动态代理创建的。
2.1.1.2 Spring中的应用
Spring中通过jdk实现的动态代理类的关系如下:
获取代理对象的核心源代码如下:
1 /** Config used to configure this proxy. */ 2 private final AdvisedSupport advised; 3 4 /** 5 * Is the {@link #equals} method defined on the proxied interfaces? 6 */ 7 private boolean equalsDefined; 8 9 /** 10 * Is the {@link #hashCode} method defined on the proxied interfaces? 11 */ 12 private boolean hashCodeDefined; 13 14 15 /** 16 * Construct a new JdkDynamicAopProxy for the given AOP configuration. 17 * 通过指定AOP配置类创建JdkDynamicAopProxy的构造方法 18 * @param config the AOP configuration as AdvisedSupport object 19 * @throws AopConfigException if the config is invalid. We try to throw an informative 20 * exception in this case, rather than let a mysterious failure happen later. 21 */ 22 public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { 23 Assert.notNull(config, "AdvisedSupport must not be null"); 24 if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) { 25 throw new AopConfigException("No advisors and no TargetSource specified"); 26 } 27 this.advised = config; 28 } 29 30 31 @Override 32 public Object getProxy() { 33 return getProxy(ClassUtils.getDefaultClassLoader()); 34 } 35 36 /** 37 * 获取指定类加载器的代理对象 38 * @param classLoader the class loader to create the proxy with 39 * (or {@code null} for the low-level proxy facility's default) 40 * @return 41 */ 42 @Override 43 public Object getProxy(@Nullable ClassLoader classLoader) { 44 if (logger.isTraceEnabled()) { 45 logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource()); 46 } 47 //通过advised获取到被代理接口 48 Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); 49 findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); 50 return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); 51 }
上面代码中第2行维护了一个被代理对象的配置类advised,该类的值通过22行的构造函数JdkDynamicAopProxy(AdvisedSupport config)传入,在48行通过AopProxyUtil工具解析被代理的接口列表,很典型的的jdk动态代理实践。
2.1.2 cglib动态代理
cglib动态代理代理的对象不是一个接口,而是一个具体的类,而且代理类需要实现MethodInterceptor接口
2.1.2.1 cglib动态代理的实现
1、引入axpectj的依赖
1 <dependency> 2 <groupId>org.springframework</groupId> 3 <artifactId>spring-aop</artifactId> 4 <version>5.2.9.RELEASE</version> 5 </dependency> 6 7 <dependency> 8 <groupId>org.aspectj</groupId> 9 <artifactId>aspectjweaver</artifactId> 10 <version>1.9.6</version> 11 </dependency>
2、创建一个类OrderService
1 package com.spring.reading.proxy.cglib; 2 3 import java.util.UUID; 4 5 /** 6 * @author: cyhua 7 * @createTime: 2021/11/27 8 * @description: 一个具体的类,提供订单相关服务 9 */ 10 public class OrderService { 11 12 public String createOrder(Order order) { 13 System.out.println("OrderService createOrder begin..."); 14 String orderNumber = UUID.randomUUID().toString().replace("-", ""); 15 order.setOrderNumber(orderNumber); 16 System.out.println("OrderService createOrder end..."); 17 return orderNumber; 18 } 19 20 }
3、创建一个代理类CglibProxy
1 package com.spring.reading.proxy.cglib; 2 3 import org.springframework.cglib.proxy.Enhancer; 4 import org.springframework.cglib.proxy.MethodInterceptor; 5 import org.springframework.cglib.proxy.MethodProxy; 6 7 import java.lang.reflect.Method; 8 9 /** 10 * @author: cyhua 11 * @createTime: 2021/11/27 12 * @description: 实现MethodInterceptor,CGLIB动态代理 13 */ 14 public class CglibProxy<T> implements MethodInterceptor { 15 16 public T getProxy(Class<T> targetClass) { 17 Enhancer enhancer = new Enhancer(); 18 return (T) enhancer.create(targetClass, this); 19 } 20 21 @Override 22 public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { 23 System.out.println("OrderInterceptor intercept begin"); 24 return methodProxy.invokeSuper(o, args); 25 } 26 }
4、通过cglig代理生成UserService的对象,并调用createOrder方法
1 package com.spring.reading.proxy.cglib; 2 3 import java.math.BigDecimal; 4 import java.util.Arrays; 5 6 /** 7 * @author: cyhua 8 * @createTime: 2021/11/27 9 * @description: 10 */ 11 public class OrderServiceCaller { 12 13 public static void main(String[] args) { 14 Order order = new Order(); 15 order.setTotalPrice(new BigDecimal(200)); 16 order.setProductIds(Arrays.asList("1", "2", "3")); 17 OrderService orderService = new CglibProxy<OrderService>().getProxy(OrderService.class); 18 System.out.println("OrderServiceCaller end:orderNumber=" + orderService.createOrder(order)); 19 } 20 }
上面的第17行就是通过cglib动态代理生成UserService类对象的方式。
2.1.2.2 Spring中的实现
Spring中通过jdk实现的动态代理类的关系如下:
其中的CgligAopProxy就是实现cglib动态代理类,因为代码较多,只选取核心代码看看,具体实现大家直接看源码:
1 // Configure CGLIB Enhancer... 2 //配置CGLIB的Enhancer 3 Enhancer enhancer = createEnhancer(); 4 if (classLoader != null) { 5 enhancer.setClassLoader(classLoader); 6 if (classLoader instanceof SmartClassLoader && 7 ((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) { 8 enhancer.setUseCache(false); 9 } 10 } 11 enhancer.setSuperclass(proxySuperClass); 12 enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised)); 13 enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); 14 enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader)); 15 16 //获取回调接口:通过反射获取 17 Callback[] callbacks = getCallbacks(rootClass); 18 Class<?>[] types = new Class<?>[callbacks.length]; 19 for (int x = 0; x < types.length; x++) { 20 types[x] = callbacks[x].getClass(); 21 } 22 // fixedInterceptorMap only populated at this point, after getCallbacks call above 23 //在上面的回调函数调用字后,执行过滤器,只使用已被填充的属性 24 enhancer.setCallbackFilter(new ProxyCallbackFilter( 25 this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset)); 26 enhancer.setCallbackTypes(types); 27 28 // Generate the proxy class and create a proxy instance. 29 //生成代理类并创建一个代理对象 30 return createProxyClassAndInstance(enhancer, callbacks);
以上就是spring中代理模式的实现。
三、行为性模式
3.1 模板方法模式
模板方法模式传送门:http://c.biancheng.net/view/1376.html
Spring中的应用:容器初始化过程
说明:AbstractApplicationContext,容器初始化的refresh方法就使用了经典模板方法模式
模板方法(Template Method)模式的定义如下:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。
Spring中的refres方法可以收是对模板方法模式的最佳实践:
1 public void refresh() throws BeansException, IllegalStateException {
2 synchronized(this.startupShutdownMonitor) {
3 this.prepareRefresh();
4 ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
5 this.prepareBeanFactory(beanFactory);
6
7 try {
8 this.postProcessBeanFactory(beanFactory);
9 this.invokeBeanFactoryPostProcessors(beanFactory);
10 this.registerBeanPostProcessors(beanFactory);
11 this.initMessageSource();
12 this.initApplicationEventMulticaster();
13 this.onRefresh();
14 this.registerListeners();
15 this.finishBeanFactoryInitialization(beanFactory);
16 this.finishRefresh();
17 } catch (BeansException var9) {
18 if (this.logger.isWarnEnabled()) {
19 this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
20 }
21
22 this.destroyBeans();
23 this.cancelRefresh(var9);
24 throw var9;
25 } finally {
26 this.resetCommonCaches();
27 }
28
29 }
30 }
refresh方法中定义了一套流程,其中postProcessBeanFactory、onRefresh以及一些其他方法中调用的方法,都是延迟到子类中实现,在AbstractApplicationContext类中只是定义了一个空实现,这就是模板方法模式中的钩子方法,下面是对该模式中各个角色在Spring中的应用枚举:
这个模式在Spring中的应用真的是太经典了,让我直呼膜拜!!!
3.2 观察者模式
观察者模式传送门:http://c.biancheng.net/view/1390.html
Spring中的应用:广播上下文事件,由监听器进行监听处理
说明:观察者(Observer)模式,指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。在spring中,会有很多不同的事件和监听器,来发布和监听上下文的变化情况,并根据具体事件作出对应的处理
观察者模式的主要角色如下。
- 抽象主题(Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
- 具体主题(Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
- 抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
- 具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。
观察者模式中的角色在Spring中的对应关系如下:
下面来看下各个角色的代码实现:
1、抽象主题(Subject)角色,提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
1 /** 2 * Interface to be implemented by objects that can manage a number of 3 * {@link ApplicationListener} objects and publish events to them. 4 * 实现该接口的对象能够管理多个ApplicationListener对象,并向它们发布事件 5 * 6 * <p>An {@link org.springframework.context.ApplicationEventPublisher}, typically 7 * a Spring {@link org.springframework.context.ApplicationContext}, can use an 8 * {@code ApplicationEventMulticaster} as a delegate for actually publishing events. 9 * 一个ApplicationEventPublisher,通常是一个spring ApplicationContext,能有使用ApplicationEventMulticaster作为实际发布事件的代理 10 * 该接口是观察者模式中的抽象主题(Subject)角色: 11 * 也叫抽象目标类,提供了用户保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法 12 * 13 * 14推荐阅读
NeurIPS 2022 | 最强斗地主AI!网易互娱AI Lab提出基于完美信息蒸馏的方法-完美信息蒸馏(PTIE) 在斗地主游戏中,非完美信息的引入主要是由于三位玩家均不能看到别人的手牌,对于任意一位玩家而言,仅可知道其余两位玩家当前手牌的并集,而难于精准判断每位玩家当前手牌。完美信息蒸馏的思路是针对这种非完美问题,构建一个第三方角色,该角色可以看到三位玩家的手牌,该角色在不告知每位玩家完美信息的情况下通过信息蒸馏的方式引导玩家打出当前情况下合理的出牌。 以强化学习常用的 Actor-Critic 算法为例,PTIE 在 Actor-Critic 算法的应用中可以利用 Critic 的 Value 输出作为蒸馏手段来提升 Actor 的表现。具体而言即在训练中 Critic 的输入为完美信息(包含所有玩家的手牌信息),Actor 的输入为非完美信息(仅包含自己手牌信息),此种情况下 Critic 给予的 Value 值包含了完美信息,可以更好地帮助 Actor 学习到更好的策略。 从更新公式上来看,正常的 Actor-Critic 算法 Actor 更新的方式如下: 在 PTIE 模式下,对于每个非完美信息状态 h,我们可以在 Critic 中构建对应的完美信息状态 D(h),并用 Critic 的输出来更新 Actor 的策略梯度,从而达到完美信息蒸馏的效果。 PTIE 框架的整体结构如下图所示: 无论是训练还是执行过程中智能体都不会直接使用完美信息,在训练中通过蒸馏将完美信息用于提升策略,从而帮助智能体达到一个更高的强度。 PTIE 的另一种蒸馏方式是将完美信息奖励引入到奖励值函数的训练中,PerfectDou 提出了基于阵营设计的完美信息奖励 node reward,以引导智能体学习到斗地主游戏中的合作策略,其定义如下: 如上所示,完美信息部分 代表 t 时刻地主手牌最少几步可以出完,在斗地主游戏中可以近似理解为是距游戏获胜的距离, 代表 t 时刻地主阵营和农民阵营距游戏获胜的距离之差, 为调节系数。通过此种奖励设计,在训练时既可以一定程度地引入各玩家的手牌信息(出完的步数需要知道具体手牌才能计算),同时也鼓励农民以阵营的角度做出决策,提升农民的合作性。 特征构建: PerfectDou 针对牌类游戏的特点主要构建了两部分特征:牌局状态特征和动作特征。其中牌局状态特征主要包括当前玩家手牌牌型特征、当前玩家打出的卡牌牌型特征、玩家角色、玩家手牌数目等常用特征,动作特征主要用于刻画当前状态下玩家的所有可能出牌,包括了每种出牌动作的牌型特征、动作的卡牌数目、是否为最大动作等特征。 牌型特征为 12 * 15 的矩阵,如下图所示: 该矩阵前 4 行代表对应每种卡牌的张数,5-12 行代表该种卡牌的种类和对应位置。 网络结构和动作空间设计 针对斗地主游戏出牌组合数较多的问题,PerfectDou 基于 RLCard 的工作上对动作空间进行了简化,对占比最大的两个出牌牌型:飞机带翅膀和四带二进行了动作压缩,将整体动作空间由 27472 种缩减到 621 种。 PerfectDou 策略网络结构如下图所示: 策略网络结构同样分为两部分:状态特征部分和动作特征部分。 在状态特征部分,LSTM 网络用于提取玩家的历史行为特征,当前牌局状态特征和提取后的行为特征会再通过多层的 MLP 网络输出当前的状态信息 embedding。 在动作特征部分,每个可行动作同样会经过多层 MLP 网络进行编码,编码后的动作特征会与其对应的状态信息 embedding 经过一层 MLP 网络计算两者间的相似度,并经由 softmax 函数输出对应的动作概率。 实验结果
深入理解Spring源码(二):探讨Spring框架中应用的设计模式
深入理解Java设计模式及其在Spring中的应用:命令与迭代器模式详解(第七部分)