1. 后端组件

1.1. 当前用户

框架提供多种方式获取当前登录用户相关信息com.seeyon.ctp.common.authenticate.domain.User

获取当前用户信息的几种方式:

  1. 后台获取当前用户信息的API为AppContext.getCurrentUser()。
  2. JSP中可以用EL表达式${CurrentUser.xxx}。
  3. Javascript中可以用$.ctx.CurrentUser.xxx(目前只开放id,name、loginAccount和loginAccountName)获取。

为避免应用自行创建User对象设置到当前用户,请统一使用UserUtil的build方法创建,setCurrentUser方法设置。

如果是系统中存在的人员,使用UserUtil.build构建, 使用UserUtil.setCurrentUser设置为当前用户

    String loginName;
    User user = new User();
    V3xOrgMember m = orgManager.getMemberByLoginName(loginName);
    user.setLoginAccount(m.getOrgAccountId());
    user.setId(m.getId());
    user.setLocale(LocaleContext.getAllLocales().get(0));
                   AppContext.putThreadContext(com.seeyon.ctp.common.GlobalNames.SESSION_CONTEXT_USERINFO_KEY, user);

改为

    User user = UserUtil.build(loginName,"",null);
    if(user!=null) {
        UserUtil.setCurrentUser(user);
    }

如果是系统中不存在的虚拟用户,如超级节点,使用 UserUtil.buildVirtualUser构建 如

    User user = new User();
    user.setName("SuperNodeUser");
    AppContext.putThreadContext(com.seeyon.ctp.common.GlobalNames.SESSION_CONTEXT_USERINFO_KEY, user);

改为

    User user = UserUtil.buildVirtualUser(1l,"SuperNodeUser");
    UserUtil.setCurrentUser(user);

1.2. 产品线相关判断

  • 获取当前产品线信息获取当前产品线信息

    com.seeyon.ctp.common.constants.ProductEditionEnum.getCurrentProductEditionEnum()
    

    参考

  • 获取当前版本信息

    com.seeyon.ctp.common.constants.ProductVersionEnum.getCurrentVersion()
    

    参考

  • 是否集团版
    (Boolean) com.seeyon.ctp.common.flag.SysFlag.sys_isGroupVer.getFlag()
    
    参考
  • 获取浏览器差异标识,如当前浏览器是否运行管理员登录
(Boolean) com.seeyon.ctp.common.flag.BrowserFlag.A8Allow4Admin(request)

参考

1.3. 国际化 i18n

CTP对国际化组件进行全新的设计,所有的国际化资源文件集中存放在cfgHome/i18n下,可以按照模块划分子文件夹存放,框架会在启动时自动加载全部资源文件,并形成总体的key->value映射,这种模式下要求各模块的key命名要严格按照规范进行定义,不允许出现重复,否则将会相互覆盖。系统Locale定义的配置存在于

WEB-INF/cfgHome/base/systemProperties.xml中的locales属性:

        <!-- 系统提供的语言种类 用逗号分隔 -->
        <locales>zh_CN,en,zh_TW</locales>

前端的调用方式如下:

<tr>
    <td>ctp:i18n函数</td>
    <td>${ctp:i18n('common.button.add.label')}</td>
</tr>
<tr>
    <td>ctp:i18n_1函数,一个参数,变量参数</td>
    <td>${ctp:i18n_1('common.charactor.limit.label',path)}</td>
</tr>
<tr>
    <td>ctp:i18n_1函数,一个参数,数值参数</td>
    <td>${ctp:i18n_1('common.charactor.limit.label',10)}</td>
</tr>
<tr>
    <td>ctp:i18n_1函数,一个参数,字符串参数</td>
    <td>${ctp:i18n_1('common.charactor.limit.label','测试')}</td>
</tr>
<tr>
    <td>ctp:i18n_2函数,两个参数</td>
    <td>${ctp:i18n_2('common.charactor.limit.label','测试',3)}</td>
</tr>
<tr>
    <td>ctp:i18n_3函数,三个参数</td>
    <td>${ctp:i18n_3('common.charactor.limit.label','测试',3,3)}</td>
</tr>
<tr>
    <td>ctp:i18n_4函数,四个参数</td>
    <td>${ctp:i18n_4('common.charactor.limit.label','测试',3,3,3)}</td>
</tr>
<tr>
    <td>ctp:i18n_5函数,五个参数</td>
    <td>${ctp:i18n_5('common.charactor.limit.label','测试',3,3,3,3)}</td>
</tr>

后端的调用方式如下:

ResourceUtil.getString("common.button.add.label");
ResourceUtil.getString("common.charactor.limit.label", 12);
ResourceUtil.getString("common.date.times", 2, 15);
ResourceUtil.getString("common.date.times", 2, 15, 3);
ResourceUtil.getString("common.date.times", 2, 15, 3, 4);
ResourceUtil.getString("common.date.times", 2, 15, 3, 4, 5);

此外,框架还提供了两种前端Javascript国际化调用支持,第一种是系统默认将把“.js”结尾的资源key生成到前端国际化资源中,通过$.i18n函数获取,同时支持动态

参数,如下例:

alert($.i18n('my.resource.js'));
alert($.i18n('my.resource.js','参数1','参数2'));

第二种是在i18n文件夹根下配置export_to_js.xml文件,用于指定哪些后台国际化资源要开放给前端Javascript引用,该文件内容例子如下:

<?xml version="1.0" encoding="utf-8"?>
<export>
    <resKey>common.button.cancel.label</resKey>
    <resKey>common.button.close.label</resKey>
    <resKey>common.button.ok.label</resKey>
</export>

1.3.1. 增加语种

以日语为例,平台采用标准的java i18n方式进行国际化,并进行了简化

国际化资源文件的位置:

1、webapps/seeyon/WEB-INF/cfgHome/i18n

2、webapps/seeyon/WEB-INF/cfgHome/plugin/*/i18n

3、webapps/seeyon/common/js/i18n

4、webapps/seeyon/WEB-INF/lib/v3x-all.jar的各个resources/i18n 下(6.0以上版本不存在v3x-all.jar可省略此步骤)

都是标准的java的properties文件,在相应的位置提供同名的_jp.properties文件即可。

此外,还有前端的js国际化资源需要修改,位置在webapps/seeyon/apps_res/*/i18n下,增加一个对应的ja.js文件。

最后,还需要修改webapps/seeyon/WEB-INF/cfgHome/base/systemProperties.xml,

