1. 基础架构

1.1. 字符集

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

  • 数据库
  • 文件:java、properties、jsp、js、css、htm等等
  • servlet:response.setContentType(“text/html; charset=UTF-8”);
  • HTML:
  • JSP:<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

1.2. 代码结构

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
WebContent
    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配置文件

一个模块的开发步骤

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

1.3. MVC

1.3.1. Controller层

需要继承BaseController,如:

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 TestController extends BaseController {
    public ModelAndView index(HttpServletRequest request, HttpServletResponse response) throws Exception {
        return new ModelAndView("apps/samples/hello");
    }
    public ModelAndView edit(HttpServletRequest request, HttpServletResponse response) throws Exception {
    }
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response) throws Exception {
    }
}

在插件的Spring配置文件中定义urlmapping如:

src/main/webapp/WEB-INF/cfgHome/plugin/samples/spring/spring-samples-controller.xml

<beans default-autowire="byName">
    <bean name="/sample/test.do" class="com.seeyon.apps.sample.SampleController" />
</beans>

上面定义的Controller可以通过下面的url访问:

http://[host]:[port]/[context]/samples/test.do?method=testAjax

1.3.2. Manager层

定义Manager接口

public interface TestManager {
    void test() throws BusinessException;
}

实现Manager

public class TestManagerImpl implements TestManager {
    public void test() throws BusinessException{
        // doSth
    }
}

Spring配置

<bean name="testManager" class="com.seeyon.apps.samples.TestManager" />

1.3.3. DAO层

数据访问采用DBAgent,旧的应用可以继续沿用继承BaseDAO的方式,手册中不再对BaseDao作特别的说明。

可使用NamedQuery方式管理HQL语句,必须使用PrepareStatement方式书写HQL,不允许拼写静态HQL,以避免出现SQL注入。

因为我们规范禁止建立关系映射,所以HQL中不能书写明确的JOIN语句。

请控制好查询条件,优化索引,保证查询的性能。

事务控制

  1. 使用全系统统一命名方法控制

    • 有事务的(REQUIRED)

      save*

      insert*

      delete*

      update*

      trans*

      test*

      import*

      add*

      create*

      ......

    • 无事务的(SUPPORTS)

      is*

      check*

      find*

      get select

      list*

      query*

      on*

      copy*

      ......

    详细可参考配置文件:/ctp-core/src/main/webapp/WEB-INF/cfgHome/spring/spring-default.xml

  2. 使用注解控制

    7.0支持注解配置事务,主要用于性能优化,消除不必要的事务,注解和XML配置了的,注解优先。

    @Transactional(propagation = Propagation.SUPPORTS, rollbackFor = com.seeyon.ctp.common.exceptions.BusinessException.class)
        @Override
        public void updateETagDate(String category, String key) {
    
        }
    

基本CRUD操作

DBAgent.save(myPO);  //新增保存PO实体对象
DBAgent.delete(myPO); //删除PO实体对象
DBAgent.update(myPO);//更新PO实体对象
DBAgent.get(MyPO.class,id);//根据PO类型和主键ID查询PO实体对象
DBAgent.loadAll(MyPO.class);//根据PO类型加载全部数据,数据量大时不建议使用
DBAgent.loadAll(MyPO.class,flipInfo);//根据PO类型进行翻页查询,需要构造FlipInfo信息参数

查询操作

原则上不允许Manager层里出现HQL,可以利用Hibernate的NamedQuery特性,将HQL放入hbm配置文件中进行统一管理和维护,该文件可以放在应用/平台代码包的po下的

子包中,框架将自动进行加载,比如com.seeyon.apps.samples.po.myquery.MyQuery.hbm.xml,相当于myquery子包中的MyQuery.hbm.xml充当了DAO的角色

<?xml version="1.0" encoding="gb2312"?>
<!DOCTYPE hibernate-mapping PUBLIC 
  "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
  "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<query name="samples_hibernate_findAll"><![CDATA[
    from Org o
]]></query>
<query name="samples_hibernate_findByOrgname"><![CDATA[
    from Org o where orgname like :orgname
]]></query>
<query name="samples_hibernate_findByOrgnames"><![CDATA[
    from Org o where orgname like :orgname or orgname=:oname
]]></query>
    <query name="samples_hibernate_findPerson"><![CDATA[
    from A6User a where a.truename like :name
]]></query>
</hibernate-mapping>

代码调用如下

