QUARKUS 框架, 用于构建云原生的应用。大体类似Spring 的Java框架,又不同常规的Java 框架,提供Native 、Reactive特性。除了提供一揽子的应用开发解决方案,更大的突破在于编译后轻量应用、高性能应用,RedHat出品。
elasticsearch-quickstart 功能点:使用表单添加元素,并且在列表中更新。 浏览器和服务器之间的所有信息都被格式化为JSON。 元素存储在Elasticsearch中。
文档非常详细,可操作性强。直接跟着指导文档步骤,就可以完成Quarkus 应用的构建。
Tips:
使用IDEA 启动Quarkus 应用,不像SpringBoot 之类的,有main 函数启动类。需要依靠Maven 命令,可以将命令配置为启动项,在IDEA 的Run/Debug Configurations,配置对应的Maven 快捷启动。
点击左上角”+”图标添加一个Maven配置如左边栏,在右边栏中的Command line中填入”compile quarkus:dev”,点击OK
构建工程报错。[ERROR] No implementation for org.eclipse.aether.RepositorySystem was bound. while locating io.quarkus.maven.CreateProjectMojo。
解决办法: 根据指导文档,升级Maven 3.6.2+
,使用IDE同理。如果是IDEA, 用新下载的Maven代替自带的。
使用IDEA Download Sources报错。Unable to import maven project。
解决办法:查看IDEA 运行日志, Help ==》 Show Log in Explorer 查看具体的报错日志。
errors: No implementation for org.apache.maven.model.path.PathTranslator was bound. 原因是IDEA版本和新版本的Maven不兼容导致的,如果是要Download Maven Sources,可以暂时先替换为原来的自带Maven,保证Sources 成功下载。呃,Quarkus 对版本的要求还是比较苛刻
,这点在Java框架不常见。
Debug 方法。需要使用Remote 连接应用,达到调试目的。
Quarkus 框架目前的生态非常丰富,快速指导文档包含了各种中间件的示例
。常用的中间件的集成和使用都可以从这里查到。
Quarkus 框架推荐JDK 11+和 GraalVM
,JDK8 后续会废弃和禁止。
直观的感受是和现在用的框架开发模式很像,学习理解和转换的成本很低。开发使用的API都是EJB 规范定义的
。
Quarkus 框架启动非常快,dev模式下,修改完即生效,开发效率高
。不用依赖JRebel 啦。
Quarkus 支持native模式编译
,可以直接生成docker image,没有尝试。参考:src/main/docker/Dockerfile.native
现有的Docker技术,从另外一个层次完成了跨平台的目的。JVM的目的在Cloud 技术栈里,已经没有那么大的作用,如果提供native 的模式,确实可以对Java应用达到各方面的“轻量化”
。
UCC-统一配置中心,实现对应用系统需要实时调整的配置属性进行管理,比如各种开关、阈值、重试次数等。
Spring-UCC 组件实现思路:基于ZooKeeper,实现配置的保存和分发。通过ZK的节点watch特性,实现管理端修改完配置数据后,每个应用的ZK client 都可以收到变动数据。
然后通过约定的配置,将对应数据同步到 Config Bean 对应的属性。
最终达到实时修改应用配置属性的目的。
功能开发中,使用UCC 配置了业务阈值用来是否监控报警。结果发现通过UCC管理端修改配置数值后,线上打印的配置一直没有发生变化。按照Spring 常用的、约定的配置方法,不应该出现问题呀。
<!-- Spring-UCC 集成启动的核心,通过UccConfigCenter 完成zk client启动、节点检查、节点监听以及应用配置数据的检查和加载 -->
<bean class="com.foo.ucc.client.service.UccConfigCenter">
<constructor-arg index="0" ref="zkConfig"/>
<constructor-arg index="1" ref="propertyConfig"/>
</bean>
<!-- 主要功能:当监听的ZK node数据发生变化时,负责完成 propertyConfig 配置属性的重新赋值 -->
<bean id="propertyConfigProcessor" class="com.foo.ucc.client.PropertyConfigProcessor"/>
<bean id="propertyConfig" class="com.foo.ucc.client.config.UccPropertyConfig">
<property name="processor" ref="propertyConfigProcessor" />
<property name="keyList">
<list>
<!-- 约定配置格式:BeanName.property -->
<value>configFoo.name</value>
<value>configFoo.age</value>
</list>
</property>
</bean>
/**
* 应用 ucc 配置的开关和阈值
*/
@Data
@Component
public class ConfigFoo {
private String name;
/**
* 不能实时配置生效的属性
*/
private double age;
}
配置不能实时生效,问题排查有两个方向:ZK 的通知机制,通知数据同步到ConfigFoo 的机制。
/**
* ZK 的通知机制
*
* UCC 初始化配置过程中,会给具体的配置项(此处抽象为 PathCache)注册对应的监听器
* 其中PathCache、IZkNodeListener 都是对zookeeper 原生API的包装,使操作使用更加的方便。
* @From UccConfigCenter
*/
PathCache pathCache = new PathCache(uccClient.getZkClient(), path);
// 典型的观察者设计模式,把监听器集合关联到对应的path,当zk path 发生数据变化时,依赖他的监听器都会得到通知,执行具体的业务逻辑。数据结构: Map<String, Set<IZkNodeListener>> nodeListener
pathCache.addNodeChangedListener(new IZkNodeListener() {
// 调用链:org.apache.zookeeper.Watcher#process -> fireNodeChangedEvents -> 根据path 获取对应的listener集合 -> listener.handleNodeChange
public void handleNodeChange(String path, Object data) throws Exception {
// 通知数据同步到ConfigFoo
loadConfigOnDataChangeEvent(propertyKey, pathArg, false);
}
});
/**
* 通知数据同步到ConfigFoo 的机制
*
* 参考上述的UccPropertyConfig 配置,具体的propertyKey 有对应的PropertyConfigProcessor 负责处理zk 变化的数据。关键是propertyKey, 和path。
* @param propertyKey,根据约定格式,可以得到beanName 和他的field,通过反射调用赋新值
* @param path,根据path 获取zk的data,即配置的最新业务数值
* @From PropertyConfigProcessor#process
*/
public boolean process(String propertyKey, String path) {
String toChangeValue = new String(zookeeper.getData(path, watch, stat));
List<String> splitList = GuavaUtils.strSplitWithTrim(propertyKey, ".");
String beanName = splitList.get(0);
String fieldName = splitList.get(1);
bean = applicationContext.getBean(beanName);
// 通过反射调用,给bean.field 赋值。问题出在这里,toChangeValue 在转化为field 定义的类型,源码是通过枚举常用的类型实现的,只支持Integer、String、Boolean。ConfigFoo.age 是double,不支持转换,忽略了。
return FieldChangeUtils.changeField(toChangeValue, fieldName, bean);
}
组件内置反射工具不支持double赋值,所以新的配置没有生效
。Spring-UCC 组件在ZooKeeper 的封装和应用配置功能的丰富度都可圈可点,有很多值得学习的地方。但是数据的同步更新功能,作为核心的功能和基础的支持,如此简陋,也是非常的遗憾。
改进方案:从Spring 框架实现中拾取类型转换的黑魔法。可以直接用 DefaultConversionService#convert 替换上述自己实现的反射工具类
。支持的类型丰富、场景多样。
在 Spring 框架中,Bean 实例化和注入是相互的过程,循环依赖是一个必须要解决的问题。
除了通过三级缓存来解决循环依赖,还有一种编程式方式,借助动态代理,推迟对象的初始化解决循环依赖。
本文以 AbstractFactoryBean 实现方案,记录这种使用场景。
AbstractFactoryBean 是 Spring 框架对于 FactoryBean 约定的一个模板实现,完成 单例对象或者原型对象的创建。支持对象延迟访问。
/**
* Expose the singleton instance or create a new prototype instance.
* 对于还未实例化的清空,堆外暴露的只是代理对象。只有真正访问时,才会对实例化的对象发起的调用。
*/
@Override
public final T getObject() throws Exception {
if (isSingleton()) {
// 对于尚未实例化的Bean, 仍然支持实例返回。
// 非实时初始化,就不会形成链式初始化,也就打破了循环依赖链条。
return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());
}
else {
return createInstance();
}
}
/**
* Determine an 'eager singleton' instance, exposed in case of a
* circular reference. Not called in a non-circular scenario.
*/
@SuppressWarnings("unchecked")
private T getEarlySingletonInstance() throws Exception {
Class<?>[] ifcs = getEarlySingletonInterfaces();
if (ifcs == null) {
throw new FactoryBeanNotInitializedException(
getClass().getName() + " does not support circular references");
}
if (this.earlySingletonInstance == null) {
// 对于真正的单例对象,支持延时访问/调用。bean 初始化非强制依赖。
// 解耦 bean 初始化和实际调用时机的耦合。打破初始化循环依赖。
this.earlySingletonInstance = (T) Proxy.newProxyInstance(
this.beanClassLoader, ifcs, new EarlySingletonInvocationHandler());
}
// 代理对象,作用:占位。
return this.earlySingletonInstance;
}
循环依赖最可能出现的场景:constructor injection
循环依赖可以类比为:a classic chicken-and-egg scenario
FactoryBean
是一个编程契约。实现不应依赖于注解驱动的注入或其他反射机制。
遇到的问题:项目中需要用到策略模式,把策略实现以集合的形式注入到Service 中。因为要强制校验策略的顺序,所以采用的是构造器注入,简单明了。结果Spring 启动失败,
Is there an unresolvable circular reference?
。
首先,查看Spring启动异常的日志,throw BeanCurrentlyInCreationException
/**
* Exception thrown in case of 引用了当前正在创建中的bean.
* Typically happens when constructor autowiring matches the currently constructed bean.
* 直接指明出现在构造器注入时发生
*/
public class BeanCurrentlyInCreationException extends BeanCreationException {
}
然后,根据异常日志提示的信息,可以梳理出依赖的关系链,aService 构造器注入的策略bService,bService 实现业务注入cService,而cService注入了aService。
需要特别强调的是:循环依赖和Spring 创建Bean 的顺序有严格的关系。例如:AService、CService 类的顺序随着ClassPath扫描后,自然的就是先创建AService Bean。如果CService 先扫描到,那么就不会出现 BeanCurrentlyInCreationException。所以深入的分析,循环依赖问题和操作系统的文件系统或者xml 配置的顺序都有关系
。
2个解决方案:
策略集合采用属性Setter注入,单独写@PostConstruct 初始化方法,完成策略顺序的校验和打印。替换掉构造器注入的写法。这个方案比较直接。
指定Spring容器创建Bean的顺序,保证cService在aService 之前创建,这样就解耦了循环依赖。详细解释如下分析可知。
// 强制指定Spring 容器先创建CService。这样可以通过earlySingletonExposure 方案解决循环 @DependsOn(value = "cService") @Service public class AService { private CService cService; @Autowired public AService(CService cService) { this.cService = cService; } } @Service public class CService { @Autowired private AService aService; }
文章主要记录查看源码的思路,和总结,便于后续方便查阅。
具体的循环依赖源码实现分析,参考:彻底理解SpringIOC、DI-这篇文章就够了、图解Spring解决循环依赖♻️
Spring对于循环依赖的处理,如果从设计的原因去看,阅读源码可能会清晰一下。如果直接入手看源码,可能会比较绕、困惑很多。一是循环依赖是个循环调用、递归的过程,静态阅读或者动态调试都会比较复杂;另外一个是由于Spring丰富的功能设计,不能非常直接的看到循环依赖核心的实现方案。
一种Insight 方法是:直接BeanCurrentlyInCreationException
打断点,查看全部的调用链,从中分析出现循环依赖的原因。属于逆向的看源码。
一种Insight方法是:先分析为什么出现循环依赖,如果要解决循环依赖需要做哪些设计。再结合博客的讲解(Spring 文档没有找到对循环依赖设计思路的描述),提纲挈领的去阅读源码。属于正向设计思路的看源码。
resolving a circular reference
),只限定在singleton 模式下,prototype不支持。默认 bean初始化是可以通过拆解初始化步骤解耦循环,如果是构造器注入,则把循环依赖拉低到JDK层级,不可控无法实现。
想要得到原始对象,肯定是要构造器执行完成后
。Spring 托管构造器后,需要的构造器参数由Spring find,如果Spring检测到循环依赖后,自然就终止了初始化过程。对应到源码就是createBeanInstance,该方法没有执行完,没有earlySingletonExposure原始对象暴露给Spring容器
。因为是singleton 模式,全局唯一的引用,支持提前赋值(注入)到其他bean 对象中
。原始对象的setter注入由populateBean完成。通过拆分bean 的instantiate、set/inject 步骤,并且把singleton 引用提前缓存到全局变量中,这样即使是循环依赖也可以提前得到初始化中的原始对象。即使概念意义的Bean没有初始化完成
,也不会出现循环依赖无法解决的情况。/**
* Instantiate the given bean using its default constructor.
* 默认的初始化bean 的实现,
* 简单描述为:return new BeanWrapper(Foo.class.getConstructor().newInstance())
* @From AbstractAutowireCapableBeanFactory
*/
protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {
try {
Object beanInstance;
final BeanFactory parent = this;
// 对象instantiate。根据BeanDefinition 获取bean Class的信息,解析默认的构造函数,通过反射得到对象
// Spring在此处也应用了策略模式,增加扩展性的同时,也降低了阅读的流畅性。
beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);
// 将原始的对象包装为Bean,暴露给Spring容器。包装的目的是:方便对原始对象做各种转换和操作。
BeanWrapper bw = new BeanWrapperImpl(beanInstance);
initBeanWrapper(bw);
return bw;
}
catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex);
}
}
本文关注 Spring 注入泛集合的调用链,以及注入集合的排序问题和应用。
泛集合是指:Array、Collection、Map。
构造注入
方式的调用链(Spring Version >= 4.x):
org.springframework.beans.factory.support.ConstructorResolver#autowireConstructor
提供各种构造器的识别、构造参数实例化的实现
org.springframework.beans.factory.support.ConstructorResolver#createArgumentArray
org.springframework.beans.factory.support.ConstructorResolver#resolveAutowiredArgument
org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency
Spring 依赖注入的核心实现方法
。
org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveMultipleBeans
泛集合注入实现,把需要的 beans 封装为 Array、Collection、Map。
其中,Array和Collection 类型的注入是经过排序后
的对象,Map 排序是以 LinkedHashMap 体现。
org.springframework.core.Ordered,声明顺序的接口,用于类似拦截器、组件的执行优先级声明。
// 根据升序顺序的约定,order value 越小,排序越靠前, 优先级越高。
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
org.springframework.core.OrderComparator, 按照升序排序。支持实现 Ordered 接口的对象排序。
org.springframework.core.annotation.AnnotationAwareOrderComparator,支持使用顺序注解的对象排序,如:@Order
和@Priority 。兼容Ordered接口 。
public class OrderComparator implements Comparator<Object> {
/**
* Shared default instance of OrderComparator.
*/
public static final OrderComparator INSTANCE = new OrderComparator();
// 升序排序
public int compare(Object o1, Object o2) {
boolean p1 = (o1 instanceof PriorityOrdered);
boolean p2 = (o2 instanceof PriorityOrdered);
// 先区分是否实现Ordered 接口。实现 Ordered 接口的对象,排序靠前。
if (p1 && !p2) {
return -1;
}
else if (p2 && !p1) {
return 1;
}
// 获取 order value,再基于value 对比排序。
int i1 = getOrder(o1);
int i2 = getOrder(o2);
return (i1 < i2) ? -1 : (i1 > i2) ? 1 : 0;
}
/**
* 对外提供排序方法,方便调用。
*/
public static void sort(List<?> list) {
if (list.size() > 1) {
Collections.sort(list, INSTANCE);
}
}
}