增加ja语种

<!-- 系统提供的语言种类 用逗号分隔,首选语言放在第一位 -->

<locales>zh_CN,en,zh_TW,ja</locales>

如果是针对单个客户交付的项目,6.1SP1以后版本可以省略这一步,直接配置系统参数中的ctp.locales即可。

系统语言选择预置了德语、法语、日语、韩语、俄语的国际化

如果增加的语种超出了系统预置的范围,还需要修改webapps/seeyon/WEB-INF/cfgHome/i18n/common/下所有的LocaleSelectorResources_*.properties文件,增加对应的语种,并为新增的语种增加一个文件。

1.3.2. 时区

国际化必须支持时区,否则是不完整的,未了很好的处理时区信息,我们要求必须使用com.seeyon.ctp.util.Datetimes的parseXXX进行日期解析,切忌自己定义DateFormat;同理,必须使用formatXXX进行日期输出。

1.4. 配置管理

配置管理提供三种模式:系统配置、配置管理组件、系统开关组件

比较项 系统配置 配置管理组件 系统开关组件
定位 系统级的配置,只要配置系统运行环境、IT环境等基础数据 业务级的配置 在配置管理组件上进行封装
方式 配置方式单一,采用key-value方式 配置灵活,采用category-item-value-extValue 采用key-value(defaultValue)方式
管理模式 由专业人员直接修改文件 通过各应用自己的界面修改 通过统一的界面修改

1.4.1. 系统配置

标准产品为了适应各种环境、需求特性采用了参数化方式。CTP提供的参数方式继承、兼容老A8方式。

对如下几方面提供了参数化:

  • 系统全局
  • 插件级

参数使用过程包括如下几步:

  1. 定义参数文件
  2. 配置参数
  3. 获取参数

定义参数文件

CTP使用xml格式的参数文件。关键特性如下:

  • 分为用户配置项和产品配置项

    • 用户配置项:指允许用户通过配置工具进行配置。
  • 产品配置项:指不允许用户配置,不能通过配置工具修改的配置项。
  • 支持层次划分,为了方便对配置项进行分类,例如按模块。类似老A8的 common.size效果。
  • 系统全局与各插件分开配置

    • 系统全局配置文件为:base/systemProperties.xml
  • 插件配置分散在个插件中,参见插件规范
用户配置项

用户配置项的标识,在元素中增加了 mark属性的配置项,表示此项为用户配置项。

例如:

<temporary>
    <!-- 系统临时目录 SystemEnvironment.getSystemTempFolder() -->
    <folder mark="userconfig" desc="系统临时目录 SystemEnvironment.getSystemTempFolder()">${ctp.base.folder}/temporary</folder>
</temporary>

对于用户配置项,可以进一步定义该数据项的类型及其他约束,此部分沿用老A8方式。

此定义放在mark属性的值中,例如:mark="{int}"

  • 约束必须用{}括起来
  • {int}:表示该配置只能输入整形值
  • {int,1,10}:表示该配置只能输入整形值,且值域是1到10
  • {boolean}:表示该配置是true, false值
  • {option,5,10,15}:表示该配置只能在5、10、15这三个值域中选择
  • {password} : 对 配 置 值 进 行 加 密 , 在 代 码 中 使 用com.seeyon.ctp.tools.util.PwdEncoder.decode(String)
  • {VE}和{VP}只在集群启用时生效。
  • 标记了{VE}的配置项,集群所有节点的值必须一致
  • 标记了{VP}的配置项,集群各节点配置的目录必须是相同的目录(物理上)。
层次划分

层次特性示例:

<officeTrans>
        <rmi>
            <!-- M Office转换服务的IP地址 -->
            <host>127.0.0.1</host>
            <!-- M{int} Office转换服务的端口 -->
            <port>1097</port>
        </rmi>
        <cache>
            <!-- M Office转换文件目录,存放Office文件的HTML缓存 -->
            <folder>${A8.base.folder}/officetrans</folder>
        </cache>
        <!-- M{int} {VE} Office转换文件最多天数(默认60),超出的文件将被清理以节省空间 -->
        <retainDay>60</retainDay>
        <file>
            <!-- M{int} {VE} 允许转换的Office文件的大小:单位是byte,缺省只转换5M以下的文件 -->
            <maxSize>5242880</maxSize>
        </file>
    </officeTrans>

配置参数

通过配置工具对用户配置项进行配置,配置保存在base/conf中。

获取参数

在程序中获取参数值调用统一的接口SystemProperties.getInstance().getProperty(key);

key为参数的完整路径,例如:"product.build.date"

1.4.2. 配置管理组件

完成系统公共的管理配置,与组织模型、权限系统和各业务模块的专有配置一起构成整个系统的完整配置,各个业务应用模块也可以使用此模块来进行配置管理。 接口:ConfigManager configManager

package com.seeyon.ctp.common.config.manager;
public interface ConfigManager {
    //增加一个配置项
    ConfigItem addConfigItem(String configCategory, String configItem,
                            String configValue);

    ConfigItem addConfigItem(String configCategory, String configItem,

                            String configValue, String configDesp, String configType);

    //删除一个配置类别
    void deleteByConfigCategory(String configCategory);

    //删除一个配置项
    void deleteConfigItem(String configCategory, String configItem);

    //更新一个配置项
    void updateConfigItem(ConfigItem config);

    //列举配置项
    List listAllConfigByCategory(String configCategory);

    //获得一个配置项
    ConfigItem getConfigItem(String configCategory, String configItem);
}

1.5. JSON解析

JSON解析请统一使用平台提供的JSONUtil,为保证一致性,请勿使用直接使用fastjson、org.json、net.sf.json或jackson。

  // JSON解析
  String s ="{1:{\"first\":1,\"second\":2,\"third\":3}}"; 
    Map o = JSONUtil.parseJSONString(s, Map.class); // 无序
    Type type = new com.seeyon.ctp.util.TypeReference<Map<Integer,LinkedHashMap<String,Integer>>>(){}.getType();
    Map o1 = JSONUtil.parseJSONString(s, type); // 有序

  // JSON转换
  Pojo bean;
  String json = JSONUtil.toJSONString(bean);

如果使用了上述的其他解析器,请按下表进行调整。

