# 插件化开发

V5基于三层模型架构运转,V5又是基于插件化开发模式以达到按模块松耦合、可插拔。

插件化开发是平台的基本能力,无论是标准产品还是二次开发,都应该基于插件化开发框架范畴进行技术完善,所以这是V5产品开发基础中的基础!

# 基础架构

1696756917145.png

# 开发准备

# 字符集

为了实现国际化编程,全局要求使用UTF-8的字符集编码,包括:

  • 数据库:参考安装维护手册配置字符集
  • 文件:java、properties、jsp、js、css、html等等使用UTF-8
  • servlet:response.setContentType(“text/html; charset=UTF-8”);
  • JSP头部标记UTF-8引用:<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

# 代码结构

完整的项目结构大概如下

src
    com
        seeyon
            apps(ctp)                                        #1
                sample
                    controller
                        SampleController.java                #2
                    manager
                        SampleManager.java                   #3
                        SampleManagerImpl.java               #4
                    dao
                        SampleDao.java                       #5
                        SampleDaoImpl.java                   #6
                    po
                        SamplePO.java                        #7
                        SamplePO.hbm.xml                     #8
webapps
    WEB-INF
        cfgHome
            i18n
                SampleResource_en.properties                 #9
                SampleResource_zh_CN.properties
                SampleResource_zh_TW.properties
            spring
                spring-sample-controller.xml                 #10
                spring-sample-manager.xml                    #11
                spring-sample-dao.xml                        #12
编号 说明
1 应用使用apps,CTP平台使用ctp如com.seeyon.apps.news和com.seeyon.ctp.form
2 MVC Controller(如果模块不大,Controller、Manager和Dao都可以放到上级package中,但PO必须放在po package中)
3 MVC Manager接口
4 MVC Manager实现
5 MVC DAO接口
6 MVC DAO实现
7 PO
8 PO Hibernate映射文件
9 Java国际化资源文件
10 Controller的Spring配置文件
11 Manager的Srping配置文件
12 Dao的Spring配置文件

# 完整开发示例

V5平台提供“插件式”开发模式:一个业务功能模块就是一个插件,该业务功能模块下的所有代码在各自插件下维护,Spring Bean也交由该插件管理,如果插件停用、删除则该业务功能完全下线。

在本地协同服务ApacheJetspeed\webapps\seeyon\WEB-INF\cfgHome\plugin目录下能看到很多子文件夹,每一个文件夹就是一个插件,比如协同collaboration、公文edoc、表单应用cap4。

一个插件模块的开发步骤包含:

  1. 定义插件
  2. 创建spring beans文件
  3. 创建对应的hbm文件
  4. 编写XXXController、XXXManager、XXXDao XXXVO、XXXPO
  5. 编写前端页面

# 定义插件

在ApacheJetspeed\webapps\seeyon\WEB-INF\cfgHome\plugin目录下新建新的插件文件夹,一个标准的目录结构如下所示:

1714300689274.png

插件定义:插件的信息定义在/src/main/webapp/WEB-INF/cfgHome/plugin/demo/pluginCfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<plugin>
 <!-- id最重要:保证全系统唯一 -->
 <id>demo</id>
 <!-- name仅仅是在ctp.log日志中输出 -->
 <name>开发实例</name>
 <!-- 数字自定义,最好不超过5位数,太大启动会报错 -->
 <category>50505</category>
</plugin>

其它spring/spring-demo-*.xml初期可以保持为空,等后续创建对应Java类之后再使用。

i18n下面的文件是国际化文件,初期同样可以保持为空,等需要国际化开发再使用。

将以上plugin/demo文件夹拷贝到本地协同服务ApacheJetspeed\webapps\seeyon\WEB-INF\cfgHome\plugin之后,启动系统完成后,查看ApacheJetspeed\logs_sy\ctp.log日志能够看到如下信息:

[localhost-startStop-1]  INFO: PluginSystemInit: - Load Plugin : 50505, demo, 开发实例

# 三层功能开发

# Controller层

基于Spring MVC开发,后端提供Controller负责数据转发,定义Controller方法需要继承平台的BaseController

