那些年,我们见过的Java服务端乱象
導讀
查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代?!币苿踊ヂ摼W的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。
筆者在初創公司摸爬滾打數年,接觸了各式各樣的Java微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象?,F在,筆者總結了一些創業公司存在的Java服務端亂象,并嘗試性地給出了一些不成熟的建議。
1.使用Controller基類和Service基類
1.1.現象描述
1.1.1.Controller基類
常見的Controller基類如下:
/** 基礎控制器類 */ public class BaseController { /** 注入服務相關 *//** 用戶服務 */@Autowiredprotected UserService userService;.../** 靜態常量相關 *//** 手機號模式 */protected static final String PHONE_PATTERN = "/^[1]([3-9])[0-9]{9}$/";.../** 靜態函數相關 *//** 驗證電話 */protected static vaildPhone(String phone) {...}... }常見的Controller基類主要包含注入服務、靜態常量和靜態函數等,便于所有的Controller繼承它,并在函數中可以直接使用這些資源。
1.1.2.Service基類
常見的Service基類如下:
/** 基礎服務類 */ public class BaseService {/** 注入DAO相關 *//** 用戶DAO */@Autowiredprotected UserDAO userDAO;.../** 注入服務相關 *//** 短信服務 */@Autowiredprotected 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繼承它,并在函數中可以直接使用這些資源。
1.2.論證基類必要性
首先,了解一下里氏替換原則:
里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。
其次,了解一下基類的優點:
所以,我們可以得出以下結論:
綜上所述,Controller基類和Service基類只是一個雜湊類,并不是一個真正意義上的基類,需要進行拆分。
1.3.拆分基類的方法
由于Service基類比Controller基類更典型,本文以Service基類舉例說明如何來拆分“基類”。
1.3.1.把注入實例放入實現類
根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。
/** 用戶服務類 */ @Service public class UserService {/** 用戶DAO */@Autowiredprivate UserDAO userDAO;/** 短信服務 */@Autowiredprivate 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 {/** 用戶服務 */@Autowiredprivate 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) {...}... }2.把業務代碼寫在Controller中
2.1.現象描述
我們會經常會在Controller類中看到這樣的代碼:
/** 用戶控制器類 */ @Controller @RequestMapping("/user") public class UserController {/** 用戶DAO */@Autowiredprivate 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);}... }編寫人員給出的理由是:一個簡單的接口函數,這么寫也能滿足需求,沒有必要去封裝成一個服務函數。
2.2.一個特殊的案例
案例代碼如下:
/** 測試控制器類 */ @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類中。
2.3.服務端三層架構
SpringMVC服務端采用經典的三層架構,即表現層、業務層、持久層,分別采用@Controller、@Service、@Repository進行類注解。
表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,并向客戶端響應結果,通常采用HTTP協議。
業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。
持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用于業務層訪問緩存和數據庫。
所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規范的。
3.把持久層代碼寫在Service中
把持久層代碼寫在Service中,從功能上來看并沒有什么問題,這也是很多人欣然接受的原因。
3.1.引起以下主要問題
3.2.把數據庫代碼寫在Service中
這里以數據庫持久化中間件Hibernate的直接查詢為例。
現象描述:
/** 用戶服務類 */ @Service public class UserService {/** 會話工廠 */@Autowiredprivate 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 {/** 會話工廠 */@Autowiredprivate 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 */@Autowiredprivate 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 */@Autowiredprivate 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映射,主要原因如下:
- 會在項目中導入一些不符合規范的代碼;
- 只需要進行一個簡單查詢,也需要導入一整套復雜代碼;
- 進行復雜查詢時,拼裝條件的代碼復雜且不直觀,不如在XML中直接編寫SQL語句;
- 變更表格后需要重新生成代碼并進行覆蓋,可能會不小心刪除自定義函數。
當然,既然選擇了使用DAO層代碼生成插件,在享受便利的同時也應該接受插件的缺點。
3.3.把Redis代碼寫在Service中
現象描述:
/** 用戶服務類 */ @Service public class UserService {/** 用戶DAO */@Autowiredprivate UserDAO userDAO;/** Redis模板 */@Autowiredprivate 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模板 */@Autowiredprivate 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 */@Autowiredprivate UserDAO userDAO;/** 用戶Redis */@Autowiredprivate UserRedis userRedis;/** 保存用戶函數 */public void saveUser(UserVO user) {// 轉化用戶信息UserDO userDO = transUser(user);// 保存Redis用戶userRedis.save(userDO);// 保存數據庫用戶userDAO.save(userDO);} }把一個Redis對象相關操作接口封裝為一個DAO類,符合面對對象的編程思想,也符合SpringMVC服務端三層架構規范,更便于代碼的管理和維護。
4.把數據庫模型類暴露給接口
4.1.現象描述
/** 用戶DAO類 */ @Repository public class UserDAO {/** 獲取用戶函數 */public UserDO getUser(Long userId) {...} }/** 用戶服務類 */ @Service public class UserService {/** 用戶DAO */@Autowiredprivate UserDAO userDAO;/** 獲取用戶函數 */public UserDO getUser(Long userId) {return userDAO.getUser(userId);} }/** 用戶控制器類 */ @Controller @RequestMapping("/user") public class UserController {/** 用戶服務 */@Autowiredprivate 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直接暴露給了外部接口。
4.2.存在問題及解決方案
存在問題:
解決方案:
4.3.項目搭建的三種方式
下面,將介紹如何更科學地搭建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),只能調用該項目中定義的服務接口。
4.4.一條不太建議的建議
有人會問:接口模型和持久層模型分離,接口定義了一個查詢數據模型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 */@Autowiredprivate 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 {/** 用戶服務 */@Autowiredprivate 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);} }后記
“仁者見仁、智者見智”,每個人都有自己的想法,而文章的內容也只是我的一家之言。
謹以此文獻給那些我工作過的創業公司,是您們曾經放手讓我去整改亂象,讓我從中受益頗深并得以技術成長。
原文鏈接
本文為云棲社區原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的那些年,我们见过的Java服务端乱象的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分布式系统:一致性协议
- 下一篇: 开放下载!从RCNN到SSD,这应该是最