调用 替代方法 示例(修改前) 示例(替代)
net.sf.json.JSONXxx.fromXxx JSONUtil.parseJSONString JSONArray.fromObject(str); JSONUtil.parseJSONString(str);
net.sf.json.JSONXxx.toXxx JSONUtil.toJSONString new JSONArray().toString() JSONUtil.toJSONString(bean);
org.json.JSONObject HashMap JSONObject data = new JSONObject(); Map data = new HashMap()
org.json.JSONObject JSONUtil.parseJSONString JSONObject data = new JSONObject(str); Map data = JSONUtil.parseJSONString(str,Map.class);
org.json.JSONArray ArrayList JSONArray arr = new JSONArray(str); List arr = JSONUtil.parseJSONString(str,List.class);
org.json.JSONXxx JSONUtil.toJSONString new JSONXxx().toString() JSONUtil.toJSONString(bean);
com.alibaba.fastjson.JSONObject HashMap JSONObject data = new JSONObject(); Map data = new HashMap()
com.alibaba.fastjson.JSONArray ArrayList JSONArray arr = new JSONArray(str); List arr = JSONUtil.parseJSONString(str,List.class);
com.fasterxml.jackson.databind.ObjectMapper JSONUtil.parseJSONString mapper.readValue(str); JSONUtil.parseJSONString(str)
com.fasterxml.jackson.databind.ObjectMapper JSONUtil.toJSONString mapper.writeValue(bean); JSONUtil.toJSONString(bean);

1.6. 脚本引擎支持(Groovy)

脚本引擎可用于

  1. 表单的公式(表达式)计算
  2. 流程分支条件运算
  3. 字符串宏替换

它最基本的功能是表达式计算,比如x+y,x=1,y=2

x+y是脚本

String script = "x+y";

而x=1,y=2则是对应的上下文,表现为

Map<String, Object> context = new HashMap<String, Object>();
context.put("x", 1);
context.put("y", 2);

通过下面的调用即可得到结果3

import com.seeyon.ctp.common.script.ScriptEvaluator;
...
Integer result = (Integer) ScriptEvaluator.getInstance().eval(script, context);

1.6.1. 表达式计算

// 预置函数库
String functions = "def 加(a,b){a+b}\n def 减 = {x,y-> x-y}\n";
// 运算上下文
Map<String, Object> context = new HashMap<String, Object>();
context.put("x", 1);
context.put("y", 2);
context.put("str", "string");
import com.seeyon.ctp.common.script.ScriptEvaluator;
...
ScriptEvaluator evaluator = ScriptEvaluator.getInstance();
try {
    assertEquals(evaluator.eval("x+y", context), 3);

    assertEquals(evaluator.eval("str.length()", context), 6);
    assertEquals(evaluator.eval(functions + "加 x,y", context), 3);
    assertEquals(evaluator.eval(functions + "减(x,y)", context), -1);
} catch (ScriptException e) {
    fail(e.getLocalizedMessage());
}

1.6.2. 字符串宏替换

import com.seeyon.ctp.common.script.ScriptEvaluator;
...
assertEquals(evaluator.evalString("x+$y", context), "x+2"); // 把$y替换为上下文中指定的值2
assertEquals(evaluator.evalString("$x+${y}", context), "1+2");//$y也可写为${y}

1.6.3. 基本函数库

为了实现预置公式,简化调用,我们可以像上面那样用Groovy的闭包或函数实现基本函数库,与表达式字符串一起调用eval

// 预置函数库
String functions = "def 加(a,b){a+b}\n def 减 = {x,y-> x-y}\n";

ScriptEvaluator.getInstance().eval(functions + "加 x,y", context);

但是,如果预置的函数库很大,无论是定义还是调用都会很麻烦。

我们可以用一种更优雅更自然的方式

  1. 首先,实现一个基础函数库类,这个类可以用groovy实现,也可以用java实现,但它的所有要被脚本调用的方法都必须是static的

    比如CommonFunctions.groovy

    package com.seeyon.ctp.common.script
    import com.seeyon.ctp.common.AppContext
    public class CommonFunctions {
        /**
         * 可以用Java method实现。
         */
        public static int add(int x,int y){
            return x+y;
        }
        /**
         * 也可以用Groovy function实现(支持中文)。
         */
        def static(a,b){
            a+b
        }
        /**
         * 还可以用Groovy closure实现。
         */
        def static= { x,y->
            x-y
        }
        /**
         * 可以为Java methdo包一个壳,简化调用。
         */
        def currentUserName(){
            AppContext.currentUserName()
        }
    }
    
  2. 然后,可以在Java中作如下调用

    evaluator.eval("import static com.seeyon.ctp.common.script.CommonFunctions.*;" + "减(x,y)", context)
    evaluator.eval("import static com.seeyon.ctp.common.script.CommonFunctions.*;" + "currentUserName()", context)
    
  1. 表单和工作流等模块可以继承CommonFunctions扩展自己的特殊函数

    package com.seeyon.ctp.common.script
    public class FormFunctions extends CommonFunctions{
    
    }
    

    然后,在调用时import

    evaluator.eval("import static com.seeyon.ctp.common.script.FormFunctions.*;" + "减(x,y)", context)
    

对于复杂的复合对象,建议在脚本中直接调用Java的系统上下文对象获取,比如

def userName = com.seeyon.ctp.common.AppContext.currentUserName()

同理,表单和工作流也可以封装供脚本调用的上下文对象,如FormContext、WorkFlowContext。

1.7. 公式组件(Since:V6.1)

公式 = 常量| 变量 | 函数 | 表达式

统一的函数、变量和表达式引擎,管理员和关键用户可以在流程分支匹配、表单计算等场景进行调用。

本组件目前只实现PC配置调用,暂不考虑移动端调用。

公式有三种形态:

公式类型 管理者 特征 示例
变量 管理员 无参 作为上下文注入 运行时可改变 x = 1 BASECURRENCY = "人民币"
表达式或Groovy函数 管理员 有参数 解释执行 运行时可改变 X = a+b
后台Java注解实现的函数 (支持国际化和参数返回值定制) 二次开发 无参\ 有参数 编译执行 运行期不可改变

公式名称不支持中文(为避免冲突,只接受至少一个大写字母和数字下划线的组合),但显示名称可为中文。

公式名称全局唯一,不可重名。

您可以登录单位管理员定义公式

公式可以更复杂,调用后台的Java方法

这样,就可以直接在配置流程的自定义函数中直接选择