Controller的职责:只负责页面转发、参数传递、数据返回,不参与业务计算,所有业务计算和数据抽取均调用Manager层。

package com.seeyon.apps.demo.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import com.seeyon.ctp.common.controller.BaseController;

public class DemoController extends BaseController{
 /**
  * index方法是:URL不传任何method,默认访问的方法
  */
 @Override
 public ModelAndView index(HttpServletRequest request, HttpServletResponse response) throws Exception {
  return new ModelAndView("apps/demo/index");
 }
}

新建的Controller需要在Spring中注册,使用XML的形式注册到:/src/main/webapp/WEB-INF/cfgHome/plugin/demo/spring/spring-demo-controller.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">
<!-- 注意是name不是id -->
 <bean name="/demo.do" class="com.seeyon.apps.demo.controller.DemoController"></bean>
</beans>

下一步是编写跳转页面,Controller中ModelAndView("apps/demo/index")意味着页面需要放置在:/WEB-INF/jsp/apps/demo/index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html class="over_hidden h100b">
<head>
 <%@include file="/WEB-INF/jsp/common/common_header.jsp"%>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 <meta name="renderer" content="webkit">
 <title>${ctp:i18n("文件标题国际化key") }</title>
 <link rel="stylesheet" href="${path}/apps_res/{module}/css/xxx.css${ctp:resSuffix()}">
</head>
<body class="over_hidden h100b">
 这里是index.jsp页面
</body>
<%@include file="/WEB-INF/jsp/common/common_footer.jsp"%>
<script type="text/javascript" src="${path }/apps_res/{module}/js/xxx.js${ctp:resSuffix()}"></script>
</html>

如果以上开发编译完成,部署到V5平台下之后,可以通过如下链接访问到:

http://[host]:[port]/seeyon/demo.do?method=index

如果你是本地默认端口则可以访问:http://localhost/seeyon/demo.do?method=index

# Manager层

Manager是业务层,负责代码逻辑运算、解析、转换、涉及业务逻辑实现全部在此层完成。

首先要定义Manager接口:

public interface DemoManager {
  public void saveDemo(Map<String,Object> params) throws BusinessException;
}

然后编写实现类:

public class DemoManagerImpl implements DemoManager {
  @Override
  public void saveDemo(Map<String, Object> params) throws BusinessException {
    //代码实现块
  }
}

新建的Manager需要在Spring中注册,使用XML的形式注册到:/src/main/webapp/WEB-INF/cfgHome/plugin/demo/spring/srping-demo-manager.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 id="demoManager" class="com.seeyon.apps.demo.manager.DemoManagerImpl"></bean>
</beans>

Manager被Controller所调用,我们可以在Controller中通过Inject注解注入Manager,随后就可以调用业务层的方法了。

public class DemoController extends BaseController{
// 通过Inject注入Manager
 @Inject
 private DemoManager demoManager;

 @Override
 public ModelAndView index(HttpServletRequest request, HttpServletResponse response) throws Exception {
  // 调用Manager方法,伪代码
  demoManager.saveDemo(params);
  return new ModelAndView("apps/demo/index");
 }
}

# DAO层

DAO层用于数据访问,直接对数据库进行操作,SQL全部在DAO层维护。

首先要定义Dao接口

public interface DemoDao {
  public List find();
}

然后编写实现类:

public class DemoDaoImpl implements DemoDao {
 public List find() {
  String sql = "select id from demo order by id";
  // CTP平台基于JDBC的工具类
  JDBCAgent jdbc = new JDBCAgent(false,false);
  try {
   // SQL查询方法
   return jdbc.findNamedSql(sql);
  } catch (BusinessException e) {
   return null;
  } finally {
   // 使用完毕后关闭连接,不关闭会造成连接池泄露
   jdbc.close();
  }
 }
}

新建的Dao需要在Spring中注册,使用XML的形式注册:/src/main/webapp/WEB-INF/cfgHome/plugin/demo/spring/srping-demo-dao.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 id="demoDao" class="com.seeyon.apps.demo.dao.DemoDaoImpl"></bean>
</beans>

