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
应用使用apps,CTP平台使用ctp如com.seeyon.apps.news和com.seeyon.ctp.form | |
---|---|
MVC Controller(如果模块不大,Controller、Manager和Dao都可以放到上级package中,但PO必须放在po package中) | |
MVC Manager接口 | |
MVC Manager实现 | |
MVC DAO接口 | |
MVC DAO实现 | |
PO | |
PO Hibernate映射文件 | |
Java国际化资源文件 | |
Controller的Spring配置文件 | |
Manager的Srping配置文件 | |
Dao的Spring配置文件 |
一个模块的开发步骤
- 创建spring beans文件
- 创建对应的hbm文件
- 编写XXXController、XXXManager、XXXDao XXXVO、XXXPO
- 编写前端页面
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语句。
请控制好查询条件,优化索引,保证查询的性能。
事务控制
使用全系统统一命名方法控制
有事务的(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
使用注解控制
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();
}
}
[] | 正常查询PO对象,获取Blob数据的byte数组 |
---|---|
[] | 用新的byte数组更新Blob数据 |
[] | 更新保存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传入 | |
---|---|
正常保存PO对象即可保存Clob数据 |
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 | |
---|---|
用新的String数据更新Clob数据 | |
更新保存PO即可更新Clob数据 |
查询记录数和是否存在数据的工具方法
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. 复制驱动和方言
- 将驱动文件复制到tomcat的lib下,如kingbasejdbc4.jar。
- 将方言文件复制到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. 处理初始化脚本
按照目标数据库的规范调整初始化脚本。