目前因为工作流分支限制只列出返回值为布尔型的公式,需要在公式中组织好变量和表达式,返回布尔值。

例如我们在标准产品预置公式中的示例

1、定义了一个变量CEO_APPROVED_AMOUNT,总经理审批额度

2、定义了一个函数REQUIRE_CEO_APPROVED,需要总经理审批

param > getVar("CEO_APPROVED_AMOUNT")

3、这样,工作流分支里就可以使用这个函数了。以后如果要调整,修改CEO_APPROVED_AMOUNT变量的值即可。

公式组件在关联系统中的应用(Since:V7.0):

可以在关联系统的URL和参数预设值中使用公式,比如

http://mail.seeyon.com?username=${getVar("USER_LOGINNAME")}&rnd=${RAND()}

${getVar("USER_LOGINNAME")} 调用了公式中的变量USER_LOGINNAME,直接传递当前登录名。

${RAND()}则生成一个随机数。

利用好这个特性,可以不需要开发就实现一些简单的单点登录场景。

1.8. 事件

1.8.1. 应用场景

底层组件(或一个模块)发生动作,需要让上层应用(或其它模块)做出响应动作。

比如:组织模型创建单位,上层公文需要给这个新单位创建一套预置公文元素、公告需要预置一套板块等等。

1.8.2. 技术背景

采用类似Observable+Observer模式,使用java.util.EventObject技术,并进行了简化开发。

方案特点:注解驱动、无需注册监听、无需定义事件类型、异步分发、易扩展。

1.8.3. 开发步骤

  1. 定义事件 (Event) 封装事件的上下文,隐含事件类型信息,一个事件对象唯一标识一类事件,不允许重用,同时也在事件触发者和事件处理者之间传递数据。必须继承com.seeyon.ctp.event.Event。

  2. 事件触发

    在事件发生的源头,发出事件通知。 com.seeyon.ctp.event.EventDispatcher.fireEvent(Event)

  3. 事件监听(注解驱动)

    其它任意模块都可以监听上述事件,只需要在方法上面什么@listenEvent(event=XXXEvent.class),方法参数即为Event对象。 监听类必须定义为spring bean

1.8.4. 事件命名规范

事件对象都放到模块的event包下,如com.seeyon.apps.collaboration.event

模块名称+操作+Event,如:

// 流程发起事件 CollaborationStartEvent

// 流程结束事件 CollaborationFinishEvent

// 流程处理事件 CollaborationProcessEvent

// 流程回退事件 CollaborationStepBackEvent

// 流程撤销事件 CollaborationCancelEvent

1.8.5. 示例

  • 第一步:定义事件,并定义好所需要的数据
   public class CollaborationStartEvent extends Event {
        private static final long serialVersionUID = -8411990532000425368L;
        public CollaborationStartEvent(Object source) {
                  super(source);
        }

        private Long summaryId;
        private String from;

        //getter
        //setter
}
  • 第二步:在协同发起代码中触发事件
   public class CollaborationController extends BaseController{  
       public ModelAndView send(HttpServletRequest request,  
                                HttpServletResponse response) throws Exception{  
           …….  
          //协同发起事件通知  
           CollaborationStartEvent event = new CollaborationStartEvent(this);  
           event.setSummaryId(colSummary.getId());  
           event.setFrom("pc");  
           EventDispatcher.fireEvent(event);  
           ……  
       }  
   }
  • 第三步:其它监听事件

    public FormEventListener{
        @listenEvent(event= CollaborationStartEvent.class)
        public void onCollaborationStart(CollaborationStartEvent event){
            //event.getSummartId()
        }
    }
    
    <bean id="" class="com.seeyon.apps.form.FormEventListener">
        ……
    </bean>
    

监听模式:

// 异步监听,异步方式执行监听代码,出现异常不影响业务执行
@ListenEvent(event = CollaborationStartEvent.class,async=true)
// 事务成功时触发,如果事务提交出现异常,不会调用对应的代码。
@ListenEvent(event = CollaborationStartEvent.class,mode=EventTriggerMode.afterCommit)

1.9. After

After反向监听注解,指定Manager或Controller的特定方法执行以后,主动执行当前方法。 比如下面的例子中,SpaceManagerImpl.saveSpaceSort调用以后会执行当前的updateSpace方法。 支持Class的{CanonicalName}.{methodName},也支持使用Spring的beanName替代CanonicalName, 如linkSystemManager.saveLinkSystem、main.do.main。 所有加了After注解的方法都可以登录系统管理员,在系统监控的EventDump中查看到。 注意:After只监听Controller以及Ajax直接调用的Manager。 如果是Controller里调用了Manager A,或者Ajax调用Manager B,B Manager再调用Manager A, 对于Manager A的After均不会触发。

    @After({"com.seeyon.ctp.portal.space.manager.SpaceManagerImpl.saveSpaceSort",
        "com.seeyon.apps.project.controller.ProjectController.saveProject",
        "/collTemplate/collTemplate.do.saveCollaborationTemplate",
        "linkSystemManager.saveLinkSystem"
        })
    public void updateSpace(){
    }

如果需要传递参数,需要按下面的格式声明(Since 7.0)

@After(value = {"com.seeyon.ctp.login.controller.MainController.index", "main.do.main",
            "sectionManager.doProjection" }, withParameters = true)

1.10. 对象缓存

总的原则,少用对象缓存,能不用则不用。

1.10.1. 缓存的界定

  1. 静态成员变量集合,在方法中有增删改操作; 只在系统初始化时加载,运行过程中不改变的可以不做缓存同步。
  2. Singleton的成员变量集合,有增删改操作;
  3. Singleton的成员变量,有修改值操作;
  4. 锁(如表单和协同的操作锁); 使用数据库保存锁状态的可以不考虑同步。
  5. 自增变量。 如果在内存中维护自增值,就必须做集群的改造。高频访问的自增变量在集群模式下必须使用数据库仿照应用锁机制自行管理。
  6. 定时任务。 可以改用Quartz组件,它已经支持集群。

平台提供了两种同步机制来确保缓存的一致性:

缓存组件和通知同步。

缓存组件适用于小对象,它们在集群节点间进行完整传输,不会给网络造成太大负载。 通知同步适用于大数据量或需要频繁进行细粒度更新的场合,比如要从数据库完全重新加载整个缓存,如果使用缓存组件就太奢侈了,这时候,我们可以使用通知同步机制,发送一个通知给其它节点,让它们也进行一次缓存重载。 如果有内存占用较大需要缓存的对象,请不要直接使用缓存组件,可以使用通知同步,传递唯一标识对象的id,让别的节点根据id自行加载(也可使用缓存组件的DataLoader)。