以上是DAO的最基本用法。V5平台基于数据库持久层引入了Hibernate框架,我们可以介入Hibernate相关API特性进行快速开发,同样也可以基于HQL进行数据库兼容编写。

更多更详细的DAO层API使用案例参考本站[DAO数据层开发]文档。

# 三层开发总结

综上,整个请求调用链路是:前端请求调用=>Controller=>Controller注入Manager+调用Manager接口获取数据并返回=>Manager注入DAO+调用Dao接口实现数据业务操作=>Dao直连数据库进行CRUD操作。

Controller、Manager、Dao三层开发各司其职,只有做好正确的切分才能保障代码的健壮性。

以上仅仅是最传统的三层模型开发,而随着技术的演进,V5平台基于这三层还衍生出了多种架构模式,如Restful、Ajax、Api等,万变不离其宗,只要三层结构封装够健壮,都可以顺利桥接。

# JSP开发准备

V5平台是单体应用架构,后端采用MVC框架,前端可以使用JSP或Html方案开发。

如果需要短平快的形式开发,可以采用JSP的开发模型,V5平台提供了完整的JSP开发配套前端组件,可以使用Seeyon UI+JQuery+AJAX进行页面操作,早期的应用均采用这种开发模式。

如果前端技能比较强,可以基于VUE+Html纯前后端分离的开发模式开发,V5部分应用正是采用这种开发模式。

V5平台推荐的一个标准的JSP页面应该是如下的结构:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html class="over_hidden h100b">
<head>
 <%@include file="/WEB-INF/jsp/common/common_header.jsp"%>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 <meta name="renderer" content="webkit">
 <title>${ctp:i18n("文件标题") }</title>
 <link rel="stylesheet" href="${path}/apps_res/{module}/css/xxx.css${ctp:resSuffix()}">
</head>
<body class="over_hidden h100b">
 这里是index.jsp页面
</body>
<%@include file="/WEB-INF/jsp/common/common_footer.jsp"%>
<script type="text/javascript" src="${path }/apps_res/{module}/js/xxx.js${ctp:resSuffix()}"></script>
</html>

common_header.jsp是平台统一封装的各种CSS以及JSP常用的JSTL和自封Taglib。

common_footer.jsp是平台统一封装的各种Javascript、js变量。

如果当前页面有比较特殊的css样式控制,可以自己编写独立的css文件,通过link rel="stylesheet" href引入。

不推荐大篇幅的Javascript在JSP中直接编写,我们推荐编写独立的.js文件,通过script type="text/javascript" src引入。

不推荐在JSP中嵌入EL表达式,甚至JSTL表达式,我们推荐使用Javascript+AJAX的形式进行页面的布局、渲染(或者就把JSP当前一个Html来开发)。

# JSP开发演示

下面是一个增、删、查的常规页面,本章基于此页面做代码演示。

1714300736442.png

第一步:使用SeeyonUI的static layout进行上下布局,上部分是操作按钮,下部分是table表格:

<!-- CSS静态上下布局,更多布局参考SeeyonUI组件库文档 -->
<style>
.stadic_head_height {
 height: 40px;
}

.stadic_body_top_bottom {
 bottom: 0px;
 top: 40px;
}
</style>
</head>
<body class="over_hidden h100b">
 <div class="stadic_layout">
  <div class="stadic_layout_head stadic_head_height">
   <div id="toolbar"></div>
  </div>
  <div class="stadic_layout_body stadic_body_top_bottom"
   id="layout_bottom">
   <table id="mytable" style="display: none"></table>
  </div>
 </div>
</body>

第二步:通过Javascript调用SeeyonUI API将上部分“添加”和“删除”按钮渲染出来:

/**
 * 这段代码可以独立js文件维护,也可以放在JSP script代码块中维护
 * toolbar学习资料:http://open.seeyon.com/seeyonui/V2.0/components/index.html#___toolbar
 */
var DemoIndex = {};
DemoIndex.initToolbar = function() {
 var tt = $("#toolbar").toolbar({
    toolbar : [{
       id : "add",
       name : $.i18n('common.button.add.label'),
       className : "ico16 add_16",
       click : DemoIndex.openDemoDialog
      }, {
       id : "delete",
       name : $.i18n('common.button.delete.label'),
       className : "ico16 del_16",
       click : DemoIndex.deleteDemo
      }]
   });
}

