假设一个营销抽奖场景,奖品发放支持满减优惠券和实物兑换码两种类型,由于兑换发放具有相似的上下文流程,可以用工厂策略模式的方式进行类型和实现类的映射。例如:
map---("coupon",CouponDistributeService) //优惠券发放
map---("redeemCode",RedeemCodeDistributeService) //兑换码发放
并且我们可能通常会需要预先初始化这个名称对应Service的映射map,然后通过map.get(String type)方法获取对应的Service接口,在实现类层面通过@Service注解标识以后,就可以通过类型去进行对应的实现类的查找,调用具体的兑换方法。
@Service
//优惠券发放接口的对应实现类
boolean Result< distributeDTO> CouponDistributeServiceImpl(DistributeContext context){
}
@Service
//兑换码发放接口的对应实现类
boolean Result< distributeDTO> RedeemCodeDistributeServiceImpl(DistributeContext context){
}
对于map的初始化方式,我们可能首先会想到通过静态代码块来实现,但是阅读过一些成熟的项目代码会发现,其实还有另外一种通过Spring框架bean加载的钩子函数ApplicationContextAware实现初始化map的写法,现对两种方法实现初始化策略模式的映射map过程进行对比。
参考博客https://blog.csdn.net/weixin_51146329/article/details/129562664
5个步骤:加载-验证-准备-解析-初始化;每一步做了什么:
(1)加载:加载是类加载过程中的一个阶段,这个阶段会加载字节码并在内存中生成一个代表这个类的 java.lang.Class 对 象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既 可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理), 也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)
(2)验证:这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
(3)准备:准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
(4)解析:解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的常量。
(5)初始化:初始化阶段是类加载最后一个阶段,此过程可执行构造器方法并对变量赋实际值,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。
我们先定义一个map:
public static final Map<String,AwardDistributeService> map = new HashMap<>();
final关键字修饰的常量,如果通过字面量方式赋值则在编译时确定(常量折叠),但是其他时候可能在准备或者初始化阶段赋值,static修饰的变量在类加载的准备阶段分配内存并且设置默认值,静态代码块进行初始化的位置是类加载的初始化阶段。
因此带static修饰的静态变量通常赋值优先于静态代码块。public static final修饰的Map,是在初始化阶段赋值的,具体解释参考博客https://blog.csdn.net/qq_42778327/article/details/119791027;https://blog.csdn.net/qq_43842093/article/details/122144825
注意生命周期加载顺序 https://www.cnblogs.com/musecho/p/17117111.html
//先定义一个map
public static final Map<String,AwardDistributeService> map = new HashMap<>();
//如果不需要从容器注入 可以用静态代码块 这里如果是需要注入就不能这么写
static{
map.put("coupon",new ArrayList<Object>());
map.put("redeemCode",new ArrayList<Object>());
}
//生命周期 @Constructor >> @Autowired >> @PostConstruct
@Autowared
CouponDistributeService couponDistributeService;
@Autowared
RedeemCodeDistributeServiceredeemCodeDistributeService;
@PostConstruct //在Autowared后只初始化执行一次
public static void initMap(){
map.put("coupon",couponDistributeService);//优惠券发放
map.put("redeemCode",redeemCodeDistributeService); //兑换码发放
}
//具体使用type映射服务的distribute发放方法
boolean success = map.get(type).distribute();
但是这样写的话,导致map的定义和初始化分离的,可能需要判断下这个初始化代码放到哪里比较合适,下次修改变量名称或者增加一个新的映射的时候,是不是方便找到这段代码并进行修改。
为了修改方便,我们可以采用一个动态配置中心的方式,让初始化部分自动读取配置中心的内容。下次更改变量名称或者增加一个新的映射的时候,不需要找到这段具体代码并进行修改,比如阿里的diamond或者字节的TCC,都是支持这种方式动态配置的,可以简化修改和维护的复杂度。
但是这样写虽然设置内容比较直观,可读性好一点,但是通常定义与配置不在一个文件里面,整体来看其实还是比较分散的,如果映射名称和具体类别动态变更导致工程里面代码实际调用的方法映射不匹配或者内容不统一,可能也会报错。
比如把映射type名称从"redeemCode"改成"redeemCoupon",代码里面用map.get(“redeemCode”)去寻找执行实现类的时候可能会找不到,又或者新增一个文字描述类型的奖品和发放服务映射关系map–(“description”,descriptionDistributeService),这个时候更改需要在动态配置中心添加这个映射,并且线下开发与测试环境、预发和线上环境都需要支持,具体工程调用方才能正确调用到。
参考博客https://www.cnblogs.com/misscai/p/14749225.html 整体流程如下:
对于ApplicationContextAware的理解:我们常用的IOC 容器是 ApplicationContext, 它的顶层接口是 BeanFactory,是给Spring框架内部用的, ApplicationContext 对BeanFactory 进行了扩展。我们拿到 IOC 容器的方式有3种,使用ApplicationContext接口下的三个实现类:ClassPathXmlApplicationContext、FileSystemXmlApplicationContext、AnnotationConfigApplicationContext, 但是 SpringBoot 中,IOC 配置文件都被简化了,无法通过上述3种方式拿到 IOC 容器,但是有时候需求又必须是用 Spring 容器才能实现,所以ApplicationContextAware 就是用来获取框架自动初始化到IOC 容器对象的。
通过bean的生命周期图,我们可以看到ApplicationContextAware接口函数调用是在bean加载的属性填充到预初始化之间的阶段进行的,因此可以类似基于支持实现类的SPI的定期回调执行机制,进行ApplicationContextAware的setApplicationContext()方法重写,执行我们具体的需要的初始化前置的属性填充操作。
参考博客https://www.jianshu.com/p/f5e3b07cc5e2;https://blog.csdn.net/Graceful_scenery/article/details/132243538;https://blog.csdn.net/m0_49499183/article/details/129900726;
https://blog.csdn.net/weixin_43888891/article/details/127837712;
Spring常用钩子 https://www.codenong.com/cs106024311/
通过查看Spring源码发现ApplicationContextAware这个接口里面有一个setApplicationContext方法支持我们自己实现,通过@Component注解标识为组件以后,可以在加载阶段执行组件内部自己定制的属性填充方法。
这里也可以顺便复习一下SpringBoot启动过程,参考博客:https://blog.csdn.net/weixin_55697693/article/details/130358395
//表示这个AwardDistributeStrategyService为bean,Spring启动扫描时候自动发现它
@Component
public class AwardDistributeStrategyService implements ApplicationContextAware {
private static final Map<String, AwardDistributeService> CONTEXT = new HashMap<>();
//重写setApplicationContext方法进行bean的属性注入
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, AwardDistributeService> beansOfType = applicationContext.getBeansOfType(AwardDistributeService.class);
beansOfType.values().forEach(distributeService -> CONTEXT.put(distributeService.getAwardType(), distributeService));
}
//具体奖品发放方法
public void distribute(DistributeDO awardDistributeDO) {
String awardType = awardDistributeDO.getAwardType();
AwardDistributeService awardDistributeService= CONTEXT.get(awardType);
if (awardDistributeService!= null) {
awardDistributeService.distribute(awardDistributeDO);
}
}
}
分析总结这种实现方式的好处在于:
(1)映射的定义与初始化配置相对比较集中,并且可以通过distributeService.getAwardType()直接获取对应奖品类的属性值,不需要显示进行type名称设置。
(2)把修改的操作权利归还给对应的奖品类去实现,比如想修改卡券的权益type名称,可以直接在各自的类里面修改,并不影响这里的配置加载。
(3)新增一个映射的时候,只需要增加对应的类,这个setApplicationContext函数里面的内容也是不需要动的,减小改的范围,符合类与方法的设计原则。
(4)这种配置方式是基于bean标识@Component以后,通过SpringBoot启动组件扫描,定期执行对应的定期SPI回调执行机制实现自动化管理和配置,充分利用了bean的生命周期和特性,使得我们对于Spring底层原理的理解不仅仅局限于八股级别,能够充分运用到实践中去。
实现方式 | 优点 | 缺点 |
---|---|---|
静态代码块 | 配置直观,易于理解 | 创建和配置分离,静态设置后期变更修改与维护复杂 |
动态配置中心 | 不依赖工程代码,支持动态配置 | 线下、测试、预发、线上环境多处配置,变更key后工程代码可能找不到方法 |
钩子接口ApplicationContextAware | 支持bean组件内部定制化实现,Spring自动扫描回调管理,创建和配置相对集中,对类内部修改即可,变动影响范围较小 | 可读性降低,需要对Spring框架原理有一定理解 |
本文对比了通过静态代码块VS钩子接口ApplicationContextAware实现策略模式map的初始化过程,两者各有优缺点,可以根据具体场景选择合适的初始化方法。