`
zhongxiucheng
  • 浏览: 68319 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

iBatis3架构分析

 
阅读更多
关于执行器包executor与其子包下的所有文件,是整个框架非常核心的部分,在代码重构之后,结构要比以前好很多
整个框架在jdbc的执行上除去那些辅助代码:如配置文件解析(从配置文件构建已映射语句及其它一些属性);
缓存系统;反射工具;数据源等,最终那些与jdbc执行相关的代码并不是太多,也不是很复杂,至少重构之后我认为
较之以前要好理解多了.
整个框架从上层到下层我认为主要可以分为以下几个层次:
A.session层:
这一层是与程序离的最近的一个层次,程序员在要求框架执行持久化操作时,就是在这个层次上
进行调用的,它的主要作用就是为程序员规范了一个接口,让所有程序以一个统一的规范来使用框架,但是执行
jdbc操作的并不是由这一层来完成,这个层次在完成操作时是委托一个Executor来完成的,即下一个层次.
对于这一个层还要明确一点,就是每一个线程执行操作都应该创建一个新的session对象,而不是共享一个session
对象,这个对象类似于hibernate的session是不支持并发的
B.executor层:
这一层对session的接口进行了归类处理,使大部分相似的操作可以共享代码,在这个层次框架做的
主要工作是什么呢?通过分析可以看到在这个层次的代码主要被抽象出了二级:第一级是一个抽象类级别主要用于缓存处理
与延迟加载,这个抽象类是每种执行器需要共享的代码;第二级是具体的某一种类型的执行器(共三种),具体选择那一种,与创建session
时所指定的参数有关系,如果没有指定参数,则会使用Configuration中配置的默认类型进行executor对象的创建

它将session的功能进行相似共享,最后抽象出了三个方法:即doUpdate,doFlushStatements,doQuery
第一个具体的子类就是要完成这三个操作,子类完成三个操作的过程都是有规律,它们总是委托一个名叫StatementHandler
处理器去完成实际的操作,那么StatementHandler又是如何完成操作的呢?
C.StatementHandler:
这一层基本是对jdbc操作的"八股文"抽象,可这么说这个层次是与jdbc操作最贴近的地方,你可以想象
一下,你在执行jdbc操作的时候,你会分几步来做:1.创建statement;2.设置statement参数;3.执行statement语句获取结果
这基本是关于statement对象处理的最重要的三步.至于之前connection创建与之后的statement释放不再抽取之列.
StatementHandler在框架中也分为二级:第一级为一个抽象类,主要完成一些基础性工作,如规范创建流程,超时等设置;
第二级主要就是实现上述的1,2,3三个主要操作,第二级的实现类主要分为Simple,Prepared,Callable,具体创建使用哪一个对象
是由Statement配置的statementType有关系,没有配置默认是Prepared

上述的三个操作可以说是jdbc操作最复杂,最繁琐的地方,尤其是后两个
设置参数与执行语句后获取结果,可以说这是要所有持久化框架必须要面临的一个问题,这里使用两个接口来抽象这个变化点
ParameterHandler与ResultSetHandler其中一个主要做参数设置,而另一个主要做结果提取,它们可以算得上是下一个层次


到此处有必须分析一下executor与StatementHandler搭配关系.executor主要有:SimpleExecutor,ReuseExecutor,BatchExecutor,CachingExecutor
其中最主要的是前三个;而对于StatementHandler主要有:SimpleStatementHandler,PreparedStatementHandler,CallableStatementHandler,
RoutingStatementHandler,其中最主要的也是前三个,从理论上讲它们之间是没有严格的约束关系的,它们是相容的,但实际它们之间的搭配关系也不是随便的

具体分析一下:
SimpleExecutor:是一个中规中矩的执行器,没有什么特别的功能,它实现的功能就是,委托StatementHandler来完成操作,例如更新,委托StatementHandler
设置参数,委托StatementHandler执行更新;同样查询,也是委托StatementHandler设置参数,委托StatementHandler执行查询.但是这个执行器不支持
doFlushStatements方法,它仅仅只返回一个空的列表,此方法主要是为批量执行器BatchExecutor预备的

BatchExecutor:是一个批量执行器,它主要着眼于更新,即批量的更新,这是它最大的作用,要实现这个功能必须要两个方法联合使用,即更新与doFlushStatements
更新doUpdate方法主要负责添加批量的语句与设置参数,而doFlushStatements主要负责一次所有操作,而对于程序员来说doFlushStatements是不可见的,因为
它会在session层进行事物提交时进行自动的执行此方法,以完成批量方法的实际执行,session实际也是委托BatchExecutor的commit方法来调用doFlushStatements
至于查询BatchExecutor与SimpleExecutor实际基本相同,只是BatchExecutor在执行查询前会先执行一次批量更新doFlushStatements,其它都一样

ReuseExecutor:被称为重复使用执行器,这个执行器与SimpleExecutor非常的相似,它唯一与SimpleExecutor不同的它缓存Statement对象,并对重复出现的SQL使用
缓存的Statement对象,前提是session还没有关闭.

CachingExecutor:暂时还不能体会它含义,只知道它是对一种执行器进行的缓存装饰,具体在什么情况下使用还不清楚
=================================================================================================
SimpleStatementHandler:简单语句处理器,此处理器只用来处理不带占位符参数的语句,这是它最大的特点,因为它设置参数方法不做任何情
它可以和各种类型的Executor进行搭配

PreparedStatementHandler:参数语句处理器,这是最常见的处理器,也是默认的语句处理器,它可以和各种类型的Executor进行搭配

CallableStatementHandler:存储过程语句处理器,这个处理器也只有调用存储过程时才用得上,其实它也可以与各种类型的Executor进行搭配,
但我认为最还是与简单类型执行器进行搭配,

综合上面的分析,在执行器当中,如果没有特别的理由我觉得就使用SimpleExecutor,如果需要批量更新,必须使用BatchExecutor
在语句处理器当中,如果没有参数可以使用SimpleStatementHandler,当然也可使用PreparedStatementHandler,如果是存在过程
语句必须使用CallableStatementHandler,其它情况使用PreparedStatementHandler,也就是说
SimpleExecutor+PreparedStatementHandler是最常见的搭配

还有一点需要注意的就是执行器是程序员可以临时选择的,而语句处理器是事先配置好,不可在程序进行更改,也就是语句处理器受语句类型约束
可以事先确定,而执行器的选择需要根据实际执行业务的情况进行选择,总体来说如果不进行大量数据批量执行一般都需要选择SimpleExecutor,
它也是iBatis的默认执行器选择,最最重要的一点是无论选择是哪一种如果全局配置是可缓存的,则无论哪一种执行器都会被CachingExecutor进行
装饰这一点一定要清楚

D.ParameterHandler与ResultSetHandler
先说ParameterHandler,它主要是为statement参数设置而设计,作为上层的调用通常只会传递一个statement进来,那么一个处理器
是如何完成参数设置的呢?默认的参数处理器是这样的处理的,一个ParameterHandler通常会持有一个boundSql,而在boundSql中
有当前要执行的sql语句的参数映射列表:List<ParameterMapping> parameterMappings,这个列表记录当前执行操作每个参数
设置的细节,列表中的一个元素,完成一个参数的设置.此处有一个很重要的问题List<ParameterMapping>这个列表是从何处而来,又是如何
与此操作联系在一起的,下面将进行详细的分析:

首先看一看ParameterHandler是如何来的,它是由BaseStaementHandler在构造方法中创建的,创建代码如下:
this.boundSql = mappedStatement.getBoundSql(parameterObject);
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowOffset, rowLimit, parameterHandler, resultHandler, boundSql);
从上面的代码你可以看出boundSql是由"映射语句"创建的,而ParameterHandler与ResultSetHandler都是由配置对象创建的
问题的焦点现在变成了"映射语句"是如何创建一个BoundSql的?因为每一个参数的设置细节是由此对象来完成的,而往往在一变化的
sql操作中参数的个数有可能事先并不确定,那么"映射语句"是如何来解决这件事情的呢?

再来看一看MappedStatement,MappedStatement是一个独立的类型,它没有继承其它类,与没有实现其它接口,这个类保留了非常详细的信息
如:语句配置时的id,资源地址,配置对象,fetchSize,timeout,statementType,resultSetType,cache,sqlSource,parameterMap
resultMaps等参数,其中一个参数是创建BoundSql的关键,这个参数是SqlSource,这个参数是一个接口,其源代码如下:

/**
* 一个sql源,总是可以通过一个提供参数的对象,来提供一个被绑定的sql
*
* @author ZXC
*
*/
public interface SqlSource {

BoundSql getBoundSql(Object parameterObject);

}

可以看到这个接口是专门为抽取BoundSql的变化而设计,每一个"已映射语句"对象都会有一个SqlSource,这是一个非常合理的设计.在"已映射语句"对象
中获取BoundSql的逻辑的为:
public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.size() <= 0) {
boundSql = new BoundSql(boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
return boundSql;
}

现在继续向下进行分析,"已映射语句"中的sqlSource与parameterMap是从哪里来的?
它是通过辅助类MapperBuilderAssistant的辅助方法:
public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType,
SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap,
Class parameterType, String resultMap, Class resultType, ResultSetType resultSetType,
boolean flushCache, boolean useCache, KeyGenerator keyGenerator, String keyProperty)
创建获取的,其中传递的参数有SqlSource与parameterMap,这是对参数设置起决定作用的两个重要参数,它们两个都可以提供参数设置的细节
即它们都有可能提供一个参数映射列表:List<ParameterMapping> parameterMappings,并且从代码中可以看出,只有在SqlSource提供
的boundSql没有List<ParameterMapping> parameterMappings时才会使用parameterMap提供的getParameterMappings来重新
构建一个BoundSql返回,那么在什么样情况下BoundSql提供的parameterMappings会不存在呢?现在我还不知道,但是我推测应该是没有使用
动态标记#{XX}的那些语句,一旦使用这个标记,总会生成parameterMappings,如果没有使用动态标记,但是使用了?我想一定要手工配置一个parameterMap
否则肯定会报异常,也就是说要不就全部配置?占位符,然后配置一个parameterMap;要么就全部配置动态占位符#{XX}而parameterMap将不再起作用


从上面辅助方法可以看parameterMap参数此时还是一个String类型的参数,而当一个MappedStatement对象创建后它就变成了一个ParameterMap
对象了,那这是如何变化的呢?
在辅助类中有一个方法setStatementParameterMap(String parameterMap, Class parameterTypeClass, MappedStatement.Builder statementBuilder)
主要就是来完成这个工作的,从这里可以看到这个方法除了第三个参数构建器之外还有两个参数parameterMap与parameterTypeClass,此时可以联想
到ibatis的配置文件,我想这两个参数应该是与映射文件配置语句听参数是对应的,这两个参数是有优先级的,如果parameterMap配置了,则parameterTypeClass就不会起作用
如果没有配置parameterMap则会使用parameterTypeClass创建一个空的parameterMap即其中的映射项List<ParameterMapping> parameterMappings是一个空列表
到此为止基本已经说清了parameterMap的形成过程了,下面继续看SqlSource来源

正常的sqlSource产生的途径主要在XMLStatementBuilder中的parseStatementNode方法,它主要用来解析"已映射语句"在配置文件
中对应的结点配置,最终也将会在此方法中产生一个sqlSource对象,详细分析可以查看XMLStatementBuilder中的相关注释以及TypeHandler接口中的注释.最后得出一个
比较粗糙的结果就是:sqlSource可以通过动态计算,来提供一个BoundSql,而BoundSql可以提供一个List<ParameterMapping> parameterMappings,
在一个"已映射语句"中sqlSource是稳定的,也就是它是被缓存的,但是由它产生的BoundSql是一即时对象,即每次操作都产生一个新的BoundSql,
当获取一个List<ParameterMapping> parameterMappings之后要设置参数就非常的简单了,只需要委托TypeHandler就可以完成相应的参数设置工作

//===================================================================================
看完成参数设置处理,接下来看一看结果集的处理在框架是如何抽象的.在TypeHandler接口的分析中(可以参考其中的文档注释),我们可以知道
TypeHandler从一个低层次上既抽象单个字段的参数设置,也抽象了单个字段结果提取,由于级别低所相对操作就比较繁琐,框架在一个较高的层次
进行了进一步的抽象参数设置即是前面分析的ParameterHandler,而结果提取就是ResultSetHandler,下面就具体看框架是如何抽象结果处理的

与ParameterHandler一样,ResultSetHandler也是从BaseStatementHandler创建而来的:
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowOffset, rowLimit, parameterHandler, resultHandler, boundSql);
一个StatementHandler通常做得事情很结构化,也很简单,实际复杂的事情都委托参数处理器与结果处理器去完成了,比如当前一个statement执行查询
后,要提取结果,其中一种StatementHandler实现的代码为:
public List query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}

现在我们就具体分析一下resultSetHandler究竟是如何完成结果处理的,查看框架的ResultSetHandler的默认实现,从整个代码上看,结果处理或者说
结果提取,要比较参数设置要复杂许多,两者是各有特点,参数设置是对象的创建非常复杂,而实际参数设置简单,因为参数动态性较大;而结果提取是对象创建
相对简单,而结果处理的本身相对复杂.通过分析可以发现,默认的ResultSetHandler进行了一些相对比较高级的处理,其中一个比较繁琐的地方是关联查询
我们先暂时将这个比较复杂的地方去除,整个流程还是比较清楚的:

第一步是将多结果集的问题转换成单一结果集问题(handleResults),在解决这个问题的时候有一个选择ResultHandler的问题,ResultHandler与ResultSetHandler不是同
一回事,ResultSetHandler是处理整个Statement,而ResultHandler处理的是一个记录行映射之后的结果上下文,代码为:void handleResult(ResultContext context);
第二步在单一结果集中处理中首先处理了分页逻辑后,接着将问题的核心从处理多条记录转向了处理一条记录(loadResultObject);
第三步在处理一条记录的逻辑中主要解决了一条记录对象中某个关联对象的延迟加载,以及连接查询,但其中最核心的逻辑留给了下一级的调用处理(mapResults);
第四步是一个纯粹的结果映射,不带有任何的其它逻辑,这也是我们当前最关心的问题,在结果映射中主要分两个比较重要的逻辑,就是以结果对象的类型是否为Map型有
了个比较明显的界限,其中的一个细节是结果对象是否存在一个columnLabel属性,这里的columnLabel是数据库列字段的大写形式,如果存在则会在自动结果映射中添加
一个这样的映射对象,而在Map型对象在进行元信息包装的时候,它们默认是具有任何属性的.

通过上面的分析基本上可以确定,如果在配置中没有指定的一个ResultMap,但只要映射对象是一个Map类型,仍然可以获取完整的数据.

接下来我们需要分析一下从外部配置的ResultMap产生的途径:
一个ResultMap总是跟随着一个"已映射语句"的,一个ResultMap在一个"已映射语句"中表现为一个列表:private List<ResultMap>resultMaps;
这是因为一个"已映射语句"在执行完成之后有可能会产生多个结果集,而第一个结果集,需要一个ResultMap,那么这个结果集列表是如何得来的呢?
在构建一个MappedStatement同时也会构建一个ResultMap的列表,下面是在MapperBuilderAssistant类中一段相关的代码

private void setStatementResultMap(String resultMap, Class resultType, ResultSetType resultSetType, MappedStatement.Builder statementBuilder) {
resultMap = applyCurrentNamespace(resultMap);

List<ResultMap> resultMaps = new ArrayList<ResultMap>();
if (resultMap != null) {
String[] resultMapNames = resultMap.split(",");
for (String resultMapName : resultMapNames) {
resultMaps.add(configuration.getResultMap(resultMapName.trim()));
}
} else if (resultType != null) {
ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(configuration, statementBuilder.id() + "-Inline", resultType, new ArrayList<ResultMapping>());
resultMaps.add(inlineResultMapBuilder.build());
}
statementBuilder.resultMaps(resultMaps);

statementBuilder.resultSetType(resultSetType);
}

从上面的代码可以看出,如果配置过resultMap则会使用配置的结果映射,如果没有配置,则退而求其次如果配置了resultType,则会使用此对象创建一个
内建的ResultMap对象,但是此对象有一个问题就是它的映射字段列表为一个空列表.如果两个都没有配置则将不会有任何的ResultMap存在,这是没有意义的
我觉得程序应该在这种情况下抛出异常(这里只所没有抛出异常我推断是因为在配置文件,type是一个必须填写的属性).

接着我们需要来看一下如果只配置resultType框架是如何解决这种信息不全的问题的,这其中的秘密就是对象的元信息包装
metaResultObject.findProperty(columnLabel);在元信息包装的时候,元信息类记录一个完全大写的虚属性到实际属性的映射的关系
这也是在使用findProperty方法而参数是一个完全大写的columnLabel时也能够正确获取一个属性名称的秘密,下面是在Reflector类中的一段代码:
for (String propName : readablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(), propName);
}
for (String propName : writeablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(), propName);
}

可以看到caseInsensitivePropertyMap会保存着一个完全大写到真实属性的一个映射关系,这在一般情况不会有太大的问题,它的确找到了一个从数据列字段
名称到属性名称间对应的途径,这可以使我们自动建立一个映射,而且还可以正常使用,至于属性冲突的问题,只要在设计时稍加注意就可以避免这样的问题,退一步
说,就是算是有冲突也没有太大的关系,因为手动配置优先于,自动映射,所以即使发生冲突我们也可以通常手工配置来解决这个问题

//==================================================================
以上基本上从最高层到最低层进行了一个很基本的分析,对于一些高级的功能并没有考虑太多,但是对于设置设置与结果映射应该
说做了相对深入的分析,至于其它的一些高级功能在以有时间在进行分析,或在用得着的时候再去看它



//==================================================================
以上几个重要的层,session的创建不Configuration对象创建的,其它几个对象都是由Configuration
对象通过对应的new前缀方法进行创建的
分享到:
评论
1 楼 mojunbin 2012-05-16  
看得有点胡乱,呵呵..

相关推荐

    深入分析 iBATIS 框架之系统架构与映射原理

    深入分析 iBATIS 框架之系统架构与映射原理深入分析 iBATIS 框架之系统架构与映射原理深入分析 iBATIS 框架之系统架构与映射原理深入分析 iBATIS 框架之系统架构与映射原理

    iBATIS实战.pdf

    内容简介 《iBATIS实战》是讲述iBATIS框架的...项目经理、数据库管理员、质量保证员与测试员以及系统分析师也能从《iBATIS实战》中受益。 作者简介 作者:(加拿大)Clinton Begin (加拿大)Brandon Goodin 译者:叶俊

    iBATIS实战 iBATIS In Action PDF Part 3/3

    iBATIS实战 iBATIS In Action PDF...本书既可为广大的开发人员(不仅仅是Web应用程序开发人员)提供指导,也可为架构师的项目决策提供参考。项目经理、数据库管理员、质量保证员与测试员以及系统分析师也能从本书中受益。

    iBATIS实战 iBATIS In Action PDF Part 1/3

    iBATIS实战 iBATIS In Action PDF...本书既可为广大的开发人员(不仅仅是Web应用程序开发人员)提供指导,也可为架构师的项目决策提供参考。项目经理、数据库管理员、质量保证员与测试员以及系统分析师也能从本书中受益。

    iBATIS实战 iBATIS In Action PDF Part 2/3

    iBATIS实战 iBATIS In Action PDF...本书既可为广大的开发人员(不仅仅是Web应用程序开发人员)提供指导,也可为架构师的项目决策提供参考。项目经理、数据库管理员、质量保证员与测试员以及系统分析师也能从本书中受益。

    iBATIS实战

    1.1.1 探索iBATIS的根源 3 1.1.2 理解iBATIS的优势 7 1.2 iBATIS适合应用在何处 10 1.2.1 业务对象模型 11 1.2.2 表现层 11 1.2.3 业务逻辑层 12 1.2.4 持久层 13 1.2.5 关系数据库 15 1.3 使用不同类型的数据库 17 ...

    ibatis实战_带书签版part2

    本书是讲述iBATIS框架的权威著作... 本书既可为广大的开发人员(不仅仅是Web应用程序开发人员)提供指导,也可为架构师的项目决策提供参考。项目经理、数据库管理员、质量保证员与测试员以及系统分析师也能从本书中受益。

    iBATIS实战—带书签版part1

    本书是讲述iBATIS框架的权威著作... 本书既可为广大的开发人员(不仅仅是Web应用程序开发人员)提供指导,也可为架构师的项目决策提供参考。项目经理、数据库管理员、质量保证员与测试员以及系统分析师也能从本书中受益。

    架构师考试-快速通关-知识点

    分析-Hibernate和iBatis 分析-内存数据库和关系数据库 分析-数据持久层定义及优点 分析-数据持久层技术分类 分析-文件系统和关系型数据库 分析-状态图和活动图定义与区别 关系模式 关系运算-比较 管理信息系统规划的...

    深入分析Java Web技术内幕 修订版

    《深入分析Java Web技术内幕(修订版)》不仅介绍这些技术和框架的工作原理,而且结合示例来讲解,通过通俗易懂的文字和丰富、生动的配图,让读者充分并深入理解它们的内部工作原理,同时还结合了设计模式来介绍这些...

    基于大数据聚类分析的电网信息化运维系统设计

    该系统整体框架采用ExtJs+Spring+iBatis架构,硬件包括:感知集成芯片、数据传输设备、数据融合处理器、中央控制器、可视化设备。软件运行利用集成芯片将相关电网数据抽取出来,传输到数据融合处理器中,基于聚类...

    深入分析Java Web技术内幕 修订版.pdf

    主要围绕Java Web 相关技术从三方面...最后介绍了Java 服务端技术,主要包括Servlet、Session 与Cookie、Tomcat 与Jetty服务器、Spring 容器、iBatis 框架和Velocity 框架等原理介绍,并介绍了服务端的一些优化技术。

    最新Java面试题视频网盘,Java面试题84集、java面试专属及面试必问课程

    微服务架构之Spring Cloud Eureka 场景分析与实战.wmv │ │ │ ├─11.高性能必学之Mysql主从架构实践 │ │ 11.高性能必学之Mysql主从架构实践.mp4 │ │ │ ├─12.架构师不得不知道的Spring事物不能回滚的深...

    .net源码生成工具DataBase2Sharp

     3、PetShop架构代码的生成(多种形式),直接生成所需的解决方案,为你封装了功能强大的基础类库和数据访问基类。  4、Web界面自动生成功能,生成列表、增加、修改界面代码和后台代码,包括烦琐的绑定数据和赋值...

    洗煤厂煤质信息管理系统框架的研究

    在此基础上搭建了洗煤厂煤质信息管理系统架构,采用FreeMarker作为页面,Webwork控制业务和分发请求,Ibatis作为数据访问和持久化,通过Spring进行整合,并通过一个具体实例进行了分析。试运行结果表明洗煤厂煤质管理系统...

    工作流引擎 Osworkflow 及其持久化机制的研究

    由OpensymPhony组织开发的开源工作流引擎 OSWorkflow是一种非常灵活的工作 流引擎。它主要基于有限状态机理论,通过状态的迁移描述...库表结构和内部接口,最后,提出了通过 ibatis实现 OSWorkflow持久化的具体解决方案

    阿里巴巴编码规范 基础技能认证 考题分析(考题+答案).docx

    B .iBATIS自带的queryForList(String statementName,int start,int size)分页接口有性能隐患,不允许使用。 C .定义明确的sql查询语句,通过传入参数start和size来实现分页逻辑。 D .可使用存储过程写分页逻辑...

    研磨设计模式 PDF ZIP.001(三个压缩包)

    陈臣:十年Java/JavaEE开发经验,高级系统架构师,功力深厚,技术精湛,精通Java/JavaEE相关技术和多种开源框架,尤其擅长系统分析和架构设计。从事过专业的中间件研发,包括基于组件的Web页面框架、基于WFMC的工作...

    研磨设计模式 PDF ZIP.002(三个压缩包)

    陈臣:十年Java/JavaEE开发经验,高级系统架构师,功力深厚,技术精湛,精通Java/JavaEE相关技术和多种开源框架,尤其擅长系统分析和架构设计。从事过专业的中间件研发,包括基于组件的Web页面框架、基于WFMC的工作...

Global site tag (gtag.js) - Google Analytics