查爾斯·狄更斯在《雙城記》中寫道:「這是一個最好的時代,也是一個最壞的時代。」移動互聯網的快速發展,出現了許多新機遇,不少創業者乘機而動;隨着行業競爭加重,互聯網紅利逐漸消失,不少創業公司九死一輩子。web
筆者在初創公司摸爬滾打數年,接觸了各式各樣的Java微服務架構,從中得到了一些優秀的理念,但也發現了一些不合理的現象。如今,筆者總結了一些創業公司存在的Java服務端亂象,並嘗試性地給出了一些不成熟的建議。redis
1.1.1.Controller基類數據庫
常見的Controller基類以下:編程
/** 基礎控制器類 */ public class BaseController { /** 注入服務相關 */ /** 用戶服務 */ @Autowired protected UserService userService; ... /** 靜態常量相關 */ /** 手機號模式 */ protected static final String PHONE_PATTERN = "/^[1]([3-9])[0-9]{9}$/"; ... /** 靜態函數相關 */ /** 驗證電話 */ protected static vaildPhone(String phone) {...} ... }
常見的Controller基類主要包含注入服務、靜態常量和靜態函數等,便於全部的Controller繼承它,並在函數中能夠直接使用這些資源。api
1.1.2.Service基類緩存
常見的Service基類以下:安全
/** 基礎服務類 */ public class BaseService { /** 注入DAO相關 */ /** 用戶DAO */ @Autowired protected UserDAO userDAO; ... /** 注入服務相關 */ /** 短信服務 */ @Autowired protected SmsService smsService; ... /** 注入參數相關 */ /** 系統名稱 */ @Value("${example.systemName}") protected String systemName; ... /** 靜態常量相關 */ /** 超級用戶標識 */ protected static final long SUPPER_USER_ID = 0L; ... /** 服務函數相關 */ /** 獲取用戶函數 */ protected UserDO getUser(Long userId) {...} ... /** 靜態函數相關 */ /** 獲取用戶名稱 */ protected static String getUserName(UserDO user) {...} ... }
常見的Service基類主要包括注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於全部的Service繼承它,並在函數中能夠直接使用這些資源。session
首先,瞭解一下里氏替換原則:架構
里氏代換原則(Liskov Substitution Principle,簡稱LSP):全部引用基類(父類)的地方必須能透明地使用其子類的對象。
其次,瞭解一下基類的優勢:app
因此,咱們能夠得出如下結論:
綜上所述,Controller基類和Service基類只是一個雜湊類,並非一個真正意義上的基類,須要進行拆分。
因爲Service基類比Controller基類更典型,本文以Service基類舉例說明如何來拆分「基類」。
1.3.1.把注入實例放入實現類
根據「使用即引入、無用則刪除」原則,在須要使用的實現類中注入須要使用的DAO、服務和參數。
/** 用戶服務類 */ @Service public class UserService { /** 用戶DAO */ @Autowired private UserDAO userDAO; /** 短信服務 */ @Autowired private SmsService smsService; /** 系統名稱 */ @Value("${example.systemName}") private String systemName; ... }
1.3.2.把靜態常量放入常量類
對於靜態常量,能夠把它們封裝到對應的常量類中,在須要時直接使用便可。
/** 例子常量類 */ public class ExampleConstants { /** 超級用戶標識 */ public static final long SUPPER_USER_ID = 0L; ... }
1.3.3.把服務函數放入服務類
對於服務函數,能夠把它們封裝到對應的服務類中。在別的服務類使用時,能夠注入該服務類實例,而後經過實例調用服務函數。
/** 用戶服務類 */ @Service public class UserService { /** 獲取用戶函數 */ public UserDO getUser(Long userId) {...} ... } /** 公司服務類 */ @Service public class CompanyService { /** 用戶服務 */ @Autowired private UserService userService; /** 獲取管理員 */ public UserDO getManager(Long companyId) { CompanyDO company = ...; return userService.getUser(company.getManagerId()); } ... }
1.3.4.把靜態函數放入工具類
對於靜態函數,能夠把它們封裝到對應的工具類中,在須要時直接使用便可。
/** 用戶輔助類 */ public class UserHelper { /** 獲取用戶名稱 */ public static String getUserName(UserDO user) {...} ... }
咱們會常常會在Controller類中看到這樣的代碼:
/** 用戶控制器類 */ @Controller @RequestMapping("/user") public class UserController { /** 用戶DAO */ @Autowired private UserDAO userDAO; /** 獲取用戶函數 */ @ResponseBody @RequestMapping(path = "/getUser", method = RequestMethod.GET) public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) { // 獲取用戶信息 UserDO userDO = userDAO.getUser(userId); if (Objects.isNull(userDO)) { return null; } // 拷貝並返回用戶 UserVO userVO = new UserVO(); BeanUtils.copyProperties(userDO, userVO); return Result.success(userVO); } ... }
編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能知足需求,沒有必要去封裝成一個服務函數。
案例代碼以下:
/** 測試控制器類 */ @Controller @RequestMapping("/test") public class TestController { /** 系統名稱 */ @Value("${example.systemName}") private String systemName; /** 訪問函數 */ @RequestMapping(path = "/access", method = RequestMethod.GET) public String access() { return String.format("系統(%s)歡迎您訪問!", systemName); } }
訪問結果以下:
curl http://localhost:8080/test/access 系統(null)歡迎您訪問!
爲何參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:
Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.
意思是說:@Value是經過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,因此WebApplicationContex不能使用父容器的屬性值。
因此,Controller不知足Service的需求,不要把業務代碼寫在Controller類中。
SpringMVC服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。
表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,一般採用HTTP協議。
業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分爲服務、做業等。
持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。
因此,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。
把持久層代碼寫在Service中,從功能上來看並無什麼問題,這也是不少人欣然接受的緣由。
這裏以數據庫持久化中間件Hibernate的直接查詢爲例。
現象描述:
/** 用戶服務類 */ @Service public class UserService { /** 會話工廠 */ @Autowired private SessionFactory sessionFactory; /** 根據工號獲取用戶函數 */ public UserVO getUserByEmpId(String empId) { // 組裝HQL語句 String hql = "from t_user where emp_id = '" + empId + "'"; // 執行數據庫查詢 Query query = sessionFactory.getCurrentSession().createQuery(hql); List<UserDO> userList = query.list(); if (CollectionUtils.isEmpty(userList)) { return null; } // 轉化並返回用戶 UserVO userVO = new UserVO(); BeanUtils.copyProperties(userList.get(0), userVO); return userVO; } }
建議方案:
/** 用戶DAO類 */ @Repository public class UserDAO { /** 會話工廠 */ @Autowired private SessionFactory sessionFactory; /** 根據工號獲取用戶函數 */ public UserDO getUserByEmpId(String empId) { // 組裝HQL語句 String hql = "from t_user where emp_id = '" + empId + "'"; // 執行數據庫查詢 Query query = sessionFactory.getCurrentSession().createQuery(hql); List<UserDO> userList = query.list(); if (CollectionUtils.isEmpty(userList)) { return null; } // 返回用戶信息 return userList.get(0); } } /** 用戶服務類 */ @Service public class UserService { /** 用戶DAO */ @Autowired private UserDAO userDAO; /** 根據工號獲取用戶函數 */ public UserVO getUserByEmpId(String empId) { // 根據工號查詢用戶 UserDO userDO = userDAO.getUserByEmpId(empId); if (Objects.isNull(userDO)) { return null; } // 轉化並返回用戶 UserVO userVO = new UserVO(); BeanUtils.copyProperties(userDO, userVO); return userVO; } }
關於插件:
阿里的AliGenerator是一款基於MyBatis Generator改造的DAO層代碼自動生成工具。利用AliGenerator生成的代碼,在執行復雜查詢的時候,須要在業務代碼中組裝查詢條件,使業務代碼顯得特別臃腫。
/** 用戶服務類 */ @Service public class UserService { /** 用戶DAO */ @Autowired private UserDAO userDAO; /** 獲取用戶函數 */ public UserVO getUser(String companyId, String empId) { // 查詢數據庫 UserParam userParam = new UserParam(); userParam.createCriteria().andCompanyIdEqualTo(companyId) .andEmpIdEqualTo(empId) .andStatusEqualTo(UserStatus.ENABLE.getValue()); List<UserDO> userList = userDAO.selectByParam(userParam); if (CollectionUtils.isEmpty(userList)) { return null; } // 轉化並返回用戶 UserVO userVO = new UserVO(); BeanUtils.copyProperties(userList.get(0), userVO); return userVO; } }
我的不喜歡用DAO層代碼生成插件,更喜歡用原汁原味的MyBatis XML映射,主要緣由以下:
固然,既然選擇了使用DAO層代碼生成插件,在享受便利的同時也應該接受插件的缺點。
現象描述:
/** 用戶服務類 */ @Service public class UserService { /** 用戶DAO */ @Autowired private UserDAO userDAO; /** Redis模板 */ @Autowired private RedisTemplate<String, String> redisTemplate; /** 用戶主鍵模式 */ private static final String USER_KEY_PATTERN = "hash::user::%s"; /** 保存用戶函數 */ public void saveUser(UserVO user) { // 轉化用戶信息 UserDO userDO = transUser(user); // 保存Redis用戶 String userKey = MessageFormat.format(USER_KEY_PATTERN, userDO.getId()); Map<String, String> fieldMap = new HashMap<>(8); fieldMap.put(UserDO.CONST_NAME, user.getName()); fieldMap.put(UserDO.CONST_SEX, String.valueOf(user.getSex())); fieldMap.put(UserDO.CONST_AGE, String.valueOf(user.getAge())); redisTemplate.opsForHash().putAll(userKey, fieldMap); // 保存數據庫用戶 userDAO.save(userDO); } }
建議方案:
/** 用戶Redis類 */ @Repository public class UserRedis { /** Redis模板 */ @Autowired private RedisTemplate<String, String> redisTemplate; /** 主鍵模式 */ private static final String KEY_PATTERN = "hash::user::%s"; /** 保存用戶函數 */ public UserDO save(UserDO user) { String key = MessageFormat.format(KEY_PATTERN, userDO.getId()); Map<String, String> fieldMap = new HashMap<>(8); fieldMap.put(UserDO.CONST_NAME, user.getName()); fieldMap.put(UserDO.CONST_SEX, String.valueOf(user.getSex())); fieldMap.put(UserDO.CONST_AGE, String.valueOf(user.getAge())); redisTemplate.opsForHash().putAll(key, fieldMap); } } /** 用戶服務類 */ @Service public class UserService { /** 用戶DAO */ @Autowired private UserDAO userDAO; /** 用戶Redis */ @Autowired private UserRedis userRedis; /** 保存用戶函數 */ public void saveUser(UserVO user) { // 轉化用戶信息 UserDO userDO = transUser(user); // 保存Redis用戶 userRedis.save(userDO); // 保存數據庫用戶 userDAO.save(userDO); } }
把一個Redis對象相關操做接口封裝爲一個DAO類,符合面對對象的編程思想,也符合SpringMVC服務端三層架構規範,更便於代碼的管理和維護。
/** 用戶DAO類 */ @Repository public class UserDAO { /** 獲取用戶函數 */ public UserDO getUser(Long userId) {...} } /** 用戶服務類 */ @Service public class UserService { /** 用戶DAO */ @Autowired private UserDAO userDAO; /** 獲取用戶函數 */ public UserDO getUser(Long userId) { return userDAO.getUser(userId); } } /** 用戶控制器類 */ @Controller @RequestMapping("/user") public class UserController { /** 用戶服務 */ @Autowired private UserService userService; /** 獲取用戶函數 */ @RequestMapping(path = "/getUser", method = RequestMethod.GET) public Result<UserDO> getUser(@RequestParam(name = "userId", required = true) Long userId) { UserDO user = userService.getUser(userId); return Result.success(user); } }
上面的代碼,看上去是知足SpringMVC服務端三層架構的,惟一的問題就是把數據庫模型類UserDO直接暴露給了外部接口。
存在問題:
解決方案:
下面,將介紹如何更科學地搭建Java項目,有效地限制開發人員把數據庫模型類暴露給接口。
第1種:共用模型的項目搭建
共用模型的項目搭建,把全部模型類放在一個模型項目(example-model)中,其它項目(example-repository、example-service、example-website)都依賴該模型項目,關係圖以下:
序號 | 項目名稱 | 打包類型 | 項目功能 |
---|---|---|---|
1 | example-model | jar | 定義了全部模型類,包括DO類和VO類等 |
2 | example-repository | jar | 對應持久層,實現了MySQL、Redis相關DAO等 |
3 | example-service | jar | 對應業務層,實現了Service、Job、Workflow等 |
4 | example-webapp | war | 對應表現層,實現了Controller、Interceptor、Filter等 |
風險:
表現層項目(example-webapp)能夠調用業務層項目(example-service)中的任意服務函數,甚至於越過業務層直接調用持久層項目(example-repository)的DAO函數。
第2種:模型分離的項目搭建
模型分離的項目搭建,單獨搭建API項目(example-api),抽象出對外接口及其模型VO類。業務層項目(example-service)實現了這些接口,並向表現層項目(example-webapp)提供服務。表現層項目(example-webapp)只調用API項目(example-api)定義的服務接口。
序號 | 項目名稱 | 打包類型 | 項目功能 |
---|---|---|---|
1 | example-api | jar | 業務層的表現層,定義了對外開放接口和VO類 |
2 | example-repository | jar | 對應持久層,定義了DO類並實現了MySQL、Redis相關DAO等 |
3 | example-service | jar | 對應業務層,實現了Service、Job、Workflow等 |
4 | example-webapp | war | 對應表現層,實現了Controller、Interceptor、Filter等 |
風險:
表現層項目(example-webapp)仍然能夠調用業務層項目(example-service)提供的內部服務函數和持久層項目(example-repository)的DAO函數。爲了不這種狀況,只好管理制度上要求表現層項目(example-webapp)只能調用API項目(example-api)定義的服務接口函數。
第3種:服務化的項目搭建
服務化的項目搭,就是把業務層項目(example-service)和持久層項目(example-repository)經過Dubbo項目(example-dubbo)打包成一個服務,向業務層項目(example-webapp)或其它業務項目(other-service)提供API項目(example-api)中定義的接口函數。
序號 | 項目名稱 | 打包類型 | 項目功能 |
---|---|---|---|
1 | example-api | jar | 對應業務層的表現層,定義了對外開放接口和VO類 |
2 | example-repository | jar | 對應持久層,定義了DO類並實現了MySQL、Redis相關DAO等 |
3 | example-service | jar | 對應業務層,實現了Service、Job、Workflow等 |
4 | example-dubbo | war | 對應業務層的表現層,經過Dubbo提供服務 |
5 | example-webapp | war | 對應表現層,實現了Controller等,經過Dubbo調用服務 |
6 | other-service | jar | 對應其它項目的業務層,經過Dubbo調用服務 |
說明:Dubbo項目(example-dubbo)只發布API項目(example-api)中定義的服務接口,保證了數據庫模型沒法暴露。業務層項目(example-webapp)或其它業務項目(other-service)只依賴了API項目(example-api),只能調用該項目中定義的服務接口。
有人會問:接口模型和持久層模型分離,接口定義了一個查詢數據模型VO類,持久層也須要定義一個查詢數據模型DO類;接口定義了一個返回數據模型VO類,持久層也須要定義一個返回數據模型DO類……這樣,對於項目早期快速迭代開發很是不利。能不能只讓接口不暴露持久層數據模型,而可以讓持久層使用接口的數據模型?
若是從SpringMVC服務端三層架構來講,這是不容許的,由於它會影響三層架構的獨立性。可是,若是從快速迭代開發來講,這是容許的,由於它並不會暴露數據庫模型類。因此,這是一條不太建議的建議。
/** 用戶DAO類 */ @Repository public class UserDAO { /** 統計用戶函數 */ public Long countByParameter(QueryUserParameterVO parameter) {...} /** 查詢用戶函數 */ public List<UserVO> queryByParameter(QueryUserParameterVO parameter) {...} } /** 用戶服務類 */ @Service public class UserService { /** 用戶DAO */ @Autowired private UserDAO userDAO; /** 查詢用戶函數 */ public PageData<UserVO> queryUser(QueryUserParameterVO parameter) { Long totalCount = userDAO.countByParameter(parameter); List<UserVO> userList = null; if (Objects.nonNull(totalCount) && totalCount.compareTo(0L) > 0) { userList = userDAO.queryByParameter(parameter); } return new PageData<>(totalCount, userList); } } /** 用戶控制器類 */ @Controller @RequestMapping("/user") public class UserController { /** 用戶服務 */ @Autowired private UserService userService; /** 查詢用戶函數(parameter中包括分頁參數startIndex和pageSize) */ @RequestMapping(path = "/queryUser", method = RequestMethod.POST) public Result<PageData<UserVO>> queryUser(@Valid @RequestBody QueryUserParameterVO parameter) { PageData<UserVO> pageData = userService.queryUser(parameter); return Result.success(pageData); } }
「仁者見仁、智者見智」,每一個人都有本身的想法,而文章的內容也只是個人一家之言。
謹以此文獻給那些我工做過的創業公司,是您們曾經放手讓我去整改亂象,讓我從中受益頗深並得以技術成長。
本文爲雲棲社區原創內容,未經容許不得轉載。