Java Web開發之一:用好的技術設計來犒賞自己


(轉帖請注明 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系統。單元測試&每日構建的分享放到下一篇文章,謝謝關注。


    注意!

    本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



     
    粤ICP备14056181号  © 2014-2020 ITdaan.com