//根据queryName进行查询操作,如果返回数据量过大不允许使用
DBAgent.findByNamedQuery(queryName);
//根据queryName和命名参数进行查询操作,如果返回数据量过大不允许使用
DBAgent.findByNamedQuery(queryName, params);
//根据queryName和命名参数进行翻页查询操作,需要构造FlipInfo翻页信息类
DBAgent.findByNamedQuery(queryName, params, flipInfo);
//根据queryName和ValueBean进行查询操作,Bean中的属性名将作为HQL中的命名参数,如果返回数据量过大不允许使用
DBAgent.findByNamedQueryAndValueBean(queryName, valueBean);
//根据queryName和ValueBean进行翻页查询操作,Bean中的属性名将作为HQL中的命名参数,需要构造FlipInfo翻页信息类
DBAgent.findByNamedQueryAndValueBean(queryName, valueBean, flipInfo);

在某些情况下需要直接传入HQL进行查询操作,比如动态拼装HQL语句时,可以调用DBAgent的find函数,但只允许在DAO层出现HQL语句,不允许在Manager中使用

//根据传入的HQL进行查询操作,如果返回数据量过大不允许使用
DBAgent.find("from Org");
//根据传入的HQL和命名参数进行查询操作,如果返回数据量过大不允许使用
DBAgent.find("from Org where orgname like :orgname", params);
//根据传入的HQL和命名参数进行翻页查询操作,需要构造FlipInfo翻页信息类
DBAgent.find("from Org where orgname like :orgname", params, flipInfo);

内存数据分页查询方法如下:

DBAgent.memoryPaging(dataList, flipInfo);

批量插入/更新/删除

//批量新增插入PO对象数据
DBAgent.saveAll(pos);
//批量删除PO对象数据
DBAgent.deleteAll(pos);
//批量修改PO对象数据
DBAgent.updateAll(pos);

这些批量操作性能在普通台式电脑的测试环境下的性能如下(单位:毫秒,仅供参考,在不满足性能要求的情况下需要考虑其它技术实现策略):

一百条数据:    SaveAll:178;UpdateAll:100;DeleteAll:82
五百条数据:    SaveAll:318;UpdateAll:192;DeleteAll:353
一千五数据:    SaveAll:518;UpdateAll:590;DeleteAll:842
三千条数据:    SaveAll:735;UpdateAll:513;DeleteAll:1575
一万条数据:    SaveAll:1282;UpdateAll:1183;DeleteAll:4750
十万条数据:    SaveAll:9155;UpdateAll:11245;DeleteAll:44919

另外,如果需要根据某些特定条件进行批量更新或删除操作时,可以采用如下方法:

DBAgent.bulkUpdate("delete from Org where orgid>? and orgid<?", 200L, new Long(210));
DBAgent.bulkUpdate("update from Org set orgname=? where orgid>? and orgid<?", "测试", 210L, new Long(220));

DBAgent.bulkUpdate方法将根据传入的delete或update的HQL语句,以及?条件参数值进行批量删除或更新操作。因为要显式传入HQL,所以该方法只允许在DAO中调用。

Blob/Clob操作

Blob新增保存示例代码如下:

