HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

那些年,我们见过的 Java 服务端乱象

  • 2019-08-12
  • 本文字数:8861 字

    阅读完需:约 29 分钟

那些年,我们见过的 Java 服务端乱象

导读

查尔斯·狄更斯在《双城记》中写道:“这是一个最好的时代,也是一个最坏的时代。”


移动互联网的快速发展,出现了许多新机遇,很多创业者伺机而动;随着行业竞争加剧,互联网红利逐渐消失,很多创业公司九死一生。笔者在初创公司摸爬滚打数年,接触了各式各样的 Java 微服务架构,从中获得了一些优秀的理念,但也发现了一些不合理的现象。现在,笔者总结了一些创业公司存在的 Java 服务端乱象,并尝试性地给出了一些不成熟的建议。

1.使用 Controller 基类和 Service 基类

1.1.现象描述

1.1.1.Controller 基类

常见的 Controller 基类如下:


/** 基础控制器类 */public class BaseController {    /** 注入服务相关 */    /** 用户服务 */    @Autowired    protected UserService userService;    ...
/** 静态常量相关 */ /** 手机号模式 */ protected static final String PHONE_PATTERN = "/^[]([3-9])[0-9]{9}$/"; ...
/** 静态函数相关 */ /** 验证电话 */ protected static vaildPhone(String phone) {...} ...}
复制代码


常见的 Controller 基类主要包含注入服务、静态常量和静态函数等,便于所有的 Controller 继承它,并在函数中可以直接使用这些资源。

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 继承它,并在函数中可以直接使用这些资源。

1.2.论证基类必要性

首先,了解一下里氏替换原则:


里氏代换原则(LiskovSubstitutionPrinciple,简称 LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。


其次,了解一下基类的优点:


  • 子类拥有父类的所有方法和属性,从而减少了创建子类的工作量;

  • 提高了代码的重用性,子类拥有父类的所有功能;

  • 提高了代码的扩展性,子类可以添加自己的功能。


所以,我们可以得出以下结论:


  • Controller 基类和 Service 基类在整个项目中并没有直接被使用,也就没有可使用其子类替换基类的场景,所以不满足里氏替换原则;

  • Controller 基类和 Service 基类并没有抽象接口函数或虚函数,即所有继承基类的子类间没有相关共性,直接导致在项目中仍然使用的是子类;

  • Controller 基类和 Service 基类只关注了重用性,即子类能够轻松使用基类的注入 DAO、注入服务、注入参数、静态常量、服务函数、静态函数等资源。但是,忽略了这些资源的必要性,即这些资源并不是子类所必须的,反而给子类带来了加载时的性能损耗。


综上所述,Controller 基类和 Service 基类只是一个杂凑类,并不是一个真正意义上的基类,需要进行拆分。

1.3.拆分基类的方法

由于 Service 基类比 Controller 基类更典型,本文以 Service 基类举例说明如何来拆分“基类”。

1.3.1.把注入实例放入实现类

根据“使用即引入、无用则删除”原则,在需要使用的实现类中注入需要使用的 DAO、服务和参数。


/** 用户服务类 */@Servicepublic 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.把服务函数放入服务类

对于服务函数,可以把它们封装到对应的服务类中。在别的服务类使用时,可以注入该服务类实例,然后通过实例调用服务函数。


/** 用户服务类 */@Servicepublic class UserService {    /** 获取用户函数 */    public UserDO getUser(Long userId) {...}    ...}
/** 公司服务类 */@Servicepublic 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) {...}    ...}
复制代码

2. 把业务代码写在 Controller 中

2.1.现象描述

我们会经常会在 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); } ...}
复制代码


编写人员给出的理由是:一个简单的接口函数,这么写也能满足需求,没有必要去封装成一个服务函数。

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(系统名称)没有被注入值?《SpringDocumentation》给出的解释是:


Notethatactualprocessingofthe@ValueannotationisperformedbyaBeanPostProcessor.

BeanPostProcessorinterfacesarescopedper-container.Thisisonlyrelevantifyouareusingcontainerhierarchies.IfyoudefineaBeanPostProcessorinonecontainer,itwillonlydoitsworkonthebeansinthatcontainer.Beansthataredefinedinonecontainerarenotpost-processedbyaBeanPostProcessorinanothercontainer,evenifbothcontainersarepartofthesamehierarchy.


