查尔斯·狄更斯在《双城记》中写道:“这是一个最好的时代,也是一个最坏的时代。”
移动互联网的快速发展,出现了许多新机遇,很多创业者伺机而动;随着行业竞争加剧,互联网红利逐渐消失,很多创业公司九死一生。笔者在初创公司摸爬滚打数年,接触了各式各样的 Java 微服务架构,从中获得了一些优秀的理念,但也发现了一些不合理的现象。现在,笔者总结了一些创业公司存在的 Java 服务端乱象,并尝试性地给出了一些不成熟的建议。
常见的 Controller 基类如下:
复制代码
/** 基础控制器类 */ publicclass BaseController { /** 注入服务相关 */ /** 用户服务 */ @Autowired protectedUserService userService; ... /** 静态常量相关 */ /** 手机号模式 */ protectedstatic finalStringPHONE_PATTERN ="/^1[0-9]{9}$/"; ... /** 静态函数相关 */ /** 验证电话 */ protectedstatic vaildPhone(Stringphone) {...} ... }
常见的 Controller 基类主要包含注入服务、静态常量和静态函数等,便于所有的 Controller 继承它,并在函数中可以直接使用这些资源。
常见的 Service 基类如下:
复制代码
/** 基础服务类 */ publicclass BaseService { /** 注入 DAO 相关 */ /** 用户 DAO */ @Autowired protectedUserDAO userDAO; ... /** 注入服务相关 */ /** 短信服务 */ @Autowired protectedSmsService smsService; ... /** 注入参数相关 */ /** 系统名称 */ @Value("${example.systemName}") protectedStringsystemName; ... /** 静态常量相关 */ /** 超级用户标识 */ protectedstatic final long SUPPER_USER_ID =0L; ... /** 服务函数相关 */ /** 获取用户函数 */ protectedUserDO getUser(Long userId) {...} ... /** 静态函数相关 */ /** 获取用户名称 */ protectedstaticStringgetUserName(UserDO user) {...} ... }
常见的 Service 基类主要包括注入 DAO、注入服务、注入参数、静态常量、服务函数、静态函数等,便于所有的 Service 继承它,并在函数中可以直接使用这些资源。
首先,了解一下里氏替换原则:
里氏代换原则(LiskovSubstitutionPrinciple,简称 LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
其次,了解一下基类的优点:
所以,我们可以得出以下结论:
综上所述,Controller 基类和 Service 基类只是一个杂凑类,并不是一个真正意义上的基类,需要进行拆分。
由于 Service 基类比 Controller 基类更典型,本文以 Service 基类举例说明如何来拆分“基类”。
根据“使用即引入、无用则删除”原则,在需要使用的实现类中注入需要使用的 DAO、服务和参数。
复制代码
/** 用户服务类 */ @Service publicclassUserService { /** 用户 DAO */ @Autowired privateUserDAO userDAO; /** 短信服务 */ @Autowired privateSmsService smsService; /** 系统名称 */ @Value("${example.systemName}") privateStringsystemName; ... }
对于静态常量,可以把它们封装到对应的常量类中,在需要时直接使用即可。
复制代码
/** 例子常量类 */ publicclassExampleConstants{ /** 超级用户标识 */ publicstaticfinallong SUPPER_USER_ID =0L; ... }
对于服务函数,可以把它们封装到对应的服务类中。在别的服务类使用时,可以注入该服务类实例,然后通过实例调用服务函数。
复制代码
/** 用户服务类 */ @Service publicclassUserService{ /** 获取用户函数 */ publicUserDO getUser(Long userId) {...} ... } /** 公司服务类 */ @Service publicclassCompanyService{ /** 用户服务 */ @Autowired privateUserService userService; /** 获取管理员 */ publicUserDO getManager(Long companyId) { CompanyDO company = ...; returnuserService.getUser(company.getManagerId()); } ... }
对于静态函数,可以把它们封装到对应的工具类中,在需要时直接使用即可。
复制代码
/** 用户辅助类 */ public classUserHelper{ /** 获取用户名称 */ publicstaticStringgetUserName(UserDOuser){...} ... }
我们会经常会在 Controller 类中看到这样的代码:
复制代码
/** 用户控制器类 */ @Controller @RequestMapping("/user") publicclassUserController { /** 用户 DAO */ @Autowired privateUserDAO 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 =newUserVO(); BeanUtils.copyProperties(userDO,userVO); returnResult.success(userVO); } ... }
编写人员给出的理由是:一个简单的接口函数,这么写也能满足需求,没有必要去封装成一个服务函数。
案例代码如下:
复制代码
/** 测试控制器类 */ @Controller @RequestMapping("/test") public class TestController { /** 系统名称 */ @Value("${example.systemName}") private String systemName; /** 访问函数 */ @RequestMapping(path ="/access", method = RequestMethod.GET) public String access() { returnString.format(" 系统 (%s) 欢迎您访问!", systemName); } }
访问结果如下:
复制代码
curlhttp://localhost:8080/test/access 系统 (null) 欢迎您访问!
为什么参数 systemName(系统名称)没有被注入值?《SpringDocumentation》给出的解释是:
Notethatactualprocessingofthe@ValueannotationisperformedbyaBeanPostProcessor. BeanPostProcessorinterfacesarescopedper-container.Thisisonlyrelevantifyouareusingcontainerhierarchies.IfyoudefineaBeanPostProcessorinonecontainer,itwillonlydoitsworkonthebeansinthatcontainer.Beansthataredefinedinonecontainerarenotpost-processedbyaBeanPostProcessorinanothercontainer,evenifbothcontainersarepartofthesamehierarchy.
意思是说:@Value 是通过 BeanPostProcessor 来处理的,而 WebApplicationContex 和 ApplicationContext 是单独处理的,所以 WebApplicationContex 不能使用父容器的属性值。
所以,Controller 不满足 Service 的需求,不要把业务代码写在 Controller 类中。
SpringMVC 服务端采用经典的三层架构,即表现层、业务层、持久层,分别采用 @Controller、@Service、@Repository 进行类注解。
表现层(Presentation):又称控制层(Controller),负责接收客户端请求,并向客户端响应结果,通常采用 HTTP 协议。
业务层(Business):又称服务层(Service),负责业务相关逻辑处理,按照功能分为服务、作业等。
持久层(Persistence):又称仓库层(Repository),负责数据的持久化,用于业务层访问缓存和数据库。
所以,把业务代码写入到 Controller 类中,是不符合 SpringMVC 服务端三层架构规范的。
把持久层代码写在 Service 中,从功能上来看并没有什么问题,这也是很多人欣然接受的原因。
这里以数据库持久化中间件 Hibernate 的直接查询为例。
复制代码
/** 用户服务类 */ @Service publicclassUserService { /** 会话工厂 */ @Autowired privateSessionFactory sessionFactory; /** 根据工号获取用户函数 */ public UserVO getUserByEmpId(StringempId){ // 组装 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 =newUserVO(); BeanUtils.copyProperties(userList.get(0), userVO); return userVO; } }
复制代码
/** 用户 DAO 类 */ @Repository publicclassUserDAO { /** 会话工厂 */ @Autowired privateSessionFactory sessionFactory; /** 根据工号获取用户函数 */ public UserDO getUserByEmpId(StringempId){ // 组装 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 publicclassUserService { /** 用户 DAO */ @Autowired privateUserDAO userDAO; /** 根据工号获取用户函数 */ public UserVO getUserByEmpId(StringempId){ // 根据工号查询用户 UserDO userDO = userDAO.getUserByEmpId(empId); if(Objects.isNull(userDO)) { return null; } // 转化并返回用户 UserVO userVO =newUserVO(); BeanUtils.copyProperties(userDO,userVO); return userVO; } }
阿里的 AliGenerator 是一款基于 MyBatisGenerator 改造的 DAO 层代码自动生成工具。利用 AliGenerator 生成的代码,在执行复杂查询的时候,需要在业务代码中组装查询条件,使业务代码显得特别臃肿。
复制代码
/** 用户服务类 */ @Service publicclassUserService { /** 用户 DAO */ @Autowired privateUserDAO userDAO; /** 获取用户函数 */ public UserVO getUser(StringcompanyId, StringempId){ // 查询数据库 UserParam userParam =newUserParam(); userParam.createCriteria().andCompanyIdEqualTo(companyId) .andEmpIdEqualTo(empId) .andStatusEqualTo(UserStatus.ENABLE.getValue()); List<UserDO> userList = userDAO.selectByParam(userParam); if(CollectionUtils.isEmpty(userList)) { return null; } // 转化并返回用户 UserVO userVO =newUserVO(); BeanUtils.copyProperties(userList.get(0), userVO); return userVO; } }
个人不喜欢用 DAO 层代码生成插件,更喜欢用原汁原味的 MyBatisXML 映射,主要原因如下:
当然,既然选择了使用 DAO 层代码生成插件,在享受便利的同时也应该接受插件的缺点。
复制代码
/** 用户服务类 */ @Service publicclassUserService { /** 用户 DAO */ @Autowired privateUserDAO userDAO; /** Redis 模板 */ @Autowired privateRedisTemplate<String, String> redisTemplate; /** 用户主键模式 */ privatestatic final String USER_KEY_PATTERN ="hash::user::%s"; /** 保存用户函数 */ public void saveUser(UserVOuser){ // 转化用户信息 UserDO userDO = transUser(user); // 保存 Redis 用户 String userKey =MessageFormat.format(USER_KEY_PATTERN, userDO.getId()); Map<String, String> fieldMap =newHashMap<>(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 publicclassUserRedis { /** Redis 模板 */ @Autowired privateRedisTemplate<String, String> redisTemplate; /** 主键模式 */ privatestatic final String KEY_PATTERN ="hash::user::%s"; /** 保存用户函数 */ public UserDO save(UserDO user) { String key =MessageFormat.format(KEY_PATTERN, userDO.getId()); Map<String, String> fieldMap =newHashMap<>(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 publicclassUserService { /** 用户 DAO */ @Autowired privateUserDAO userDAO; /** 用户 Redis */ @Autowired privateUserRedis userRedis; /** 保存用户函数 */ public void saveUser(UserVOuser){ // 转化用户信息 UserDO userDO = transUser(user); // 保存 Redis 用户 userRedis.save(userDO); // 保存数据库用户 userDAO.save(userDO); } }
把一个 Redis 对象相关操作接口封装为一个 DAO 类,符合面对对象的编程思想,也符合 SpringMVC 服务端三层架构规范,更便于代码的管理和维护。
复制代码
/** 用户 DAO 类 */ @Repository publicclassUserDAO{ /** 获取用户函数 */ publicUserDO getUser(LonguserId) {...} } /** 用户服务类 */ @Service publicclassUserService{ /** 用户 DAO */ @Autowired privateUserDAO userDAO; /** 获取用户函数 */ publicUserDO getUser(LonguserId) { returnuserDAO.getUser(userId); } } /** 用户控制器类 */ @Controller @RequestMapping("/user") publicclassUserController{ /** 用户服务 */ @Autowired privateUserService userService; /** 获取用户函数 */ @RequestMapping(path ="/getUser", method = RequestMethod.GET) publicResult<UserDO> getUser(@RequestParam(name ="userId", required = true)LonguserId) { UserDO user = userService.getUser(userId); returnResult.success(user); } }
上面的代码,看上去是满足 SpringMVC 服务端三层架构的,唯一的问题就是把数据库模型类 UserDO 直接暴露给了外部接口。
下面,将介绍如何更科学地搭建 Java 项目,有效地限制开发人员把数据库模型类暴露给接口。
共用模型的项目搭建,把所有模型类放在一个模型项目(example-model)中,其它项目(example-repository、example-service、example-website)都依赖该模型项目,关系图如下:
风险:表现层项目(example-webapp)可以调用业务层项目(example-service)中的任意服务函数,甚至于越过业务层直接调用持久层项目(example-repository)的 DAO 函数。
模型分离的项目搭建,单独搭建 API 项目(example-api),抽象出对外接口及其模型 VO 类。业务层项目(example-service)实现了这些接口,并向表现层项目(example-webapp)提供服务。表现层项目(example-webapp)只调用 API 项目(example-api)定义的服务接口。
风险:表现层项目(example-webapp)仍然可以调用业务层项目(example-service)提供的内部服务函数和持久层项目(example-repository)的 DAO 函数。为了避免这种情况,只好管理制度上要求表现层项目(example-webapp)只能调用 API 项目(example-api)定义的服务接口函数。
服务化的项目搭,就是把业务层项目(example-service)和持久层项目(example-repository)通过 Dubbo 项目(example-dubbo)打包成一个服务,向业务层项目(example-webapp)或其它业务项目(other-service)提供 API 项目(example-api)中定义的接口函数。
说明:Dubbo 项目(example-dubbo)只发布 API 项目(example-api)中定义的服务接口,保证了数据库模型无法暴露。业务层项目(example-webapp)或其它业务项目(other-service)只依赖了 API 项目(example-api),只能调用该项目中定义的服务接口。
有人会问:接口模型和持久层模型分离,接口定义了一个查询数据模型 VO 类,持久层也需要定义一个查询数据模型 DO 类;接口定义了一个返回数据模型 VO 类,持久层也需要定义一个返回数据模型 DO 类……这样,对于项目早期快速迭代开发非常不利。能不能只让接口不暴露持久层数据模型,而能够让持久层使用接口的数据模型?
如果从 SpringMVC 服务端三层架构来说,这是不允许的,因为它会影响三层架构的独立性。但是,如果从快速迭代开发来说,这是允许的,因为它并不会暴露数据库模型类。所以,这是一条不太建议的建议。
复制代码
/** 用户 DAO 类 */ @Repository publicclassUserDAO { /** 统计用户函数 */ public Long countByParameter(QueryUserParameterVOparameter){...} /** 查询用户函数 */ public List<UserVO> queryByParameter(QueryUserParameterVOparameter){...} } /** 用户服务类 */ @Service publicclassUserService { /** 用户 DAO */ @Autowired privateUserDAO userDAO; /** 查询用户函数 */ public PageData<UserVO> queryUser(QueryUserParameterVOparameter){ Long totalCount = userDAO.countByParameter(parameter); List<UserVO> userList = null; if(Objects.nonNull(totalCount)&&totalCount.compareTo(0L)>0) { userList = userDAO.queryByParameter(parameter); } returnnewPageData<>(totalCount, userList); } } /** 用户控制器类 */ @Controller @RequestMapping("/user") publicclassUserController { /** 用户服务 */ @Autowired privateUserService userService; /** 查询用户函数 (parameter 中包括分页参数 startIndex 和 pageSize) */ @RequestMapping(path="/queryUser",method= RequestMethod.POST) public Result<PageData<UserVO>> queryUser(@Valid @RequestBody QueryUserParameterVOparameter){ PageData<UserVO> pageData = userService.queryUser(parameter); returnResult.success(pageData); } }
“仁者见仁、智者见智”,每个人都有自己的想法,而文章的内容也只是我的一家之言。
谨以此文献给那些我工作过的创业公司,是您们曾经放手让我去整改乱象,让我从中受益颇深并得以技术成长。
陈昌毅,花名常意,高德地图技术专家,2018 年加入阿里巴巴,一直从事地图数据采集的相关工作。
https://mp.weixin.qq.com/s/I_pfVRYLv5hlBA2JgAQxEQ