public void testSaveBLob() throws BusinessException {
    Lobtest lob = new Lobtest();
    long id = 1L, start = System.currentTimeMillis(), end;
    String file = "e:\\MyProjects\\test.rar";
    lob.setTid(id);
    try {
        InputStream is = new FileInputStream(new File(file));
        byte[] bytes = IOUtility.toByteArray(is);// 将要保存的二进制数据转为byte数组


        is.close();
        lob.setTblob(bytes); // 将byte数组设置到PO对象中
        DBAgent.save(lob); // 保存PO对象即可
        end = System.currentTimeMillis();
        System.out.println("Saved:" + (end - start));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Blob读取和更新例子代码如下:

public void testGetAndUpdateBLob() throws BusinessException {
    Lobtest lob = new Lobtest();
    long id = 1L, start = System.currentTimeMillis(), end;
    String file = "e:\\MyProjects\\test.rar";
    String ofile = "e:/test.rar";
    lob.setTid(id);
    try {
        lob = (Lobtest) DBAgent.get(Lobtest.class, id);
        byte[] bytes = lob.getTblob();
        OutputStream os = new FileOutputStream(new File(ofile));
        IOUtility.copy(bytes, os);
        os.flush();
        os.close();
        end = System.currentTimeMillis();
        System.out.println("Readed:" + (end - start));
        start = end;

        InputStream is = new FileInputStream(new File(file));
        bytes = IOUtility.toByteArray(is);
        is.close();
        lob.setTblob(bytes);
        DBAgent.update(lob);
        end = System.currentTimeMillis();
        System.out.println("Updated:" + (end - start));
        start = end;
    } catch (Exception e) {
        e.printStackTrace();
    }
}
[1] 正常查询PO对象,获取Blob数据的byte数组
[2] 用新的byte数组更新Blob数据
[3] 更新保存PO即可更新Blob数据

Clob新增保存例子代码如下:

public void testSaveCLob() throws BusinessException {
    long id = 2L, size = 100000, start = System.currentTimeMillis(), end;
    Lobtest lob = new Lobtest();
    lob.setTid(id);
    StringBuffer sb = new StringBuffer((int) size);
    for (int i = 0; i < size; i++)
        sb.append('a');
    lob.setTclob(sb.toString());
    DBAgent.save(lob);
    end = System.currentTimeMillis();
    System.out.println("Saved:" + (end - start));
}
将要保存的Clob数据作为String传入 1
正常保存PO对象即可保存Clob数据 2

Clob读取和更新例子代码如下:

public void testGetAndUpdateCLob() throws BusinessException {
    long id = 2L, size = 100000, start = System.currentTimeMillis(), end;
    Lobtest lob = (Lobtest) DBAgent.get(Lobtest.class, id);
    end = System.currentTimeMillis();
    System.out.println("Readed:" + lob.getTclob().length() + ":" + (end - start));
    start = end;
    StringBuffer sb = new StringBuffer((int) size);
    for (int i = 0; i < size; i++)
        sb.append('b');
    lob.setTclob(sb.toString());
    DBAgent.update(lob);
    System.out.println("Updated:" + (end - start));
    start = end;
}
正常查询PO对象,获取Clob数据作为String 1
用新的String数据更新Clob数据 2
更新保存PO即可更新Clob数据 3

查询记录数和是否存在数据的工具方法

DBAgent提供了查询记录数的方法int count(String hql)和int count(String hql, Map params)

DBAgent提供了判断是否存在数据的方法boolean exists(String hql)和boolean exists(String hql, Map params)

1.4. 异常处理

框架规范Manager和DAO各层方法应抛出com.seeyon.ctp.common.exceptions.BusinessException,其它类型异常均通过此异常对象包装抛出,框架支持controller和ajax调用的统一异常处理机制。利用该异常对象可以支持提示消息和错误异常两种类型。提示消息类异常例子代码如下:

throw new BusinessException("提示消息");

或者BusinessException内包含无限层级的BusinessException均视为提示型异常消息,前端将以message提示框提示用户

错误型异常为BusinessException中包含了非BusinessException类型的异常的异常,前端将以系统级错误信息提示用户

1.4.1. 异常消息国际化

异常消息国际化例子代码如下:

public BusinessException(String message[^1])
public BusinessException(String i18nKey, Object... i18nArgs[^2])
public BusinessException(Throwable cause, String i18nKey, Object... i18nArgs[^3])
1. 首先会将message作为国际化资源key查找资源,如果存在则做国际化转换,否则直接将message作为异常消息
2. 第一个参数为提示消息国际化资源key,后面为多个国际化资源参数
3. 第一个参数为内嵌异常,第二个参数为异常消息国际化资源key,后面为多个国际化资源参数

1.4.2. 例子

请参考:http://[host]:[port]/[context]/samples/test.do?method=testError

1.5. Ajax

通过Ajax前端组件可以调用后台注册到白名单的Manager的方法。

1.5.1. 后台接口

import com.seeyon.ctp.util.annotation.AjaxAccess;
public interface ColManager {
    // 注册到白名单,白名单中的方法才可以从前端调用
    @AjaxAccess
    public FlipInfo getPendingList(FlipInfo flipInfo,Map<String,String> query)
}

1.5.2. 前端调用

以下为历史版本的Ajax调用方式,新版本请使用无存根调用

<!-- 引入Manager的动态Javascript存根,其中colManager为Spring中注册的bean id,要使用多个manager做ajax操作可将多个bean id之间用逗号分隔 -->
<script type="text/javascript" src="${path}/ajax.do?managerName=colManager"></script>
<script>
$().ready(
    function() {
        var manager = new colManager(); // 构建Manager对象,其中colManager为Spring中注册的bean id,注意变量名不能和bean id重名
        manager.getPendingList({page:1,size:20},{}, {
            success: function(returnVal){
                alert(returnVal);
            }
        };// 调用Manager方法,参数个数和数据类型需与Manager实现保持一致(符合javascript和java之间的类型映射规则),最后增加回调参数时则视为异步ajax调用方式
        var rtVal = tBS.testAjaxBean2(ajaxTestBean);// 方法调用最后不增加回调参数,则视为同步调用,可直接取得ajax返回值(虽然简单,但不建议采用,因为同步调用操作时间长会造成浏览器锁死)
    }
);

1.5.3. 无存根调用方式

出于性能考虑,6.0SP1以后提供无存根的调用方式,可以不引用Ajax存根,以后版本建议使用此方式

示例方法如下

callBackendMethod("colManager","getPendingList",
                  {page:1,size:20},{},
                  {success:function(returnVal){
                    alert(returnVal);
                  }});
// 亦即callBackendMethod("bean名称","方法名称","第一个参数","第二个参数",...,{success:xxx}

1.5.4. 类型映射

  • JavaScript参数映射到Java参数类型
JavaScript类型 Java类型
数组 数组,List
对象 Map,JavaBean
字符串 String
数值 int、long、double、float或封装数据类型
  • Java返回值映射JavaScript类型
Java类型 JavaScript类型
数组、List 数组
Map 对象
JavaBean 对象
基本数据类型(String、int、long、double、float)和封装数据类型 变量

1.6. 资源权限控制

框架提供方便的方式实现根据资源权限控制界面元素显示/隐藏,根据登陆用户具备的资源权限控制按钮、控件、区域是否显示。开发态(systemProperties.xml中ctp.runningMode=develop)

不控制,以方便开发调试。代码例子如下:

<input type="button" class="resCode" resCode="f1_my_resource1" value="按钮1">
<div class="resCode" resCode="f1_my_resource2">
    <input type="text">
    <input type="button" value="按钮2">
</div>

需要在前端JavaScript中判断当前登录用户是否具有某个资源权限,可以在JS中做如下调用:

if($.ctx.resources.contains('f1_my_resource')) // $.ctx.resources为当前登录用户具有权限的资源Code(对应priv_resource资源表的resource_code字段值)数组,contains判断是否具备某个资源权限
    do something...

如果需要在后台进行资源权限判断代码如下:

if(AppContext.getCurrentUser().hasResourceCode("f1_my_resource")) // AppContext.getCurrentUser()获取当前登录用户,hasResourceCode函数判定是否具有指定资源Code(对应priv_resource资源表的resource_code字段值)的权限
    do something...

注意:toolbar组件的资源权限控制,请参考toolbar组件部分说明

1.7. 插件依赖(模块化解耦机制)

框架规范F系列各模块/插件代码(包括后台manager和javascript)之间发生调用关系时,需要判定被调用插件/模块是否存在或启用,以达到灵活产品线封装和灵活的模块组合策略。判定规则可以是根据插件id进行判断,也可以是根据被调用插件所注册的资源进行判断(判断方式参考上一节“资源权限控制”说明)

1.7.1. 插件判断

利用com.seeyon.ctp.common.AppContext可以在后台判断某个插件是否启用:

if(AppContext.hasPlugin("collaboration")) // 跨插件的manager调用时需要判断对方插件是否存在,参数为被调用插件id
    do something...

对于前端界面元素的插件判断可以用如下方式控制界面元素显示/隐藏,根据登陆用户具备的资源权限控制按钮、控件、区域是否显示。

<input type="button" class="resCode" pluginId="collaboration" value="按钮1">
<div class="resCode" pluginId="collaboration">
    <input type="text">
    <input type="button" value="按钮2">
</div>

需要在前端JavaScript中判断当前系统是否启用某个插件,可以在JS中做如下调用:

if($.ctx.plugins.contains('collaboration')) // $.ctx.plugins为当前系统启用的插件id数组,contains判断是否具备某个插件
    do something...

1.7.2. 插件资源判断

跨插件调用也可以利用被调用插件中注册的资源Code(对应priv_resource资源表的resource_code字段值)进行更细粒度的判定,具体判断方式参考上一节“资源权限控制”

中的说明。

注意:toolbar组件的插件依赖控制,请参考toolbar组件部分说明

1.8. 日志

平台的日志统一输出到中间件下的logs_sy目录下,10M一个文件滚动,每日归档到子目录下。

日志文件 用途
ctp.log 系统主要日志,输出平台和应用的日志信息。
cluster.log 集群相关日志。
capability.log 性能日志,记录每一个请求的耗时信息。
ajax.log Ajax错误信息,日志级别调为Debug可跟踪详细的Ajax请求。
login.log 登录日志。
event.log 事件响应日志,输出事件监听执行的错误。
workflow.log 工作流日志。
form.log 表单日志。
uc.log 致信相关日志。
rest.log REST日志。
hibernate.log hibernate错误日志。
sql.log SQL执行日志,日志级别调为Debug可跟踪所有执行的SQL语句。
spring.log Spring错误日志。
quartz.log Quartz定时任务执行日志。

日志的配置文件在webapps/seeyon/WEB-INF/cfgHome/base下,6.1之前版本修改log4j.properties,6.1及以后版本修改log4j2_sys_starting.xml(启动过程中的配置)和log4j2_sys_running.xml(启动完毕的配置)。

6.1以后版本可以登录system系统管理员,在“系统监控”下“运行态改变日志级别”,调整日志级别无需重启实时生效(重启后恢复缺省级别)。

1.8.1. 插件日志自定义扩展

应用场景

提供一种无侵入的插件日志配置机制,可以将日志输出到新的log文件,避免直接修改产品日志配置文件。

配置方式

在插件目录下增加一个log.xml文件,如seeyon/WEb-INF/cfgHome/plugin/test/log.xml,格式如下

<?xml version="1.0" encoding="UTF-8"?>
<Loggers>
    <Logger level="INFO" name="com.seeyon.apps.test" fileName="test"/>
    <Logger level="INFO" name="test" fileName="test2"/>
</Loggers>

对应的test插件启用时,框架会按照配置生成新的日志文件,如test.log、test2.log,其配置机制与框架其他标准配置相同,满10M生成新的文件存储到对应日期的子目录下

提供的示例每20秒输出一次日志

package com.seeyon.apps.test;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.seeyon.ctp.common.SystemInitializer;
import com.seeyon.ctp.common.timer.TimerHolder;

public class TestInit implements SystemInitializer{
    private static Log LOG = LogFactory.getLog("test");
    @Override
    public void initialize() {
        LOG.info("test log is beging");
        TimerHolder.newTimer(new Runnable() {
            @Override
            public void run() {
                LOG.info("test log is running ......................");
                TestUtil.welcome();
            }
        }, 20*1000);    
    }
}

package com.seeyon.apps.test;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class TestUtil {
    private static Log LOG = LogFactory.getLog(TestUtil.class);

    public final static void welcome() {
        LOG.warn("welcome");
    }
}

1.9. 扩展数据库类型支持

标准产品支持Oracle、Microsoft SQLServer、MySQL和PostgreSQL,以及国产的达梦和人大金仓数据库。如果需要增加新的关系型数据库支持,可以采取下面的方式:

1.9.1. 实现方言扩展

以人大金仓为例,需要扩展其方言实现Kingbase8Dialect,并实现CTPDBDialect的两个方法

package org.hibernate.dialect;
import org.hibernate.mapping.Column;
public class CTPKingbase8Dialect extends Kingbase8Dialect implements CTPDBDialect {
  /**
   * ALTER Column的语法。
   **/
    public String getModifyColumnString(String columnName) {
        StringBuilder sb = new StringBuilder();
        sb.append(" modify ");
        sb.append(new Column(columnName).getQuotedName(this));
        sb.append(" ");
        return sb.toString();
    }

  /**
   * 数据库Text类型的长度限制,如MySQL为65535,没有则返回-1。
   **/
    public int getTextLimit() {
        return -1;
    }
}

1.9.2. 复制驱动和方言

  1. 将驱动文件复制到tomcat的lib下,如kingbasejdbc4.jar。
  2. 将方言文件复制到tomcat的webapps/seeyon/WEB-INF/lib下,如Kingbase8Dialect.jar。

1.9.3. 配置数据源信息

修改base/conf目录的datasourceCtp.properties文件。

db.hibernateDialect=org.hibernate.dialect.CTPKingbase8Dialect
workflow.dialect=Kinbase
ctpDataSource.driverClassName=com.kingbase.Driver
ctpDataSource.url=jdbc:kingbase://127.0.0.1:54322/v5

8.0版本,如果getModifyColumnString和getTextLimit都是标准的,可以略过第1步实现方言扩展,无需编码按下面的方式配置即可:

db.hibernateDialect=org.hibernate.dialect.Kingbase8Dialect
workflow.dialect=Kinbase
ctpDataSource.driverClassName=com.kingbase.Driver
ctpDataSource.url=jdbc:kingbase://127.0.0.1:54322/v5

1.9.4. 处理初始化脚本

按照目标数据库的规范调整初始化脚本。

results matching ""

    No results matching ""