<!--
如下的Mybatis 配置,collection 是如何工作的,对于SQL查询的数据结果集,在Mybatis 映射生成对象时,怎样组装的?
-->
<resultMap id="viewResultMap" type="com.mybatis.model.CityView">
<result column="pid" jdbcType="INTEGER" property="pId"/>
<result column="name" jdbcType="VARCHAR" property="countryName"/>
<collection property="citys" resultMap="moreResultMap"/>
</resultMap>
resultMap.hasNestedResultMaps()
),从而确定执行哪种匹配映射策略。与collection 相似的还有association
标签,都是一样的作为内嵌映射进行处理。new LinkedHashMap<K, List<V>>()
描述, Mybatis 也是基于这样的结构进行操作。内嵌映射是个递归处理的过程
,如果单纯看源码实现,就很容易陷入其中。如果能先带着猜想,通过设计思路的验证去阅读,会清晰很多。关键类:org.apache.ibatis.executor.resultset.DefaultResultSetHandler
/**
* 内嵌结果集处理整体逻辑:
* 遍历ResultSet,借助全局变量 nestedResultObjects,以主数据为全局唯一主键,处理带有内嵌配置的结果集。类似于在内存中做group by 聚合操作。
* 具体的数据映射,对象构造,内嵌递归处理,参考 getRowValue()
*/
private void handleRowValuesForNestedResultMap() throws SQLException {
// 遍历ResultSet
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
// 构造主数据的唯一主键,这个主键会缓存在全局 nestedResultObjects
final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
// 根据resultSet 构造主数据,或者构造内嵌数据,追加到主数据
rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
// putIfAbsent 操作
Object partialObject = nestedResultObjects.get(rowKey);
if (partialObject == null) {
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
}
/**
* 功能逻辑:resultSet 转化为 instance 策略实现。内嵌映射递归检测、处理。
*
* @partialObject 如果为null,说明是父对象的初始化过程
* 如果非null,说明是内嵌对象的初始化过程
*/
private Object getRowValue() {
// 内嵌对象的初始化过程
if (partialObject != null) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
// 为了明确知道递归调用的阶段,需要借助ancestorObjects 来暂存对应的对象实例。递归完后remove。
putAncestor(rowValue, resultMapId);
applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
ancestorObjects.remove(resultMapId);
} else {
// 父对象的初始化过程
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 1. constructor.newInstance()
rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
// 2. 根据resultSet 赋值给 newInstance
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
putAncestor(rowValue, resultMapId);
// 3. 检测是否有内嵌查询,如果有,则追加。递归调用,直到所有的内嵌查询查询完成。
// 内嵌数据追加到主数据的逻辑:instantiateCollectionPropertyIfAppropriate -> 内嵌查询getRowValue() -> linkObjects
foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues;
ancestorObjects.remove(resultMapId);
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
if (combinedKey != CacheKey.NULL_CACHE_KEY) {
// 4. 缓存rowValue,保证集合数据唯一
nestedResultObjects.put(combinedKey, rowValue);
}
}
}
Snaker是一个基于Java的开源工作流引擎
,适用于企业应用中常见的业务流程。本着轻量、简单、灵巧理念设计,定位于简单集成,多环境支持。
流程引擎源码(oscgit): http://git.oschina.net/yuqs/snakerflow
演示应用源码(oscgit): http://git.oschina.net/yuqs/snaker-web
插件源码(oscgit): http://git.oschina.net/yuqs/snaker-designer
如果了解activiti、jbpm,那么理解、使用snaker可以平滑过渡。API的使用方式和流程模型的定义基本相同。
如果没有接触过工作流,对于snaker 简洁的流程模型的定义格式,以及丰富的示例,也能很快入手。
snaker-core 源码工程test
主要提供流程引擎的组件和功能点的测试、API使用用例。
提供全部支持的流程模型测试用例,可以在 snakerflow\snaker-core\src\test\resources\test 路径下找到
snaker-spring 源码工程test
spring 集成示例
snaker-web 源码工程
完整的工作流程应用实例。业务使用、集成snaker,完全可以参考该工程配置。
技术栈:SpringMVC + shiro + Hibernate + snaker + raphael.js + mysql
snaker提供Eclipse 插件、web 流程设计器
。
web 流程设计器提供在线设计、编辑和保存流程模型。参考snaker-web,设计完的流程模型可以直接部署到应用中使用。
基于 raphael.js 开发
的web设计器和流程可视化显示。
拖拽工具栏,根据业务需求编制即可。需要注意的是,transaction 组件使用,先点击源节点,再点击目的节点,即可完成连接。
具体的节点属性配置,在弹出的表单配置即可。
也可以在生成的xml 流程模型文件中修改属性,重新上传即可实现刷新、覆盖。
应用开发主要使用流程引擎接口API,服务对象只用于流程引擎的调用,不应该直接调用低层的API。
// api集成方式,构造SnakerEngine对象
SnakerEngine engine = new Configuration().buildSnakerEngine();
// spring集成方法,通过配置注入的方式构造SnakerEngine对象
<bean class="org.snaker.engine.spring.SpringSnakerEngine">
<property name="processService" ref="processService"/>
<property name="orderService" ref="orderService"/>
<property name="taskService" ref="taskService"/>
<property name="queryService" ref="queryService"/>
<property name="managerService" ref="managerService"/>
</bean>
// 根据文件输入流,部署流程定义。
engine.process().deploy(StreamHelper.getStreamFromClasspath("test/task/simple/process.snaker"))
// 根据流程定义ID,操作人ID,参数列表启动流程实例
Order order = engine.startInstanceById(processId, operator, args);
// 根据流程名称启动流程实例
Order order = engine.startInstanceByName(name, version, operator);
// 历史任务查询
engine.query().getHistoryTasks(new Page<HistoryTask>(), new QueryFilter().setOperator("foo"));
// 查询当前责任人的任务
engine.query().getActiveTasks(new Page<Task>(), new QueryFilter().setOperator("foo"));
// 根据当前用户查询待办任务列表
engine.query().getWorkItems(page, new QueryFilter().setOperator("foo"));
// 根据流程定义ID,系统调用、执行任务
List<Task> tasks = engine.query().getActiveTasks(new QueryFilter().setOrderId(order.getId()));
for(Task task : tasks) {
engine.executeTask(task.getId(), "foo", args);
}
// 根据任务提交请求,执行任务。args 可以认同为提交的表单内容。
engine.executeTask(task.getId(), "foo", args);
web工程 内置提供了‘请假流程测试’、‘借款申请流程’,可以直接使用。
IDEA 添加jetty 运行配置,启动即可。
访问 http://localhost:8080/snaker-web/login ,使用admin 用户登录即可使用所有的功能。
业务流程如果简单,可以直接把业务数据存在流程实例的变量中。如果复杂业务,可以在业务表中增加order_id、task_id 来支持业务流引擎的集成。业务表属于应用层,流程表属于服务层,高层依赖底层,底层不能耦合高层
。表设计千万注意。
‘借款申请流程’发起申请,可以参考,作为样本代码使用。
整体的设计思路和activiti、jbpm 相似。snaker 基于轻量的方向,只有流程引擎驱动相关的表,只提供经典的工作流特性。
工作流管理系统(Workflow Management System, WfMS)是一个软件系统,它
完成工作量的定义和管理
,并按照在系统中预先定义好的工作流逻辑进行工作流实例的执行
。工作流管理系统不是企业的业务系统,而是为企业的业务系统的运行提供了一个软件的支撑环境。
###流程引擎
public List<Task> executeTask(String taskId, String operator, Map<String, Object> args) {
// 1.完成当前任务,并且构造执行对象
Execution execution = execute(taskId, operator, args);
ProcessModel model = execution.getProcess().getModel();
if(model != null) {
// 反查当前任务,对应在流程定义中的节点
NodeModel nodeModel = model.getNode(execution.getTask().getTaskName());
// 2.该任务对应的节点模型执行(根据路由策略,递归调用,驱动调用的核心设计!)
nodeModel.execute(execution);
}
return execution.getTasks();
}
snker 的流程模型是以字节码的形式存在数据库中的。
如果是根据processId 查询,那么对应的版本就是固定的。
如果是根据processName 查询,那么应该查询最新的版本的流程定义。
// 根据name获取process对象
public Process getProcessByVersion(String name, Integer version) {
if(version == null) {
// select max(version) from wf_process where name = ?
version = access().getLatestProcessVersion(name);
}
if(version == null) {
version = 0;
}
// select * from wf_process where 1=1 and name in(?) and version = ? order by name asc
List<Process> processs = access().getProcesss(null, new QueryFilter().setName(name).setVersion(version));
return processs.get(0);
}
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
是一个编程契约。实现不应依赖于注解驱动的注入或其他反射机制。