以上只是定义了初始化操作按钮的方法initToolbar,接着我们通过JQuery的ready事件,在初次进入页面时执行渲染即可实现按钮的显示:

<script type="text/javascript">
 $().ready(function() {
  DemoIndex.initToolbar();
 });
</script>

第三步:同理,通过Javascript调用SeeyonUI API将下部分列表渲染出来,由于SeeyonUI ajaxGrid涉及数据获取,还需要编写后端代码,本章就不做全部代码,只做伪代码演示:

var DemoIndex = {};

// 工具按钮参考文档:http://open.seeyon.com/seeyonui/V2.0/components/index.html#___toolbar
DemoIndex.initToolbar = function() {}

// 列表渲染参考文档:http://open.seeyon.com/seeyonui/V2.0/components/index.html#___grid
DemoIndex.initGrid = function() {}

基于以上方式,一个简单的前后端页面即开发完成。

# 示例源码下载

我将这个简单的示例源码提交到Gitee,你可以从Gitee获取了完整的开发示例:https://gitee.com/qqbless/seeyon_v5_demo

# 插件化深度应用

# 插件主动停止

假设我的插件需要依赖一些基础配置,只有这些配置生效才能使用本插件,则可以继承平台PluginInitializer接口实现isAllowStartup方法:

package com.seeyon.apps.fk;

public class FKPluginDefintion implements PluginInitializer {
    @Override
    public boolean isAllowStartup(PluginDefinition arg0, Logger arg1) {
    	boolean flag = "on".equals(arg0.getPluginProperty("fk.enable"));
        return flag;
    }
}

以上完成后,需要在插件配置文件下注册initializer:\WEB-INF\cfgHome\plugin\插件名\pluginCfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<plugin>
	<id>自定义</id>
	<name>自定义</name>
	<category>自定义</category>
	<!-- 自定义PluginInitializer的实现类 -->
	<initializer>com.seeyon.apps.fk.FKPluginDefintion</initializer>
</plugin>

最终效果:启动OA过程中,如果插件PluginInitializer判断isAllowStartup返回为false,则会主动停用插件,提示信息类似于:

插件[222, fk, 分库插件]的initializer[com.seeyon.apps.fk.FKPluginDefintion]禁止了此插件的启动.

# 插件初始化Initializer

假设我的插件需要从数据库初始化加载缓存,以方便后续使用,则我们需要在插件类实例化完成后,执行初始化操作。

平台绝对禁用使用Spring的init来初始化缓存:

<!-- Bad code 绝对禁止使用init-method -->
<bean name="" class="" init-method="init" ></bean>

插件化开发标准解决方案是:继承SystemInitializer并实现initialize方法,initialize方法的执行时机时:系统所有Spring初始化完成之后,由根据平台默认任务编排依次执行继承了SystemInitializer的initialize方法。

public class DemoInitializerl extends AbstractSystemInitializer {

    // 重写AbstractSystemInitializer下的initialize方法,
    @Override
    public void initialize() {
        // 此时所有Bean已经初始化完成,可以放心调用所有Bean
        TODO
    }

}

# 插件与插件解耦

由于V5平台不是云原生应用,未实现模块与模块之间微服务化,故原则上插件与插件的代码可以相互调用,但这种混乱调用会造成严重耦合性。

针对此问题,平台基于Maven做基础,要求每个插件一个Project,每个Project一份Maven,通过Maven来控制依赖:不允许插件A直接调用插件B,只能通过中间工程apps-api做中间桥梁连接,这样的代码管理就实现了插件间解耦。

相关规范和编写方法见【APPS-API解耦开发 (opens new window)

# 插件是否可用判断

假设插件A涉及插件B的代码调用,则要先判断插件B是否启用。如果插件B未启用,则它的所有类均未被实例化,调用就会异常。

平台提供了AppContext#hasPlugin方法用于判断插件是否启用:

if(AppContext.hasPlugin("插件B")){
	插件BAPI.xxx();
}
创建人:admin
修改人:het