现在的位置: 首页 > 综合 > 正文

Java Web开发之一:用好的技术设计来犒赏自己

2013年12月05日 ⁄ 综合 ⁄ 共 9410字 ⁄ 字号 评论关闭

(转帖请注明 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.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;
      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. 代码设计

除了架构设计,作为NB的技术人员,我们也要能够把代码设计得好。

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让部署变得简单

    我们的系统由多个java应用组成,最开始header上的导航是写在vmcommon中的,一旦调整,就需要把每个应用重新部署一遍,有时会有漏掉的。
    后来,我们把导航移到了js中,在vmcommon中加载这个js,部署的时候,就只需要deploy一个静态文件。从这以后,导航的发布不再出错。

    3. 总结

    经过一番的坚持和打磨,在最后发布的时候很顺利,在为上层应用提供服务时基本上没怎么联调就通过了,上线后也很少遇到问题。我的感觉是,做出好的技术设计来犒赏自己:省力、赏心。本篇分享的技术设计 和 实践 并不依赖于webx+hsf,可以通用到其他框架设计的java web系统。单元测试&每日构建的分享放到下一篇文章,谢谢关注。

    抱歉!评论已关闭.