意思是说:@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.引起以下主要问题

  • 业务层和持久层混杂在一起,不符合 SpringMVC 服务端三层架构规范;

  • 在业务逻辑中组装语句、主键等,增加了业务逻辑的复杂度;

  • 在业务逻辑中直接使用第三方中间件,不便于第三方持久化中间件的替换;

  • 同一对象的持久层代码分散在各个业务逻辑中,背离了面对对象的编程思想;

  • 在写单元测试用例时,无法对持久层接口函数直接测试。

3.2.把数据库代码写在 Service 中

这里以数据库持久化中间件 Hibernate 的直接查询为例。

现象描述:

/** 用户服务类 */@Servicepublic 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类 */@Repositorypublic 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);    }}
/** 用户服务类 */@Servicepublic 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 是一款基于 MyBatisGenerator 改造的 DAO 层代码自动生成工具。利用 AliGenerator 生成的代码,在执行复杂查询的时候,需要在业务代码中组装查询条件,使业务代码显得特别臃肿。


/** 用户服务类 */@Servicepublic 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 层代码生成插件,更喜欢用原汁原味的 MyBatisXML 映射,主要原因如下:


  • 会在项目中导入一些不符合规范的代码;

  • 只需要进行一个简单查询,也需要导入一整套复杂代码;

  • 进行复杂查询时,拼装条件的代码复杂且不直观,不如在 XML 中直接编写 SQL 语句;

  • 变更表格后需要重新生成代码并进行覆盖,可能会不小心删除自定义函数。


当然,既然选择了使用 DAO 层代码生成插件,在享受便利的同时也应该接受插件的缺点。

3.3.把 Redis 代码写在 Service 中

现象描述:

/** 用户服务类 */@Servicepublic 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类 */@Repositorypublic 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);    }}
/** 用户服务类 */@Servicepublic 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 服务端三层架构规范,更便于代码的管理和维护。

4.把数据库模型类暴露给接口

4.1.现象描述

/** 用户DAO类 */@Repositorypublic class UserDAO {    /** 获取用户函数 */    public UserDO getUser(Long userId) {...}}
/** 用户服务类 */@Servicepublic 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 直接暴露给了外部接口。

4.2.存在问题及解决方案

存在问题


  • 间接暴露数据库表格设计,给竞争对手竞品分析带来方便;

  • 如果数据库查询不做字段限制,会导致接口数据庞大,浪费用户的宝贵流量;

  • 如果数据库查询不做字段限制,容易把敏感字段暴露给接口,导致出现数据的安全问题;

  • 如果数据库模型类不能满足接口需求,需要在数据库模型类中添加别的字段,导致数据库模型类跟数据库字段不匹配问题;

  • 如果没有维护好接口文档,通过阅读代码是无法分辨出数据库模型类中哪些字段是接口使用的,导致代码的可维护性变差。


解决方案


  • 从管理制度上要求数据库和接口的模型类完全独立;

  • 从项目结构上限制开发人员把数据库模型类暴露给接口。

4.3.项目搭建的三种方式

下面,将介绍如何更科学地搭建 Java 项目,有效地限制开发人员把数据库模型类暴露给接口。

第 1 种:共用模型的项目搭建

共用模型的项目搭建,把所有模型类放在一个模型项目(example-model)中,其它项目(example-repository、example-service、example-website)都依赖该模型项目,关系图如下:




风险:表现层项目(example-webapp)可以调用业务层项目(example-service)中的任意服务函数,甚至于越过业务层直接调用持久层项目(example-repository)的 DAO 函数。

第 2 种:模型分离的项目搭建

模型分离的项目搭建,单独搭建 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)定义的服务接口函数。

第 3 种:服务化的项目搭建

服务化的项目搭,就是把业务层项目(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),只能调用该项目中定义的服务接口。

4.4.一条不太建议的建议

有人会问:接口模型和持久层模型分离,接口定义了一个查询数据模型 VO 类,持久层也需要定义一个查询数据模型 DO 类;接口定义了一个返回数据模型 VO 类,持久层也需要定义一个返回数据模型 DO 类……这样,对于项目早期快速迭代开发非常不利。能不能只让接口不暴露持久层数据模型,而能够让持久层使用接口的数据模型?


如果从 SpringMVC 服务端三层架构来说,这是不允许的,因为它会影响三层架构的独立性。但是,如果从快速迭代开发来说,这是允许的,因为它并不会暴露数据库模型类。所以,这是一条不太建议的建议。


/** 用户DAO类 */@Repositorypublic class UserDAO {    /** 统计用户函数 */    public Long countByParameter(QueryUserParameterVO parameter) {...}    /** 查询用户函数 */    public List<UserVO> queryByParameter(QueryUserParameterVO parameter) {...}}
/** 用户服务类 */@Servicepublic 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); }}
复制代码