1.10.2. 缓存组件

组件提供三种缓存数据结构

1、 CacheMap 与java.util.Map一致的数据结构,支持集群同步,对CacheMap的任何变更都会同步到集群的其他节点.

2、 CacheSet 类似CacheMap,替代java.util.Set

3、 CacheObject 缓存单一变量

img

详细调用请参照API文档,下面以CacheMap为例来说明缓存的使用。

    // 取得缓存管理工厂实例
    private CacheAccessable factory = CacheFactory.getInstance(MyClass.class);

    //创建缓存
    private CacheMap<String,String> cache = factory.createMap("first");
    //...
    //使用缓存
    cache.put("aKey",value);
    //...
    String value = cache.get("aKey");

缓存的创建方式类似Log4J,以当前类的class作为组名称隔离不同的缓存,createMap的参数定义了缓存的名称。 具体的调用则与Map完全一样。

DataLoader: 对于大对象,直接同步会导致较大的网络开销的,可以建立一个DataLoader,只传递Key,由接收方自行重载缓存。

img

/**
 * 缓存数据加载。为缓存的reload方法提供重新加载缓存的支持。缓存调用者需要自己实现此接口。
* @see AbstractMapDataLoader
 */
public interface DataLoader<K extends Serializable> {
    /**
     * 重新加载整个缓存。
     */
    void load();

    /**
     * 重新加载指定Key的缓存项。
     * 
     * @param key
     *            缓存的key。
     */
    void load(K key);
}

DataLoader开发示例

        cache.setDataLoader(new AbstractMapDataLoader<String, String>(cache) {
            @Override
            protected Map<Long, String> loadLocal() {
                Map<String, String> map = new HashMap<String, String>();
                //    从数据库中加载所有数据,组织为与缓存一致的结构

                ……
            return map;
            }
            @Override
            protected String loadLocal(String key) {
            //    从数据库中加载指定数据,返回
                final String value = dao.get(key);
                return value;
            }
        });

isSkipFillData(Since V7.0):由于新的缓存组件(Geode)构建的缓存对象(如果是Global类型的)在集群环境下是自动同步的,只要加入集群中会自动从集群节点中同步相关缓存的数据,针对这种情况,增加该方法以便应用构造缓存对象时判断是否需要载入初始化数据,从而避免系统启动 (通常为从节点启动或者主节点重启)时重复加载数据造成浪费。

 if(!CacheFactory.isSkipFillData()){
     // 从数据库中加载组织模型缓存
 }

缓存的注意事项

  1. 控制缓存的大小,不要把一棵树放到缓存里,内存占用和同步代价都会很大。

  2. 控制缓存更新的频次,避免过大的东西流量影响系统稳定性。

  3. 还有一个著名的坑

    CacheMap<String,Map> cache;
    Map value = cache.get(key);
    value.put(xxx,xxx);
    cache.put(key,value);
    
    // 上面一切OK,下面就开始失控了
    value.put(xxx,xxx)// end
    

    这样的代码会导致各节点的缓存不一致,因为你取出来的value已经脱离缓存组件的控制了,value.put修改操作修改了本地JVM的值,但远程节点未得到同步。

    // 正确的做法是value变更以后告知缓存组件
    cache.notifyUpdate(key);
    // 或者再进行一次put
    cache.put(key,value)

1.11. 加解密

com.seeyon.ctp.common.encrypt.CoderFactory

/**
 * 加密深度  
 * 
 * @return 轻度加密:{@link ICoder#VERSON01};
 * 深度加密:{@link ICoder#VERSON02};
 * 不加密:<code>no</code>
 */

public String getEncryptVersion() ;

1.12. 应用锁

应用锁组件用于控制并发修改

比如协同流程并发修改和公文正文并发编辑控制。

为应用提供统一的独占锁,管理用户、资源、操作三者之间的关系。 只控制锁的状态(加锁、解锁、锁失效、锁判断),不实际对资源进行锁定,资源和操作的锁定由应用自己管理。

为同时适应单机和集群环境,提升性能,提供内存锁和数据库锁两种模式。 单机环境使用内存保存锁;集群环境使用数据库保存锁。

1.12.1. 名词解释

 模块:需要加锁的应用。如协同、公文、表单,可以自行指定一个字符串,如ColLock、FormLock作为标识。

 资源:被锁定对象。可以是协同、公文、表单,甚至是人员。

 操作:被锁定对象的行为。对于流程,可以是处理、加签减签等。

1.12.2. 功能

功能一: 锁定资源 特定用户锁定资源,解锁或锁失效之前其他用户不允许访问资源。 锁定资源后指定资源只允许加锁者独占使用。

功能二: 锁定资源的操作 特定用户锁定资源的操作,解锁或锁失效之前其他用户不允许进行资源的操作。

功能三: 解锁 解除资源的所有锁或解除资源特定操作的锁。

功能四: 锁判断 判断指定用户是否可以访问资源或资源的操作。 这里会有一定歧义:对资源加锁是锁定了资源的所有操作,还是操作为空本身就是一种操作。 在这里不做处理,由应用自己解释。如果有特定的解释,请不要使用组件提供的锁判断方法,取得资源的所有锁后自己判断。

功能五: 锁过期 提供了锁过期的策略,缺省过期时间设置为8小时,以后可以进行扩展。

功能六: 锁失效 以下三种情况会导致锁失效,失效的锁将被系统自动清理。

 锁过期;

 加锁人不在线;

 加锁人虽然在线,但加锁以后登出过(加锁时间<登录时间)。

1.12.3. 调用

  1. 应用如果要使用锁组件,确定模块编号以后,在Spring配置文件中定义一个新的bean,如下所示
   <bean id="formLock" parent="lockManager">
       <property name="module" value="formLock"/>
   </bean>
  1. 然后在自己的class中引用定义的bean即可。
   <bean id="formManager" class="com.seeyon.v3x.form.FormManagerImpl">
       <property name="formLock" ref="formLock"/>
   </bean>
  1. 加锁
   public class FormManagerImpl {
       private long owner;
       private LockManager formLock;
       private long resourceId;
       private int action = -1;


       public long getFormLock() {
           return formLock;
       }

       public void setFormLock(LockManager formLock) {
           this.formLock = formLock;
       }

       public long lock( long memberId,long formId) {
           formLock.lock(memberId,formId);
       }
   }

