前言
PE拥有自己的web框架,下面具体分析PE-MVC。
基本流程
请求从JSP/Servlet容器出发到前端业务逻辑处理的流程如下:
MainServlet–>MainController–>CoreController–>Chain–>Template–>Action
MainServlet
MainServlet是通过MainServletRegistry类来完成注册的,MainServletRegistry主要做了以下两个工作:
- 注册MainServlet,映射到“/”,这样它会作为默认的servlet。
- 注册静态资源映射。
MainServlet组合了以下对象:
- multipartResolver multipart解析器
- localeResolver locale解析器
- viewResolver 视图解析器
- mainController 主控制器
MainServlet的主要工作是:
- 将请求委托给mainController处理
- 解析视图,由视图对象完成视图的渲染和返回。
MainController
MainController做的是HTTP Adapter工作,将HTTP请求转换为与请求来源无关的context,其组合了以下对象:
- idResolver transactionId解析器
- contextResolver 上下文解析器
- exceptionHandler 异常处理
- coreController 逻辑处理,责任链入口
MainController的主要工作是:
- 将HTTP请求解析为Context
- 委托coreController完成逻辑处理
- 返回model数据
- 若发生异常委托exceptionHandler处理
从Servlet容器到这里就基本是PE的MVC部分。与SpringMVC类比,MainServlet可以看做DispatchServlet,MainController可以看作映射到/*.do
的Controller。
功能详解
Controller
PE-MVC中的“Controller”(指MVC中的Controller)并没有SpringMVC中那样的灵活,PE-MVC中所有请求都是以xxx.do
的末尾路径,而xxx表示一个transaction
(交易),交易的最终执行逻辑单元即为action
(动作),通常一个交易映射一个动作。因此可以将action类比SpringMVC中的Controller。
一个常见的交易配置信息如下,PE-MVC通过transaction指定了请求路径中的xxx,指定了action,指定了可以接受的请求参数及其校验方式,指定了通道及其视图。
<transaction id="QueryList" template="queryTemplate"><!--id值即为xxx--> <actions><!--动作--> <ref name="action">QueryListAction</ref> </actions> <fields><!--字段及其校验方式--> <field name="Seq"></field> </fields> <channels><!--通道--> <channel type="http"> <param name="success">area/QueryList</param> </channel> <channel type="WX"> <param name="success">json,</param> </channel> <channel type="PMBS"> <param name="success">json,</param> </channel> </channels> </transaction>
可以看到,这是一种格式很固定的交易流风格,后面将对transaction进行分析。
multipart支持
multipart形式的数据,指的就是用户的上传行为。
Multipart解析器
MainServlet将解析multipart请求数据的任务委托给Spring的MultipartResolver
接口的实现,将请求转换为MultipartHttpServletRequest
。
if(multipartResolver != null && multipartResolver.isMultipart(request)){ if(request instanceof MultipartHttpServletRequest) //请求已被解析为Multipart else try{ processedRequest = multipartResolver.resolveMultipart(request); } ... }
Multipart配置信息
PE框架中使用的Servlet版本很古老,自然是不支持Servlet3.0对multipart的支持。
由于采取自定义DTD,单纯从配置信息无法看到其实现类,实际上其采用的是CommonsMultipartResolver
:
<multipartResolver> <param name="defaultEncoding">GBK</param> <param name="uploadTempDir">${uploadTempDir}</param> <param name="maxUploadSize">${maxUploadSize}</param> </multipartResolver>
通过自定义dtd,对框架的重要bean采用自定义标签而非通用的Spring标签。略去实现类等关键信息,只留下支离破碎的没有文档解释的配置信息,这是一种很糟糕的封装方式。
处理multipart请求
在业务逻辑代码中,只需要使用multipart的抽象即可,很简单:
MultipartFile mfile = (MultipartFile) context.getData("ContractPhoto");
国际化
Local解析
在MainServlet中,通过名为_locale
的请求参数提取local信息,然后获取Locale对象,并将其存储于session中。
其解析器为:
org.springframework.web.servlet.i18n.SessionLocaleResolver
消息
PE采用的是Spring的MessageSource接口。信息主要分为以下几个大类,一般配合自定义JSP标签使用:
- checkmsg 用于自定义错误信息
- consmsg 用于前端字段自定义显示,如下拉列表选项
- dictionary 字典,用于校验错误的信息展示
异常处理
在web开发中,如何将请求处理过程中抛出的异常加以区分,并合理的返回给客户端是很重要的。这有着两方面的作用:
- 提升用户友好度,合理的错误展示让用户能清晰理解
- 系统安全,防止内部错误泄露
在PE框架的ExceptionHandler类中,异常的处理方式为:
1.请求返回类型为json,解析异常消息,返回json视图
if(request.getHeader("Accept").contains("application/json")){ model = new HashMap(); resolverRejectMessages(messageSource, request, locale, ex, context, model); request.setAttribute("_viewReferer", defaultJsonErrorView); return model; }
2.异常为验证错误,解析异常信息,返回提交视图
if(backToInputForValidationError && (ex instanceof ValidationMessage)){ //... }
3.解析异常信息,存在提交页则返回提交页,否则返回公共错误视图
if(viewName == null){ if(context != null){ User user = context.getUser(); if(user == null || user.isLogout()) request.setAttribute("_viewReferer", defaultPublicErrorView); else request.setAttribute("_viewReferer", defaultErrorView); } else{ request.setAttribute("_viewReferer", defaultErrorView); } } else{ request.setAttribute("_viewReferer", viewName); }
无论怎么处理,都需要对异常内容进行解析,通过message的映射,实现国际化以及良好的展示。这里有两个点:
- messageKey的提取,通过特定接口
Messageable
的getMessageKey()
或Exception的getMessage()
来获取 - placeholder的填充
在业务逻辑代码中,如果验证性错误(前端提交数据有误)或需要向前端反馈的错误,可以使用ValidationRuntimeException
来承载可解析异常,而且因为是RuntimeException
,也不需要一层层的try-catch块。
throw new ValidationRuntimeException(CHECKMSG.EXECUTE_SHELL_FAILED);//文件传输失败
java代码中的messageKey可以通过一个工具类CHECKMSG
来维护。
public class CHECKMSG { public static final String VALIDAATION_CHECK_TRANSTIME="Validaation.check.TransTime"; //不在当日交易时间内 ... }
视图
视图解析器
PE中的ViewResolver类比SpringMVC中的ViewResolvers,本质是一个map:
<bean id="mainViewResolver" class="com.csii.pe.channel.http.servlet.HashMapViewResolver"> <map name="mapping"> <bean name="servlet" class="com.csii.pe.channel.http.servlet.UrlView"> <ref name="dynamicWebModuleRegistry">WebModuleRegistry</ref> <param name="cacheSeconds">0</param> <param name="prefix"></param> <param name="suffix">.do</param> <param name="localeMode">0</param> <param name="clientType">false</param> </bean> </map> </bean>
MainServlet中根据model属性_viewReferer
得到视图名来解析视图,并完成渲染:
void render(Object model, HttpServletRequest request, HttpServletResponse response, Locale locale){ String viewName = (String)request.getAttribute("_viewReferer"); String splittedViewName[] = resolverViewResolverName(viewName); CsiiView view = viewResolver.resolveView(splittedViewName[0]); if(view != null) view.render(splittedViewName[1], model, locale, request, response); }
视图
PE中的View类比SpringMVC中的ViewResolver与View的结合体,既需要查找是否能够渲染视图,又需要执行渲染逻辑。
<bean name="default" class="com.csii.pe.channel.http.servlet.UrlView"> <param name="usingDeviceClass">false</param> <ref name="dynamicWebModuleRegistry">WebModuleRegistry</ref> <param name="cacheSeconds">0</param> <param name="prefix">/WEB-INF/</param> <param name="suffix">.jsp</param> <param name="forceJavaScriptDisabled">true</param> </bean>
针对JSP、下载、重定向、json、文件(pdf、xls)等等都提供了视图,但由于糅合了查找和渲染的功能,因此总体逻辑较乱。
JSP标签
PE的主要采用JSP视图,因此提供了许多自定义标签,由于文档不全,曾经我花了一段时间去分析标签的功能及实现,后来发现根本没有意义。JSP提供的标签技术简单来说三类:老的旧接口、简化的新接口、标签文件,PE采用的是简化的新接口。
总的来说,JSP+标签的模板技术易用性相比Velocity,Thymeleaf等现代模板技术来说,还有很大不足。
REST支持与内容协商
PE-MVC对REST几乎没有任何支持,除了可以简单的生成JSON格式视图外。PE-MVC将JSON视图与请求渠道(手机银行,微信,PMBS)进行粗糙的绑定,通过名为_ChannleId
的请求属性来确定请求所属来源。
在PE-MVC中所有请求都是以交易的方式存在,通常无论是browser还是phone的请求都路由到同一个交易action中,导致JSON视图与html视图共享的相同的model数据,只是表现形式不同。换种话说,就是PE-MVC对json视图的支持是简单的把html视图的model进行json序列化而已,是很粗糙的方式。从最佳实践来讲,提供独立的REST api很有必要,因为它们需要的数据不同。
总结
PE-MVC是为PE框架后续流程服务的,是browser,mobile client,wx gateway的访问入口。由于历史久远,同时没有本质的更新迭代,因此相比文档完整、功能完全、注解驱动的SpringMVC,SpringBoot等开源框架有着很大的不足。比如视图仅支持jsp,json,vx而不支持许多开源模板如FreeMarker、Velocity等,异常处理不支持HTTP状态码映射,不支持Restful,不支持路径参数…代码质量也一般,抽象和可扩展性不足,组件有的采用配置而有的采用硬编码,封装层次过深…
对于新手而言,我不建议在pe-mvc框架上花太多时间。而建议深入学习SpringMVC,学习WEB MVC中的核心思想,这是PE-MVC提供不了的。
PE-MVC是10多年前为银行量身打造的业务框架,我不认为这样一个业务型框架能像SpringMVC一样通用,我曾经在其上开发P2P,也听闻前同事在其上开发商城,我认为都是不合适的:功能不全、效率低下。非网银产品,我建议丢弃PE-MVC直接采用开源方案。