# 动态接口
# 后端Bean替换机制和示例
# 支持场景
Bean替换是最通用的无侵入源码进行二次开发的能力,Java后台Controller、Manager、Dao任何一层都支持Bean替换。
Bean替换简单说就是自己新增一个类,extends标准产品的Controller、Manager、Dao任何一个实现类,Override重写标准产品父类的方法,这样最终代码逻辑就会走二次开发自己新增的类里面去。
Bean替换的支持场景就非常多:
1、客户需要修改某个前端JSP页面的显示效果,客开为了保证不动标准产品JSP源码,可以采用Bean替换后端的Controller,将原计划ModelAndView重定向到标准产品JSP页面的方法重写,重定向到客开的JSP页面。
2、客户需要调整前端某个页面的列表显示内容,此列表的数据是通过Dao层SQL查询出来的。客开为了保证不动标准产品Dao源码,可以采用Bean替换后端的Dao实现类,重写原本获取数据的SQL方法,改为客户期望的规则输出到前端。
3、同样客开发现标准产品的Manager类某个方法不符合客户需求,需要全面重写,则可以采用Bean替换对应Manager实现类,Override重写标准产品方法即可。
# 实现步骤
1、按照插件化开发规则,新增一个独立客开插件工程
2、新插件工程下新建一个实现类,extends自己想要替换的任何一个Controller、Manager、Dao
3、然后通过Override重写父类中的方法,实现客户化开发需要的逻辑
4、最后将新建的实现类注册到spring容器中,随后系统运行时将跳过父类直接调用Bean替换的实现类。
5、将新的客开插件打包发布到标准产品,重启OA
# 开发示例
场景描述: 客户需要大改某个前端页面的交互逻辑,经过客开分析发现客开的前端页面是个capRunningLog.jsp,通过springmvc跳转,代码如下。
public class CapLogController extends BaseController {
@Override
public ModelAndView index(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mav = new ModelAndView("cap4/monitor/capRunningLog");
List<Enums.CapLogType> capLogTypeList = Enums.CapLogType.getLogTypeListByOwnerType();
mav.addObject("capLogType", JSONUtil.toJSONString(capLogTypeMapList));
return mav;
}
}
<!-- cap4日志Bean XML注册信息 -->
<bean name="/cap4/capLog.do" class="com.seeyon.cap4.form.modules.log.CapLogController"/>
由于页面改动太大,等同于重新编写一个页面,故可以采用替代capRunningLog.jsp的方案来开发,实现逻辑是:
- 基于capRunningLog.jsp复制一个新的jsp文件名为:kkcapRunningLog.jsp(名称可以自己定义,存放目录也可以自己定义)
- 在kkcapRunningLog.jsp里面修改代码,实现客户的前端交互逻辑
- 采用Bean替换方式继承CapLogController并重写index方法,将ModelAndView由标准产品的capRunningLog修改为客开的kkcapRunningLog
1)第一步复制并编写kkcapRunningLog.jsp页面这里略过
2)客开按照插件化开发规则,新增或复用一个与标准产品无关的客开插件,在插件化开发框架下完成如下代码实现:
- 新增一个客开类KKCapLogController.java(类名可以自定义),此类extends标准产品的CapLogController.java
- 在客开类的顶部增加@Replace注解,beanName一定是标准产品CapLogController在XML中注册的名字
- 在客开类中,通过@Override注解重写index方法,再将里面的跳转路径改为客开自己的JSP
@Replace(beanName = "/cap4/capLog.do")
public class KKCapLogController extends CapLogController{
@Override
public ModelAndView index(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mav = super.index(request, response);
mav.setViewName("cap4/monitor/kkcapRunningLog");
return mav;
}
}
下面的写法等同上面的写法,但上面的写法更优:彻底保留了标准产品的源码,如果标准产品修复了代码,上面的写法就天然承接了标准产品的修改。
@Replace(beanName = "/cap4/capLog.do")
public class KKCapLogController extends CapLogController {
@Override
public ModelAndView index(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mav = new ModelAndView("cap4/monitor/kkcapRunningLog");
List<Enums.CapLogType> capLogTypeList = Enums.CapLogType.getLogTypeListByOwnerType();
mav.addObject("capLogType", JSONUtil.toJSONString(capLogTypeMapList));
return mav;
}
}
3)最后将KKCapLogController.java注册到客开插件Spring XML中:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans default-autowire="byName">
<!-- 客开Bean替换的XML注册信息,这里的name没什么特别意义,只要保证唯一性即可 -->
<bean name="/cap4/kkcapLog.do" class="com.seeyon.客开插件路径.KKCapLogController"/>
</beans>
4)最后将文件编译,将KKCapLogController.class、客开插件配置、kkcapRunningLog.jsp部署到服务器测试。
5)完成以上操作之后,用户使用过程中,在请求/cap4/capLog.do?method=index这个URL的时候就不会再走标准产品代码,而是走客开的KKCapLogController.index方法。
# 后端动态接口开发和调用
# 支持场景
面对Controller、Manager、Dao、Resource(Rest类)任何一层,客开期望在标准产品方法中某个位置插入一段自己的实现逻辑,不涉及大改,均可以通过后端动态接口开发适配。
非常适合做动态接口的场景是:客户从低版本升级到V8.1或更高版本,此时涉及客开代码合并,为了防止以后无休止的客开源码合并工作量,可以通过动态接口进行快速转型开发。
# 实现步骤
1、客开先按照侵入源码的方式进行二次开发,自测确认客开有效性,如果有效则开始确认动态接口开发有效性
2、在上一步基础上,去开放平台Open API目录-动态接口章节,搜索是否有现成的动态接口,如果有则直接复用开发
3、如果开放平台Open API中没有现成动态接口,则可以:发起动态接口支持流程,由研发新增动态接口并提交到ctp-studio,随后客开调用新动态接口开发
# 开发示例
场景描述: 按照客户需求,需要在标准产品ColManagerImpl.findsSummaryComments里面增加一段二次开发代码,以满足客户的需求。
传统的实现方式如下所示,我们会直接修改标准产品源码,插入一段客开实现,这种方式短平快,能解决用户问题。
但随之而来的是后期维护成本:ColManagerImpl是协同模块核心类,研发会修改此类进行新功能开发和BUG修复,而客户每次升级、打补丁包都需要客开确认代码是否冲突,无形增加了N倍后期维护成本。
为了解决以上问题,我们可以通过动态接口埋点的形式无侵入源码开发!
public class ColManagerImpl implements ColManager{
@Override
public Map<String, Object> findsSummaryComments(Map<String, String> params) throws BusinessException {
// ......此处省略若干行标准产品源码
// xxx客户 过滤意见内容 start
try {
String depart = params.get("departId");
String isOk = params.get("isOk");
if (depart != null) {
Long departId = Long.parseLong(depart.substring(depart.indexOf('|') + 1));
ZgyzManager zgyzManager = (ZgyzManager) AppContext.getBean("zgyzManager");
zgyzManager.filterComment(commentList, departId, "true".equals(isOk));
if(commentList != null) {
allCommentCount.put(0, commentList.size());
}
}
} catch (Exception e) {
LOG.error("***意见过滤异常:", e);
}
// xxx客户 过滤意见内容 end
// ......此处省略若干行标准产品源码
}
}
# 研发侧-动态接口埋点
基于以上代码,首先需要在客开位置进行动态接口“埋点”,这个是研发的工作,资深客开工程师也可以主动开发动态接口,然后将源码提交给研发。
首先一个大原则:按照解耦思想,apps-xxx模块的动态接口全部要在apps-api下定义,表单应用在cap-api定义,工作流在ctp-workflow-api。
1)首先要确认开通接口的模块是否已经有动态接口基础定义,比如协同模块,在apps-api下能找到如下基础定义,则这一步就不用新开了。
如果所属的模块没有,则需要参考如下代码去实现。比如bbs没有,则新增一个BBSDynamicInterface extends DynamicInterface,重写getModuleDes()=BBS讨论模块动态接口,重写getModuleName()='bbs'。
// since注解表示代码初始化版本
@Since("8.1")
public interface ColDynamicInterface extends DynamicInterface {
@Override
default String getModuleDes() {
return "协同动态接口";
}
@Override
default String getModuleName() {
return "collaboration";
}
}
2)然后再在apps-api下定义一个interface接口,extends第一步的ColDynamicInterface。当然,如果ColManagerImpl有多处埋点,多个接口可以共用一个ColManagerImplDynamicApi。
@Since("8.1")
public interface ColManagerImplDynamicApi extends ColDynamicInterface {
// 本次新开埋点
@Since("8.1SP1")
default void findsSummaryCommentsOfFilterComment(ColSummaryCommentBO colSummaryCommentBO){
}
// 历史埋点
@Since("8.1")
default void removeColExtendInfo(ColSummaryBean bean) {
};
// 历史埋点
@Since("8.1")
default void putColExtendInfo(ColExtendInfoBean bean) {
};
}
新开埋点需要传入一个对象ColSummaryCommentBO,实现如下:
public class ColSummaryCommentBO implements Serializable {
Map<String, String> params;
List commentList;
Map<Integer, Integer> allCommentCount;
// ......此处省略getter、setter
}
注意:我们所有动态接口都要求以对象的形式传递参数,面向对象开发对后期接口扩展维护有极大增益。
default关键字是JDK8新特性,开发必学!
3)以上完成之后,在ColManagerImpl标准产品客开修改代码位置进行动态接口埋点:
public class ColManagerImpl implements ColManager{
/**如下代码片段是标准产品开通动态接口埋点必备代码start**/
@Inject
private DynamicContext dynamicContext;
private ColManagerImplDynamicApi colManagerImplDynamicApi = null;
@PostConstruct
public void init() {
colManagerImplDynamicApi = dynamicContext.getBean(ColManagerImplDynamicApi.class);
}
/**如下代码片段是标准产品开通动态接口埋点必备代码end**/
@Override
public Map<String, Object> findsSummaryComments(Map<String, String> params) throws BusinessException {
// ......此处省略若干行标准产品源码
// xxx客户 动态接口埋点 start
if(colManagerImplDynamicApi != null){ // colManagerImplDynamicApi != null是必写代码,否则NullPointerException必现
ColSummaryCommentBO colSummaryCommentBO = new ColSummaryCommentBO();
colSummaryCommentBO.setCommentList(commentList);
colSummaryCommentBO.setAllCommentCount(allCommentCount);
colSummaryCommentBO.setParams(params);
colManagerImplDynamicApi.findsSummaryCommentsOfFilterComment(colSummaryCommentBO);
}
// xxx客户 动态接口埋点 end
// ......此处省略若干行标准产品源码
}
}
4)以上完成之后,将代码提交到标准产品,后续版本客开就可以复用。同时将修改的源码提交到客开对应的ctp-studio工程中,客开即可复用,进行接口化调用。
# 客开侧-动态接口调用
在研发已经提供了对应动态接口埋点基础上,客开即可进行动态接口的接入,按此方式接入即可无侵入标准产品源码,也无需担心升级高版本、月度修复包代码合并成本。
1)客开按照插件化开发规则,新增或复用一个与标准产品无关的客开插件,在插件化开发框架下新增一个类继承ColManagerImplDynamicApi接口:
/**客开代码*/
public class ColManagerImplDynamicApiImpl implements ColManagerImplDynamicApi {
// 养成记录Log日志的习惯
private static final Log LOG = CtpLogFactory.getLog(ColManagerImplDynamicApiImpl.class);
}
2)重写研发的动态接口findsSummaryCommentsOfFilterComment方法,这里面埋入客开的代码:
public class ColManagerImplDynamicApiImpl implements ColManagerImplDynamicApi {
private static final Log LOG = CtpLogFactory.getLog(ColManagerImplDynamicApiImpl.class);
@Override
public void findsSummaryCommentsOfFilterComment(ColSummaryCommentBO colSummaryCommentBO) {
if(LOG.isDebugEnabled()){
LOG.debug("进入动态接口findsSummaryCommentsOfFilterComment...");
}
Map<String,String> params = colSummaryCommentBO.getParams();
List commentList = colSummaryCommentBO.getCommentList();
Map allCommentCount = colSummaryCommentBO.getAllCommentCount();
try {
String depart = params.get("departId");
String isOk = params.get("isOk");
if (depart != null) {
Long departId = Long.parseLong(depart.substring(depart.indexOf('|') + 1));
ZgyzManager zgyzManager = (ZgyzManager) AppContext.getBean("zgyzManager");
zgyzManager.filterComment(commentList, departId, "true".equals(isOk));
if(commentList != null) {
allCommentCount.put(0, commentList.size());
}
}
} catch (Exception e) {
LOG.error("***意见过滤异常:", e);
}
}
}
3)将ColManagerImplDynamicApiImpl.java注册到客开插件Spring XML中:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans default-autowire="byName">
<!-- 客开Bean替换的XML注册信息,这里的name没什么特别意义,只要保证唯一性即可 -->
<bean id="colManagerImplDynamicApiImpl" class="com.seeyon.客开插件路径.ColManagerImplDynamicApiImpl"/>
</beans>
4)最后将文件编译、部署到服务器测试。
# 前端JSP动态脚本链入
# 场景
常用于JSP页面的客户化开发,不修改标准产品前端页面,按照规范将javascript文件放置在对应位置,标准产品JSP页面也能加载到客户化开发的javascript文件。
# 开发示例
下面的过程,我们以新建协同为例,不修改系统的jsp或js文件,通过一个动态引入的js文件,达到替换系统的协同发送按钮的目的。
# 确定页面的ModelAndView
可以访问对应页面,请求中加入ctp_dump_modelAndView参数确定ModelAndView的名称
http://127.0.0.1/seeyon/collaboration/collaboration.do?method=newColl&rescode=F01_newColl&ctp_dump_modelAndView=true
查看Response的Header,ModelAndView.name即为View的名称。
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
ModelAndView.name: apps/collaboration/newCollaboration
Pragma: No-cache
Cache-Control: no-store
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: text/html;charset=UTF-8
Content-Language: zh-CN
Vary: Accept-Encoding
推荐使用Fiddler工具。
至此,我们确定新建协同页面的ModelAndView为apps/collaboration/newCollaboration。
# 建立自定义的JavaScript文件
1、在webapps/seeyon的extend/js目录下,建立与ModelAndView名称相同的子目录(请注意大小写),如
apps/collaboration/newCollaboration
2、创建任意名称的js文件,如
webapps/seeyon/extend/js/apps/collaboration/newCollaboration/replaceButton.js
编写控制逻辑:利用IE的开发人员工具或直接查看源代码,我们可以确定新建协同的发送按钮Id为sendId,因此,我们这样编写replaceButton.js
// 在原有的按钮后面添加我们自己定义的发送按钮
$('#sendId').after('<input id="btnCustomSend" type="button" value="发送"/>');
// 隐藏原发送按钮
$('#sendId').hide();
3、新建的js文件在重启后生效。
# 代码实现原理
注:以下只是标准产品实现原理演示,客开无需自行封装。
后端:com.seeyon.ctp.common.web.ExtendJavascriptInteceptor#postHandle,将EXTEND_JS传递给前端:
String parentPath = SystemEnvironment.getApplicationFolder() + "/extend/js/";
String path = parentPath + viewName.replace("raw:", "");
File directory = new File(parentPath);
File file = new File(path);
if (FileUtil.inDirectory(file, directory)) {
list = this.getListFiles(path, "js", false);
int size = list.size();
List<String> l = new ArrayList<String>(size);
for (String js : list) {
l.add("extend/js/" + viewName + "/" + js);
}
list = l;
}
view.addObject("EXTEND_JS", list);
前端:标准产品的common_footer.jsp公共jsp文件中,实现了解析扩展的url路径,追加到页面中:
<c:if test="${fn:length(EXTEND_JS)>0}">
<c:forEach var="js" items="${EXTEND_JS}">
<script type="text/javascript" src="${staticPath}/${js}${ctp:resSuffix()}"></script>
</c:forEach>
</c:if>
# 前端动态接口开发和调用
# 支持场景
前端接口主要适用于JS文件中一些标准产品已有的方法逻辑的修改,以及替换一些方法的实现逻辑;前端动态接口实现了第三方JS文件的加载机制以及通用的事件生命周期,从而降低客开后期升级成本。
前端接口的支持场景:
1、要在列表界面新增展示字段
2、修改按钮点击事件逻辑,或者在事件逻辑中新增校验
# 实现步骤
1、客开先按照侵入源码的方式进行二次开发,自测客开有效性,如果有效则开始确认动态接口开发有效性
2、在上一步基础上,去开放平台Open API目录-动态接口章节,搜索是否有现成的动态接口,如果有则直接复用开发
3、如果开放平台Open API中没有现成动态接口,则可以:发起动态接口支持流程,由研发新增动态接口并提交到ctp-studio,随后客开调用新动态接口开发
# 开发示例
场景描述:阅读、知会、传阅节点用户点开后直接变已办,不需要点击提交
传统的实现方式如下所示,我们会直接在标准产品源码对应的JS中增加逻辑,这样虽然可以解决用户问题,但是在之后版本升级时会造成代码冲突,大大的增加了后期维护成本
为了解决以上问题,我们可以通过JS中埋点的形式实现无源码侵入开发!
function _onloadFunc() {
//省略标准产品代码。。。
//客开 加 屏蔽知会、阅读、传阅 节点意见处理框
if(nodePolicyName == '知会' || nodePolicyName == '阅读' || nodePolicyName == '传阅'){
var sideBtn = document.getElementById('sideBtn');
sideBtn.click();
var east = document.getElementById('east');
east.style.display= "none";
callBackendMethod("noSubmissionManager", "noSubmission", affairId,"collaboration",null);
//客开 加 屏蔽知会、阅读、传阅 节点意见处理框
}
}
以上在修改在标准产品方法中新增了逻辑,会导致升级产品时再一次迁移代码,如果升级产品对应地方代码有变动,升级产品耗费会大大增加,我们可以在新增代码位置埋点,将新增代码拿出来放在一个单独的JS里,用来实现埋的动态接口
实现逻辑是:
1)、在调用对应方法的JSP或者HTML里引入
<script src="/seeyon/common/cap-dynamic-front/load.js"></script>
2)、将实现接口的JS文件放入/seeyon/common/cap-dynamic-front/js 下,执行上级目录的dev-app.exe,重新编辑load.js,重新生成的load.js会将客开js中的url放入map,在请求的时候就能加载的客开的JS
# 研发侧-动态接口埋点
JS动态接口埋点:
//界面加载完后执行onload方法
function _onloadFunc(){
//省略标准产品代码。。。
customManage.methodEmit('apps-Col-doSubmitAfterLoad',{nodePolicyName:nodePolicyName});
}
apps-Col-doSubmitAfterLoad为研发埋点提供方法名,研发可根据具体需求自定义,nodePolicyName为客开需要用到的参数。
在summary.jsp以及其他引用了summary.js中引入load.js:
<script type="text/javascript" src="${path}/common/cap-dynamic-front/load.js${ctp:resSuffix()}"></script>
# 客开侧-动态接口实现
客开实现代码:
customManage.load({
match: {
url: ['/collaboration/collaboration.do?method=summary'],
},
unmatch: {
url: ''
},
cssLoadPath: [],
// 加载顺序
order: 99
});
/**
* DOM加载完毕后的处理,加入模板3页面元素
*/
customManage.methodHook('apps-Col-doSubmitAfterLoad',function () {
var nodePolicyName = this.nodePolicyName;
if(nodePolicyName == '知会' || nodePolicyName == '阅读' || nodePolicyName == '传阅'){
var sideBtn = document.getElementById('sideBtn');
sideBtn.click();
var east = document.getElementById('east');
east.style.display= "none";
callBackendMethod("noSubmissionManager", "noSubmission", affairId,"collaboration",null);
//客开 加 屏蔽知会、阅读、传阅 节点意见处理框
}
}
});
match.url 用来指定需要加载该js的页面url,可以根据IE开发者工具查看
cssLoadPath 加载该js的同时,将会加载该css文件
order指定加载顺序,js加载优先级
# 客开JS文件存放生效
1、放入标准产品ApacheJetspeed/webapps/seeyon/common/cap-dynamic-front/js目录下
2、运行ApacheJetspeed/webapps/seeyon/common/cap-dynamic-front/dev-app.exe文件,提示新生成load.js文件后成功,服务器环境不可运行dev-app.exe文件的,可把cap-dynamic-front文件夹到处,执行dev-app.exe后替换服务器load.js文件
3、不需要考虑引入客开自己创建的js文件,运行dev-app.exe程序后,自己生成的js会被加载到load.js中,埋点时引入了load.js,可以直接加载到客开的js文件
← APP-API解耦开发 缓存组件 →