详细信息请参考相关API文档

1.12.4. 最佳实践

1、控制编辑工作流流程业务并发操作

// 请求1:打开编辑流程页面时
long resourceId;
long userId = AppContext.currentUserId();
boolean result = lockManager.check(userId,resourceId);

if(result){
    // 可操作
    result = lockManager.lock(userId, resourceId);
}

if(!result){
    // 不可操作或加锁失败
    // 提示前端用户
}

//-------------------------------------------------------------//
// 请求2:关闭编辑流程页面时,调用解锁
lockManager.unlock(resourceId);

2、控制集群环境的用户并发操作

long resourceId;
long userId = AppContext.currentUserId();
boolean result = lockManager.check(userId,resourceId);

// 可操作
if(result){
    result = lockManager.lock(userId, resourceId);
}

if(!result){
    // 不可操作或加锁失败
    // 提示前端用户重试,也可以采取3的方式等待一段时间重试,但考虑用户体验,时间不宜过长
}else{
    try{
        doSomething();
    } catch (Exception e) {
        // ...
    }finally {
        // 完成业务操作后解锁
        lockManager.unlock(resourceId);
    }  
}

3、控制集群环境的后台并发操作

long resourceId;
long userId = AppContext.currentUserId();
boolean result = checkAndLock(userId,resourceId);
if(!result){
    // 不可操作或加锁失败
    // 等待重试 如果能确保不会死锁,也可以while(true)
    for (int i = 0; i < 20; i++) {
        if(!checkAndLock(userId,resourceId)){
           Thread.sleep(1000);
        }
    }
}
if(result){
    try{
        doSomething();
    } catch (Exception e) {
        // ...
    }finally {
        // 完成业务操作后解锁
        lockManager.unlock(resourceId);
    }  
}else{
    // 无法获取锁,抛出异常
}

private boolean checkAndLock(long userId,long resourceId){
    boolean result = lockManager.check(userId,resourceId);
    // 可操作
    if(result){
        result = lockManager.lock(userId, resourceId);
    }
    return result;
}

1.13. 定时任务 Quartz

定时任务分为两种,一种是业务定时任务,另一种是系统定时任务

Table 定时任务分类

名称 使用范围 技术特征 举例
业务定时任务 应用中的定时任务 定时任务将被持久化 提前提醒,超期提醒
系统定时任务 异步的调度系统 定时任务将在系统启动初生成,shutdown是终止,不会被持久化 定时从移动网关取短信内容

1.13.1. 业务定时任务

  1. 核心类com.seeyon.ctp.common.quartz.QuartzHolder

  2. 基本原理

    定时任务采用异步处理模式,在生成定时任务的时候,需要指定:任务处理器Bean + 参数。 任务处理器Bean是解释执行特定任务的JavaBean,通过参数来描述业务特征

  3. 接口规范

    • 定义任务处理,实现com.seeyon.ctp.common.quartz.QuartzJob接口
     class ABCQuartz implement QuartzJob{

     public void execute(Map<String, String> parameters){

              Long id = parameters.get("id");

              ...

     }

     }

     <bean name="abcQuartz" class="package.ABCQuartz" >
  • 生成/注册任务

    调用接口/方法: com.seeyon.ctp.common.quartz.QuartzHolder

    Map<String, String> parameters = new HashMap<String, String>();
    
    parameters.put("id", String.valueOf(id));
    
    QuartzHolder.newQuartzJob("jobName", new Date(109, 1, 1), "abcQuartz", parameters);
    
  • 任务查找和删除

    /**
     * 检测任务是否存在
     * 
     * @param name 任务名称
     * @return
     */
    public static boolean hasQuartzJob(String name);
    /**
     * 删除任务
     * 
     * @param name 任务名称
     * @return
     */
    public static boolean deleteQuartzJob(String name);
    

    | [Caution] | Caution | | ---------------------------------------- | ------- | | 特别注意 任务名称要求唯一,即每次调用newQuartzJob时,参数name都是不同的,即使jobBeanId是一致的。如:协同的提前提醒,jobBeanId是同一个,但每一个节点的name、parameters都不是一样的,也就是说这不属于同一个任务。目的:方便任务解除。 | |

1.13.2. 系统定时任务

  1. 核心类com.seeyon.ctp.common.timer.TimerHolder

  2. 定义任务处理器

    class SampleTask implements Runnable{
    
             public void run(){
    
                     ...
    
             }
    
    } 
    
    <bean name="sampleTask" class="package.SampleTask" >
    
  3. 生成/注册任务

    public void StartTask() {
    
             TimerHolder.newTimer(sampleTask, 60 * 60 * 1000);
    
    }
    

1.14. 文件(附件)管理JAVA 接口

此部分只描述JAVA程序可用的附件接口,不描述前端附件组件,附件组件的完整用法,请参考前端组件部分。

后端提供如下接口:

  1. 对输入流的下载。
  2. 对附件的删除。

1.14.1. 对输入流下载的接口

  1. FileUploadUtil.downLoadStream(……)
   FileUploadUtil工具类

     /**
        * 对输入流直接下载
        * 例如对需要导出的数据组织成输入流,对该流直接下载。
        * 在request中需要设置下面属性:filename (即 request.setAttribute("filename","某某数据"))
        * @param request
        * @param response
        * @param in
        * @return
        * @throws BusinessException
        */
    public static ModelAndView downLoadStream(HttpServletRequest request, HttpServletResponse response,InputStream in) throws BusinessException;

1.14.2. 删除附件的接口

  1. attachmentManager.removeByReference(……)

    attachmentManager.deleteById(……)

            /**
             * 按照主数据和次数据删除: 文件做物理删除 
             * @param reference
             * @param subReference
             */
            public void removeByReference(Long reference, Long subReference)
            throws BusinessException;


            /**
             * 按照附件Id删除,非物理删除
             * @param attachmentId
             */
            public void deleteById(long attachmentId);

1.15. 文件压缩/解压

支持ZIP格式文件的压缩和解压