后记

“仁者见仁、智者见智”,每个人都有自己的想法,而文章的内容也只是我的一家之言。


谨以此文献给那些我工作过的创业公司,是您们曾经放手让我去整改乱象,让我从中受益颇深并得以技术成长。


作者介绍


陈昌毅,花名常意,高德地图技术专家,2018 年加入阿里巴巴,一直从事地图数据采集的相关工作。


本文转载自公众号阿里巴巴中间件(ID:Aliware_2018)


原文链接


https://mp.weixin.qq.com/s/I_pfVRYLv5hlBA2JgAQxEQ


2019-08-12 08:005200

评论 5 条评论

发布
用户头像
哈哈,第一个想到的就是 mybatis plus 。。。从审美上就不能苟同那设计……
2019-08-17 15:41
回复
用户头像
看不太懂
2019-08-13 22:41
回复
用户头像
Controller中的@Value能正常注入啊 专门复制了作者的代码 发现能够成功注入啊

➜ ~ curl http://localhost:8080/test/access
系统 (foo) 欢迎您访问!%
2019-08-13 22:27
回复
用户头像
干货,对初中级开发人员很有帮助
2019-08-13 15:52
回复
用户头像
同感。有很多类似的场景现在还在重复中。
2019-08-12 10:00
回复
没有更多了
发现更多内容

TDesign 更新周报(2022年3月第4周)

TDesign

面试题笔记

Clarke

性能测试中的LongAdder

FunTester

性能测试 FunTester

个全中文注释的迷你Spring!

程序员阿杜

Java spring springboot

NFT游戏NFT数字藏品交易系统搭建开发

薇電13242772558

NFT

企业如何实现在线客服功能?

小炮

在线客服

车联网数据安全新挑战的技术应对方案

Speedoooo

车联网 物联网 数据安全 容器安全

在线HTML压缩工具

入门小站

工具

Flink Next:Beyond Stream Processing

Apache Flink

大数据 flink 编程 流计算 实时计算

澳鹏数据标注平台MatrixGo加速人工智能落地

澳鹏Appen

人工智能 数据标注 训练数据

车载运行小程序,快速打造智慧汽车应用生态

Speedoooo

车联网 物联网 智慧终端 智慧汽车 车载小程序

Petal Maps的美学钥匙,解锁AITO问界M5的硬核浪漫

脑极体

科技向善,“以人为本”将掷地有声!

鼎道智联

代码评审的最佳解决方案

阿里云云效

云计算 阿里云 敏捷开发 代码管理 代码评审

赋能创新,深开鸿重磅发布面向金融行业KaihongOS发行版

科技汇

直播预告|FeatureStore Meetup V2

星策开源社区

人工智能 大数据 开源 特征平台 MLOps

【ELT.ZIP】OpenHarmony啃论文俱乐部——轻翻那些永垂不朽的诗篇

ELT.ZIP

OpenHarmony 数据压缩 ELT.ZIP

架构实战营6&微信业务架构&学生管理系统方案

唐诗宋词

Linux之time命令

入门小站

Linux

数字孪生PaaS平台WDP4.3正式发布!三大升级,让开发更简单

Meta 小元

云原生 智慧城市 数字孪生

阿里巴巴代码规约检测&Java 代码规约扫描

阿里云云效

阿里巴巴 阿里云 代码扫描 #java 代码规约检测

JavaScript 引擎是如何实现 async/await 的

CRMEB

架构实战营-第6期 模块一课后作业

乐邦

「架构实战营」

微信业务架构图&学生管理系统架构设计

高山觅流水

架构实战营 「架构实战营」

CPP进阶:迭代器失效

正向成长

迭代器失效

一文读懂并发与并行

潘大壮

并发编程 多线程 并行 并发’ #java

Hoo虎符研究院|区块链简报 20220328期

区块链前沿News

虎符 Hoo 虎符交易所

java高级用法之:在JNA中使用类型映射

程序那些事

Java 程序那些事 3月月更 JNA

在线常用crontab表达式大全验证解析

入门小站

工具

恒源云(GpuShare)_无监督的QG方法

恒源云

自然语言处理 深度学习

智能家居开放平台技术建设新思路

Speedoooo

物联网 智慧社区 智慧家居 智能终端 应用平台

那些年,我们见过的 Java 服务端乱象_文化 & 方法_陈昌毅_InfoQ精选文章