现有项目使用Spring3 框架,想要对应用所有的请求进行统一监控。
一种方案是配置全局的
HandlerInterceptor
,实现对请求的监控。一种方案是基于AOP,拦截@RequestMapping,实现对Controller 的方法进行监控。项目中采用的是这种方案,主要目的是收集方法粒度的性能、可用率的数据。
编写完自定义的Aspect 监控类后,发现切面不生效。
在spring-config.xml 中配置启用了@AspectJ 特性。
<!--Enables the use of the @AspectJ style of Spring AOP.-->
<aop:aspectj-autoproxy proxy-target-class="true" />
使用的是Spring3 的框架,web.xml 中集成Spring 配置如下所示
<!-- spring3 常见的配置方式,分为Spring容器配置 和 SpringMVC 配置,两个配置文件中各自扫描对应的包路径 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-config.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-config-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
首先确认自定义的AOP Bean 是正常加载后,剩下的问题就是探究为什么<aop:aspectj-autoproxy/>
没有生效。
aspectj-autoproxy 配置的工作原理参考 Insight aop:aspectj-autoproxy 解析。
通过该配置,会给Spring 容器注册AnnotationAwareAspectJAutoProxyCreator, 属于BeanPostProcessor
组件,这样的话,就会在生成bean 的以后,根据Bean的特征,对bean 生成代理类。
作用:checking for marker interfaces or wrapping them with proxies.
对应到本案例,就是检查是否存在 @RequestMapping 注解,并对符合条件的bean 进行代理,实现监控功能的增强。
通过检查AspectJ 语法和对应的路径,发现也正常,那么问题就可能出现在Spring 容器中。
通过上述的web.xml 配置,可以分析出存在两个spring 容器,容器是父子的关系。
// listener配置的父容器
// Bootstrap listener to start up and shut down Spring's root WebApplicationContext
// org.springframework.web.context.ContextLoader#initWebApplicationContext
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
// servlet 配置的子容器
// Instantiate the WebApplicationContext for this servlet
ConfigurableWebApplicationContext wac = BeanUtils.instantiateClass(XmlWebApplicationContext.class);
wac.setEnvironment(getEnvironment());
// set root WebApplicationContext
wac.setParent(parent);
按照正常的思路来看,子容器应该能集成父容器的特性和注册的解析器、处理器等。
事实证明,父容器注册AnnotationAwareAspectJAutoProxyCreator,子容器不会查找和使用。这也是AOP代理不生效的原因
。
/**
* 根据指定的类型,返回唯一的bean
* 如果在当前的BeanFactory 找不到,会委托给父BeanFactory 查找,从而实现递归查找的策略
*/
public <T> T getBean(Class<T> requiredType) throws BeansException {
Assert.notNull(requiredType, "Required type must not be null");
String[] beanNames = getBeanNamesForType(requiredType);
if (beanNames.length > 1) {
// ......
}
if (beanNames.length == 1) {
return getBean(beanNames[0], requiredType);
}
else if (beanNames.length == 0 && getParentBeanFactory() != null) {
// 从父容器中查找指定的Bean
return getParentBeanFactory().getBean(requiredType);
}
else {
throw new NoSuchBeanDefinitionException(requiredType, "expected single bean but found " +
beanNames.length + ": " + StringUtils.arrayToCommaDelimitedString(beanNames));
}
}
需要在子容器的配置文件中添加:<aop:aspectj-autoproxy proxy-target-class="true" />
或者合并容器的配置,统一由servlet 启动加载。
从父容器中获取Bean 的方法:
org.springframework.beans.factory.BeanFactoryUtils#beansOfTypeIncludingAncestors()
AOP - Aspect Oriented Programming
/**
* 只需要测试类添加@Transactional, 保证所有的测试方式都有事务控制,并且执行完成后自动回滚。
* 参考 https://docs.spring.io/spring-framework/docs/4.3.13.RELEASE/spring-framework-reference/htmlsingle/#testcontext-tx-enabling-transactions
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = TestConfig.class)
@Transactional
public class HibernateUserRepositoryTests {
@Test
public void createUser() {
// track initial state in test database:
final int count = countRowsInTable("user");
User user = new User(...);
repository.save(user);
// Manual flush is required to avoid false positive in test
sessionFactory.getCurrentSession().flush();
assertNumUsers(count + 1);
// 方法执行完成后,insert user 会自动回滚,不会在数据库中添加脏数据
}
}
Spring 采用容器注入的方式,在Junit 测试框架的基础上
,引入TestExecutionListener 的概念,方便拓展自定义的事件。
事务控制就是基于这样的设计,通过实现TestExecutionListener 接口,达到事务特性增强的目的
。
/**
* 支持Spring Test 事务控制实现的TestExecutionListener
*
* @see org.springframework.test.context.transaction.TransactionalTestExecutionListener
*/
public class TransactionalTestExecutionListener extends AbstractTestExecutionListener {
/**
* 如果是以事务的形式运行,那么会在测试方法执行前,开启事务
* 前提条件:有@Transactional 注解,并且容器中配置了TransactionManager。SpringBoot 不用考虑,使用Spring 框架的需要检查下是否有这个配置 <tx:annotation-driven transaction-manager="transactionManager"/>
*/
public void beforeTestMethod(TestContext testContext) throws Exception {
final Method testMethod = testContext.getTestMethod();
// 如果是@NotTransactional,直接跳过
if (testMethod.isAnnotationPresent(NotTransactional.class)) {
return;
}
// 检测当前的方法或者类是否需要事务、是否有配置TransactionManager
PlatformTransactionManager tm = null;
if (transactionAttribute != null) {
tm = getTransactionManager(testContext, transactionAttribute.getQualifier());
}
// 获取到tm,开启事务。按照惯例,缓存事务对象,方便afterTestMethod 进行事务继续操作。
if (tm != null) {
TransactionContext txContext = new TransactionContext(tm, transactionAttribute);
runBeforeTransactionMethods(testContext);
// 复习下开启事务的过程:Check settings、check propagation、Acquire Connection、setAutoCommit(false)、Bind the session holder to the thread
startNewTransaction(testContext, txContext);
this.transactionContextCache.put(testMethod, txContext);
}
}
public void afterTestMethod(TestContext testContext) throws Exception {
Method testMethod = testContext.getTestMethod();
// If the transaction is still active...
TransactionContext txContext = this.transactionContextCache.remove(testMethod);
if (txContext != null && !txContext.transactionStatus.isCompleted()) {
try {
// rollback or commit
endTransaction(testContext, txContext);
}
finally {
runAfterTransactionMethods(testContext);
}
}
}
}
在测试容器启动过程中,会检测当前的测试类是否有指定 TestExecutionListeners。如果没有就采用默认的TestExecutionListeners。
/**
* Determine the default TestExecutionListener classes.
* 默认的TestExecutionListeners:
* ServletTestExecutionListener
* DependencyInjectionTestExecutionListener
* TransactionalTestExecutionListener 就是本人啦!
* @see org.springframework.test.context.TestContextManager
*/
protected Set<Class<? extends TestExecutionListener>> getDefaultTestExecutionListenerClasses() {
Set<Class<? extends TestExecutionListener>> defaultListenerClasses = new LinkedHashSet<>();
for (String className : DEFAULT_TEST_EXECUTION_LISTENER_CLASS_NAMES) {
try {
defaultListenerClasses.add((Class<? extends TestExecutionListener>) getClass().getClassLoader().loadClass(className));
} catch (Throwable t) {
}
}
return defaultListenerClasses;
}
方法注解,不受事务控制
。自定义标识测试的方法是否要回滚
,用来避免默认情况下回滚的操作。在使用Mybatis 查询过程中,会有如下日志打印:
DEBUG com.foo.dao.FooMapper.selectFooList - <== Total: 276
我们知道,Mybatis 只有接口,并不存在日志中的这个类和对应的方法,那么Mybatis 执行日志是怎么打印的?
logger 的name即mappedStatementId,也就是接口名 + 方法名
Mybatis 会在对 Connection、PreparedStatement、ResultSet 进行动态代理
。这样,就会分别打印出需要执行的SQL、SQL入参、结果集等。ConnectionLogger、PreparedStatementLogger、ResultSetLogger
,都是实现了InvocationHandler。既然一个类对应一个代理实现类,那么能不能用静态代理去实现呢?
如果是全部的方法增强,那么可以静态代理,如果是个别方法去增强,那么还是动态代理更加方便和灵活。打印日志只需要对个别的方法进行拦截,在不侵入原有数据库从操作逻辑的前提下,还是动态代理更加合适。/**
* 获取DB Connection
* 如果DEBUG日志级别,会对获取的connection 进行日志增强代理。
* @param statementLog 根据 MappedStatement.getStatementLog() 获得。MappedStatement 初始化过程中,会针对每个Mapper 接口的方法,初始对应的logger,即statementLog = LogFactory.getLog(logId)
*
* @from org.apache.ibatis.executor.BaseExecutor#getConnection
*/
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
// 针对日志DEBUG级别,会对当前的connection 进行代理,通过statementLog 打印日志和传递Logger
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
/**
* JDBC 查询结果集的日志增强实现。
* ResultSetLogger 实现InvocationHandler 接口,拦截 java.sql.ResultSet#next, 对查询结果进行统计和打印
*/
public final class ResultSetLogger extends BaseJdbcLogger implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
Object o = method.invoke(rs, params);
// 拦截next() 方法,进行row count统计和结果数据打印。
if ("next".equals(method.getName())) {
if (((Boolean) o)) {
rows++;
// 如果应用的日志级别为TRACE, Mybatis 会详细的打印出ResultSet 的所有返回数据。
if (isTraceEnabled()) {
ResultSetMetaData rsmd = rs.getMetaData();
final int columnCount = rsmd.getColumnCount();
if (first) {
first = false;
printColumnHeaders(rsmd, columnCount);
}
printColumnValues(columnCount);
}
} else {
// 结果集遍历完后,打印 Total 信息,解答本文的疑问。
debug(" Total: " + rows, false);
}
}
clearColumnInfo();
return o;
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<properties>
<property name="patternlayout">%d{HH:mm:ss} [%t] %p %c - %m%n</property>
</properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="${patternlayout}"/>
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="Console"/>
</Root>
<!-- name对应Mapper 所在的包路径下 -->
<Logger name="com.foo.dao" level="TRACE" additivity="true"/>
</Loggers>
</Configuration>
<!--
如下的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);
}