import com.seeyon.ctp.util.ZipUtil;
// 压缩整个文件夹
ZipUtil.zip(new File("d:\\新建文件夹"), new File("c:\\新建文件夹.zip"), true);
// 压缩指定文件夹下所有文件
ZipUtil.zip(new File("d:\\新建文件夹"), new File("c:\\新建文件夹.zip"), false);
// 压缩一组文件
ZipUtil.zip(Collection<File>, new File("c:\\新建文件夹.zip"));
// 解压缩zip包
ZipUtil.unzip(File zipFile, File directory);

1.16. 消息接口

1.16.1. 发送消息接口

注入消息接口

  • userMessageManager

例如:

<bean class="com.seeyon.app.collaboration.manager.impl.ColManagerImpl"> 
<property name="userMessageManager" ref="UserMessageManager" /> 
</bean>

接口说明

几种接口形式:
/** 
  * 发送系统消息,发送给单个接收者
  * @param content  消息体 
  * @param messageCategroy 消息所属应用分类 在com.seeyon.ctp.common.constants.ApplicationCategoryEnum中定义 
  * @param senderId  发送者ID 
  * @param receiver  接收者 
  * @param messageFilterArgs  消息转移的参数,与对应的UserMessageFilter对应 
  * @see com.seeyon.ctp.common.constants.ApplicationCategoryEnum 
  * @throws MessageException 
  */ 
public void sendSystemMessage(MessageContent content, 
    ApplicationCategoryEnum messageCategroy, long senderId, 
    MessageReceiver receiver, Object... messageFilterArgs) throws MessageException; 

/** 
  * 发送系统消息,发送给多个接收者
  * @param content  消息体 
  * @param messageCategroy 
  *                       消息所属应用分类 
  *                     在com.seeyon.ctp.common.constants.ApplicationCategoryEnum中定义 
  * @param senderId  发送者ID 
  * @param receivers  接收者 
  * @param messageFilterArgs  消息转移的参数,与对应的UserMessageFilter对应 
  * @see com.seeyon.ctp.common.constants.ApplicationCategoryEnum 
  * @throws MessageException 
  */ 
public void sendSystemMessage(MessageContent content, 
    ApplicationCategoryEnum messageCategroy, long senderId, 
    Collection<MessageReceiver>  receivers,  Object...  messageFilterArgs)  throws 
MessageException; 

/** 
  * 发送系统消息,使用指定的时间发送给多个接收者
  * @param content   消息体 
  * @param messageCategroy 
  *                       消息所属应用分类 
  *                       在com.seeyon.ctp.common.constants.ApplicationCategoryEnum中定义 
  *                       如果是插件,需要在插件定义文件中配置applicationCategory属性 
  * @param senderId  发送者ID 
  * @param creationDate  发送时间 
  * @param receivers  接收者 
  * @param messageFilterArgs  消息转移的参数,与对应的UserMessageFilter对应 
  * @see com.seeyon.ctp.common.constants.ApplicationCategoryEnum 
  * @throws MessageException 
  */ 
public void sendSystemMessage(MessageContent content, int messageCategroy, 
    long senderId, Date creationDate, 
    Collection<MessageReceiver> receivers, Object... messageFilterArgs) 
    throws MessageException;

接口使用注意事项

  • 如果:MessageContent相同,接收者为多人的情况下,禁止通过forEach 方式调用sendSystemMessage方法发送消息 ;而应该使用多接受者的接口。
  • 如果MessageContent的Subject是动态的,比如:处理协同的状态,通过消息定义来解决。

接口参数说明:

MessageContent 构造消息体

MessageContent 消息体,主要封装国际化key以及参数。国际化内容放在各插件的中的国际化文件中,不需要在消息组件中的国际化文件中增加。

几种构造方法说明:

  • 方式一: new MessageContent(String key, Object... param)
  • 方式二:MessageContent.get(String key, Object... param)
  • 方式三:new MessageContent() .add(String key, Object... params)……

    例如:
    MessageContent messageContent = new MessageContent() 
      .add("col.send", subject, sender.getName()) 
    .add("col.agent") 
    .setBody(bodyContent, bodyType, bodyCreateDate);
    

设置正文,主要用于email的正文

  • MessageContent.setBody(String bodyContent, String bodyType, Date bodyCreateDate)

设置重要程度:1普通,2重要,3非常重 要

  • MessageContent.setImportantLevel(Integer)

MessageReceiver 消息接收者

MessageReceiver 消息接收者,主要封装关联主体对象id、接收人Id、链接类型及链接参数

几种构造方法说明:

  • 每个接收者的链接地址都不一样,通过ForEach单个构造

    MessageReceiver MessageReceiver.get(Long referenceId, long receiverId, String linkType, String... linkParam)

  • 一个接收者,但没有连接

    MessageReceiver MessageReceiver.get(Long referenceId, long receiverId)

  • 多个接收者,一样的链接

    List get(Long referenceId, List receiverIds, String linkType, String... linkParam)

  • 多个接收者,都没有链接

    List get(Long referenceId, List receiverIds)

1.17. 全文检索

1.17.1. 环境准备

准备开发所需环境:

  • 加载lucene相关开源包,路径:WEB-INF/lib:

    lucene-*.jar
    
  • 加载全文检索组件:

    加载jar包,路径:WEB-INF/lib:

    seeyon-ctp-index.jar
    

​ 加载插件,路径:

​ WEB-INF/cfgHome/plugin/index

​ WEB-INF/cfgHome/plugin/indexResume

​ 加载页面,路径:

​ WEB-INF/jsp/index

​ WEB-INF/jsp/indexresume

​ apps_res/index

1.17.2. 开发准备

