# 动态接口

# 后端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)客开按照插件化开发规则,新增或复用一个与标准产品无关的客开插件,在插件化开发框架下完成如下代码实现:

  1. 新增一个客开类KKCapLogController.java(类名可以自定义),此类extends标准产品的CapLogController.java
  2. 在客开类的顶部增加@Replace注解,beanName一定是标准产品CapLogController在XML中注册的名字
  3. 在客开类中,通过@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文件

创建人:het