(转帖请注明 http://taobaotesting.com/blogs/2359)
2012年下半年,我负责的测试平台部分业务开始采用java进行开发,10月份的时候我也加入了具体的设计开发工作中,负责用户模块的建设。对于当时的我来说,从ruby on rails转向 分布式java web一切还得从头开始:语言陌生、web框架陌生、两种框架的理念不同、以及进度压力等。后来,经过自己的不断琢磨 以及 团队的讨论,总算是如期完工了,而且结果还不错:每日构建、单元测试块覆盖率超过95%、联调时间短、遗漏BUG少等。过程中,逐渐积累了一些设计心得和实践,现总结出来分享给大家。
我所使用的技术环境是 分布式Java Web:java 6 + webx 3.0.7(阿里巴巴研发的java web框架,基于spring,2010年已开源) + HSF(淘宝开发的java api远程调用框架,未开源)。我所负责的用户模块特点是:页面少,多大需求是通过HSF 为其他应用提供API。可以想象,对我来说最重要的就是要保证HSF API接口的质量:接口定义要清晰、不BUG要少、性能要高、开放出来的Jar包要精简&稳定等等。在操作过程中,最首先做的就是跟大家把需求了解清楚、DB设计好、API原型定义好,然后配合全面的单元测试 和 每日构建,余下的事情就是Coding了。
本篇博文,我先分享下我对技术设计的一些体会,以及在设计中是怎样考虑测试的。
1. 分层设计,分层测试
用户模块基于webx框架开发,并通过HSF接口 为其他应用提供服务,因此,我需要把服务接口定义出来,并提供给第三方应用。
1.1 user-common
- 用途:基础Jar包,暴露POJO对象/HSF接口(Interface)给上层应用程序;
-
原则:剥离掉业务代码,以增强Jar包的稳定性;
-
定义好POJO对象,一般只有简单的setter/getter操作,不会涉及业务层面的API;
package pattern: com.taobao.kelude.xxx.model
example: com.taobao.kelude.user.model.Rolepublic class Role extends BaseModel implements Comparable<Role>{ private static final long serialVersionUID = 1L; public static final int STATUS_ACTIVE=1; public static final int STATUS_DELETED=9; public static final String STAMP_COMMON="Common"; private String name; ... public String getName() { return name; } public void setName(String name) { this.name = name; } ... }
- 定义好服务接口,通过Java Interface定义想要暴露给外部应用访问的接口,并提供完整的JavaDoc;
package pattern: com.taobao.kelude.xxx.service
example: com.taobao.kelude.user.service.RoleService
public interface RoleService { /** * Get a role by id * * @param roleId role's id * @return a role if found, otherwise null */ public Role getById(Integer roleId); /** * Get roles list by role ids * * @param roleIds a list of role id * @return a list of role, or an empty list if not found */ public List<Role> getListByIds(List<Integer> roleIds); ... }
- 单元测试:此工程一般只包含接口定义和POJO对象,不涉及业务,因此通常不需要单独测试。除非提供额外的公共API;
1.2 user-dal
定义好了接口和数据对象,现在就来实现她们。尊崇java web的一般实践,我们启用了dal层 来访问数据库。
- 用途: 完成数据库访问,并通过JAVA基础数据类型或common jar中封装的POJO类型 为BIZ层提供支持
- 原则: 只提供数据库访问API,不封装复杂的业务逻辑,一般一个API在sqlmap文件中会对应一条SQL语句;并且,DB操作都应该由DAO完成;
- 依赖:user-common,一般只需要使用POJO对象;
- 同样的,定义好DAO Interface,提供JavaDoc;
pattern: com.taobao.kelude.xxx.dao
example: com.taobao.kelude.user.dao.RoleDao
public interface RoleDao extends IBaseDAO<Role>{
/**
* Get roles list by role ids
*
* @param roleIds a list of role id
* @return a list of role, or an empty list if not found
*/
public List<Role> getListByIds(List<Integer> roleIds);
/**
* Get roles map by role ids
*
* @param roleIds a list of role id
* @return a map set of role, or an empty map set if not found
*/
public Map<Integer, Role> getMapByIds(List<Integer> roleIds);
...
}
- DAO implements;
pattern: com.taobao.kelude.xxx.dao.impl
example: com.taobao.kelude.user.dao.impl.RoleDaoImpl
public class RoleDaoImpl extends AbstractBaseDAO<Role> implements RoleDao{
...
@Override
public List<Role> getListByIds(List<Integer> roleIds) {
if(roleIds==null || roleIds.isEmpty()){
return new ArrayList<Role>();
}
try {
return sqlMapClient.queryForList(getModelName()+".getListByIds", roleIds);
} catch (SQLException e) {
e.printStackTrace();
throw new UnknownException(e);
}
}
...
}
- 异常处理,sqlmap抛出的SQLException必须处理,记录到日志中,并继续抛出异常,但在API声明中不抛出异常,以避免强迫上层API捕获异常,增加编码成本;
- 单元测试:需要全面测试,除了catch SQLException外,其他代码块都应该覆盖;
example: com.taobao.kelude.user.dal.RoleDaoTest
public class RoleDaoTest extends BaseTestCase {
@Resource
private RoleDao roleDao;
@Resource
private JdbcTemplate jdbcTemplate;
@Test
public void test_getListByIds() {
List<Integer> ids = jdbcTemplate.queryForList("select id from roles limit 5", Integer.class);
List<Role> list = roleDao.getListByIds(ids);
Assert.assertTrue(list.size() == ids.size());
// test with invalid parameter
list = roleDao.getListByIds(null);
Assert.assertTrue(list.isEmpty());
}
}
1.3 user-biz
尊崇java web的一般实践,基于dal层来实现具体的业务逻辑。在spring mvc里面称之为service层。
- 用途:实现业务逻辑,实现HSF接口
- 原则:通过调用dal提供的API实现业务逻辑;
- 依赖:user-dal
- 定义好 biz interface,提供JavaDoc;
pattern: com.taobao.kelude.xxx.biz
example: com.taobao.kelude.user.biz.RoleManager
public interface RoleManager extends IBaseManager<Role> {
/**
* Check whether a role has permission to do the action
*
* @param role
* @param action
* @return true if has permission, false if has no permission
*/
public boolean hasPermissionOn(Role role, String action);
/**
* Check whether a role has permission to do the action
*
* @param role
* @param action
* @return true if has permission, false if has no permission
*/
public Map<Role,Boolean> hasPermissionOn(List<Role> roles, String action);
...
}
- biz implements;
pattern: com.taobao.kelude.xxx.biz.impl
example: com.taobao.kelude.user.biz.impl.RoleManagerImpl
public class RoleManagerImpl extends AbstractBaseManager<Role> implements RoleManager{
@Autowired
private RoleDao roleDao;
@Override
public List<Role> getListByIds(List<Integer> roleIds) {
return roleDao.getListByIds(roleIds);
}
...
}
- hsf service implements: 与biz implements实现方式相同。 事实上,hsf接口只是一种远程调用机制,并不影响接口的编写,同样的接口,你可以使用其他的RMI技术来实现远程调用;
- 单元测试:需要全面测试,包括非法参数的覆盖,必要的时候可以mock掉dal层 来快速验证biz逻辑;
- tips: biz interface可继承user-common中定义的hsf interface,以省去重复定义接口的工作量
example: com.taobao.kelude.user.biz.RoleManager
public interface RoleManager extends IBaseManager<Role>, RoleService {
// remove declarations of api which defined in hsf service
}
如此一来,hsf接口的实现由biz来完成,不需要额外创建一组实现类,既减少的代码编写/维护的工作量,也省去了单元测试的工作;
1.4 user-web
现在需要为用户提供页面,选择你熟悉的web框架来开发页面。把HSF.sar放到tomcat容器中,部署好web应用,hsf接口就可以提供远程调用服务了。
- 用途:提供用户界面
- 原则:只提供用户界面,复杂的业务处理提取到biz,或则封装到Helper中,以增强系统可测试性;
- 依赖:user-biz
- 说明:可以选用webx框架,或其他;
- 测试:通过webui进行功能测试,模拟http请求的测试方式也很好;
1.5 总结:architecture pattern for java web
延伸开来,java web基本上都可以遵循这种设计模式来架构。
xxx-common jar,供外部应用使用,独立工程
- com.taobao.xxx.model: POJOs
- com.taobao.xxx.service: hsf api interface
xxx-dal jar,实现数据库访问,可与biz工程合并
- com.taobao.xxx.dal.dao: DAO interface
- com.taobao.xxx.dal.dao.impl: DAO implements
- com.taobao.xxx.dal.model: DAL层实现中额外需要的POJOs
- com.taobao.xxx.dal.helper: DAL层辅助类函数
xxx-biz jar,实现业务逻辑,可与dal工程合并
- com.taobao.xxx.biz: BIZ interface
- com.taobao.xxx.biz.impl: BIZ implements
- com.taobao.xxx.service.impl: 可选,可以合并到biz.impl中一起实现
xxx-web war,提供用户界面,独立工程
- com.taobao.xxx.web.amodule.screen: 获取数据,生成页面内容
- com.taobao.xxx.web.amodule.action: 数据操作,通常处理form提交
- com.taobao.xxx.web.amodule.control: 可选,公共页面片段
- com.taobao.xxx.web.ajax.*: 可选,子模块同上,可以不用单独拧出来;
- com.taobao.xxx.web.api.action: 一般用于对外提供REST API,认证方式会采用API授权方式,例如OAuth,而不是web中的用户登录;
- com.taobao.xxx.web.api.screen: 获取数据,生成页面内容
2. 代码设计
2.1 POJO设计
- 定义常量: 通常用于枚举某个字段的可选值。以静态常量的方式定义在POJO内部,会更容易维护,也更简单;
example: com.taobao.kelude.user.model.Role
public class Role extends BaseModel implements Comparable<Role>{
private static final long serialVersionUID = 1L;
public static final int STATUS_ACTIVE=1;
public static final int STATUS_DELETED=9;
...
}
usage: role.setStatus(Role.STATUS_ACTIVE);
- hashCode, 作为hash key的时候需要使用,通常使用POJO的联合主键来生成
example: com.taobao.kelude.user.model.Role
//name 与 stamp 组合唯一决定一个role.
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((stamp == null) ? 0 : stamp.hashCode());
return result;
}
- equals, 通常使用POJO的联合主键来生成
example: com.taobao.kelude.user.model.Role
@Override
public boolean equals(Object obj) {
if (obj == null) {return false;}
if (this == obj) {return true;}
if (getClass() != obj.getClass()) {return false;}
Role other = (Role) obj;
if (name == null) {
if (other.name != null) {return false;}
} else if (!name.equals(other.name)) {
return false;
}
if (stamp == null) {
if (other.stamp != null) {return false;}
} else if (!stamp.equals(other.stamp)) {
return false;
}
return true;
}
- toString, 通常使用POJO的联合主键来生成
example: com.taobao.kelude.user.model.Role
@Override
public String toString() {
return "Role [name=" + name + ", stamp=" + stamp + "]";
}
- 为什么要重写hashCode & equals http://hi.baidu.com/langmanyuai/item/3498aa9421919337336eebb0 在java的集合(hashMap/hashSet)中,判断两个对象是否相等的规则是: 首先,判断两个对象的hashCode是否相等 如果不相等,认为两个对象也不相等 如果相等,则判断两个对象用equals运算是否相等 如果不相等,认为两个对象也不相等 如果相等,认为两个对象相等
2.2 API设计
- 函数命名保持清晰,并遵守统一的命名规则
List<Model> getListByIds(List<Integer> ids)
Map<Integer, Model> getMapByIds(List<Integer> ids)
List<Model> getListByName(String name); //精确查找
List<Model> searchListByName(String name); //like查找
-
函数入参数量不超过4个,尽量用简单类型,不然难于测试
如果参数过多,请拆分为多个函数 -
通过函数重载 来实现 缺省值 或 不同的入参
//获取用户在一个项目中的角色 public List<Role> getRolesOfUser(String targetType, Integer targetId, Integer userId); //获取用户在多个项目中的角色,可避免SQL多次查询 public Map<Integer,List<Role>> getRolesOfUser(String targetType, List<Integer> targetIds, Integer userId);
-
入参&返回 不使用复杂的数据对象,尽量使用java原生的简单类型 或 公共的POJO类型
别人容易理解,且单元测试/对象序列化都更简单;
比如,这样的POJO类型UserHsf/RoleHsf,你大概不容易猜测出它包含了什么,跟HSF有何种耦合,能不能用到其他地方;
如果坚持使用公共的POJO User/Role,你可以立即知道它的含义,并且可以放心的用到任何地方。 -
提供有意义的返回值,尽量避免void
通过提供返回值,让调用方知道是否成功。
即使是不需要返回数据的,也应该返回boolean类型,如果参数校验失败,则返回false -
如果参数验证失败,尽量不返回null,而是返回blank对象,以增强调用方的容错性:
Boolean : false;
Integer : 0;
String : "" or null;
POJO : null;
List : new ArrayList();
Map : new HashMap();
example:
public List<User> getListByIds(List<Integer> userIds) {
if(userIds==null || userIds.isEmpty()){
return new ArrayList<User>();
}
try {
return sqlMapClient.queryForList(getModelName()+".getListByIds", userIds);
} catch (SQLException e) {
e.printStackTrace();
throw new UnknowException(e);
}
}
- 参数合法性检测 由 具体的实现函数来检测,调用方不需要额外的检测 例如:有些biz api是直接调用 dal api来实现的。
public class UserManagerImpl extends AbstractBaseManager<User> implements UserManager {
@Override
public List<User> getListByIds(List<Integer> userIds) {
return userDao.getListByIds(userIds);
}
}
public class UserDaoImpl extends AbstractBaseDAO<User> implements UserDao{
public List<User> getListByIds(List<Integer> userIds) {
if(userIds==null || userIds.isEmpty()){
return new ArrayList<User>();
}
try {
return sqlMapClient.queryForList(getModelName()+".getListByIds", userIds);
} catch (SQLException e) {
e.printStackTrace();
throw new UnknowException(e);
}
}
}
-
对于数据映射类函数,返回值的类型 与 入参 保持一致,少引入其他类型
比如,根据ids list查询 对象list:
List getListByIds(List ids) -
异常类型定义
每个业务层可自行定义异常类型;
API声明中不抛出异常,以免强迫调用方使用try-catch,除非必须;
2.3让部署变得简单
3. 总结
经过一番的坚持和打磨,在最后发布的时候很顺利,在为上层应用提供服务时基本上没怎么联调就通过了,上线后也很少遇到问题。我的感觉是,做出好的技术设计来犒赏自己:省力、赏心。本篇分享的技术设计 和 实践 并不依赖于webx+hsf,可以通用到其他框架设计的java web系统。单元测试&每日构建的分享放到下一篇文章,谢谢关注。