实现IndexEnable接口,开发人员可以参考原来各自模块关于全文检索的manager类,通过继承,可以少写很多。

  • IndexEnable接口的类介绍

    package com.seeyon.apps.index.manager;
    public interface IndexEnable {
      /**
       * 设置各应用的类别名称,参见{@link ApplicationCategoryEnum}
       * @return 各应用自身的key值
            */
              public Integer getAppEnumKey();
    
      /**
       * 根据ID获取索引信息
       * @param id 各应用的实体ID
       * @return
       * @throws BusinessException
            */
              public IndexInfo getIndexInfo(Long id)throws BusinessException;
    
      /**
       * 获取所有需要重建索引的实体数量
       * @param starDate 开始日期
       * @param endDate  结束日期
       * @return
       * @throws BusinessException
            */
               public Integer findIndexResumeCount(Date starDate, Date endDate)throws BusinessException;
    
      /**
       * 获取所有需要重建索引的实体ID集合
       * @param starDate
       * @param endDate
       * @param firstRow
       * @param pageSize
       * @return
       * @throws BusinessException
            */
              public List<Long> findIndexResumeIDList(Date starDate, Date endDate, Integer firstRow,Integer pageSize)throws BusinessException;
    
      /**
       * 获取实体的某些信息
       * @param entityId
       * @return Map的key说明:
       * IndexEnable.FOLDER_ID 所属文档夹ID
       * IndexEnable.SOURCE_ID 实体的源ID:协同、表单、公文为affairId  文档为sourceId
       * IndexEnable.SOURCE_PATH 所处位置路径
       * @throws BusinessException
            */
              public Map<String,Object> findSourceInfo(Long entityId)throws BusinessException;
            }
    
  • getAppEnumKey():设置各应用的类别名称,比如:

     @Override
      public Integer getAppEnumKey() {
          return ApplicationCategoryEnum.doc.getKey();
      }
    
  • getIndexInfo(Long):根据实体ID获取索引信息,这个方法可以参考原来各模块的实现,挪移过来就可

  • findIndexResumeCount(Date, ​ Date):根据开始、结束时间,查找此断时间内创建的实体熟练,语句如下:

    "select count(id)  from  table where  properties>=? and properties<=? "+other
    

参数说明

模块 table properties other 备注
collaboration ColSummary createDate and bodyType<>'FORM' -
form ColSummary createDate and bodyType='FORM' -
doc DocResource createTime - -
plan Plan createTime - -
organization OrgMember createTime - -
taskManage TaskInfo createTime - -
meeting MtMeeting createDate - -
bulletin BulData createDate - -
news NewsData createDate - -
calendar CalEvent createDate - -
bbs V3xBbsArticle issueTime - -
inquiry InquirySurveybasic sendDate - -
  • findIndexResumeIDList(Date, Date, Integer,Integer):分页查询时间段内的实体ID集合,语句如下:

    return (List<Long>) getHibernateTemplate().execute(
        new HibernateCallback() {
            public Object doInHibernate(Session session)throws HibernateException, SQLException {
                String member = "";
                if (base.equals(V3xOrgMember.class.getName())) {
                    member = "isDeleted ='0' and enabled='1' and ";
                }
                String hql = "select id from  base where properties>=? and properties<=? "+other+" order by properties desc";
                Query query = session.createQuery(hql);
                query.setParameter(0, starDate);
                query.setParameter(1, endDate);
                query.setFirstResult(fromIndex);
                query.setMaxResults(pageSize);
                return query.list();
            }
        });
    
  • findSourceInfo(Long): 获取实体的某些信息:

    文档:IndexEnable.FOLDER_ID:所属文档夹ID

    ​ IndexEnable.SOURCE_ID:实体ID,sourceId

    ​ IndexEnable.SOURCE_PATH:文档路径

    协同、表单、公文:IndexEnable.SOURCE_ID:代办事项ID,affairId

    其他模块无须实现。

1.17.3. 索引入库

本节介绍如何在开发中调用接口实现索引入库,以便检索。

全文检索接口说明:

  package com.seeyon.apps.index.manager;
  public interface IndexManager {

    /**
     * 新增索引信息
     * @param entityId  实体ID
     * @param appType  实体类别 参见{@link ApplicationCategoryEnum}
     * @throws BusinessException
     */
    public void add(Long entityId, Integer appType) throws BusinessException;

    /**
     * 新增索引信息
     * @param indexInfo  索引信息
     * @throws BusinessException
     */
    public void add(IndexInfo indexInfo)throws BusinessException;

    /**
     * 修改索引信息,使用先删后增策略
     * @param entityId  实体ID
     * @param appType  实体类别 参见{@link ApplicationCategoryEnum}
     * @throws BusinessException
     */
    public void update(Long entityId, Integer appType) throws BusinessException;

    /**
     * 修改索引信息,使用先删后增策略
     * @param indexInfo  索引信息
     * @throws BusinessException
     */
    public void update(IndexInfo indexInfo) throws BusinessException;

    /**
     * 修改索引信息 ,使用部分信息更新策略,无需获取应用的索引获取操作
     * @param entityId  实体ID
     * @param appType  实体类别 参见{@link ApplicationCategoryEnum}
     * @param param 需要修改的数据信息
     * @throws BusinessException
     * @deprecated 暂时不用
     */
    public void update(Long entityId, Integer appType, Map param) throws BusinessException;
    /**
     * 删除索引信息
     * @param entityId  实体ID
     * @param appType  实体类别 参见{@link ApplicationCategoryEnum}
     * @throws BusinessException
     */
    public void delete(Long entityId, Integer appType) throws BusinessException;

    /**
     * 单索引库查询
     * @param userInfoStr
     * @param keyMap
     * @param indexLib
     * @param startPoint
     * @return
     * @throws IndexException
     */
    public SearchResultWapper search(String userInfoStr, Map keyMap,
            ApplicationCategoryEnum indexLib, int startPoint)
            throws BusinessException;

    /**
     * 多库查询
     * @param userInfoStr 当前用户的所有组织ID
     * @param keyMap
     * @param indexLib 索引库标识集合数组
     * @param startPoint
     * @return
     * @throws IndexException
     */
    public SearchResultWapper search(String userInfoStr, Map keyMap,
            String[] indexLib, int startPoint)
            throws BusinessException;
            }

全文检索接口调用

  • 新增索引

    try {
              if (AppContext.hasPlugin("index")) {
                  indexManager.add(id, ApplicationCategoryEnum.doc.getKey());
              }
          } catch (Exception e) {
              logger.error("新增文档[id=" + id + "]全文检索信息时出现异常:", e);
          }
    
  • 修改索引

    try {
              if (AppContext.hasPlugin("index")) {
                  indexManager.update(id, ApplicationCategoryEnum.doc.getKey());
              }
          } catch (Exception e) {
              logger.error("更新文档[id=" + id + "]全文检索信息时出现异常:", e);
          }
    
  • 删除索引

    try {
              if (AppContext.hasPlugin("index")) {
                  indexManager.delete(drId, ApplicationCategoryEnum.doc.getKey());
              }
          } catch (Exception e) {
              logger.error("删除文档[id=" + id + "]全文检索信息时出现异常:", e);
          }
    

索引检索

菜单: 常用工具 -> 全文检索

地址: http://ip:port/context/index/indexController.do?method=searchAll

results matching ""

    No results matching ""