作者 | 陈昌毅
责编 | 伍杏玲
查尔斯狄更斯在《双城记》中写道:“这是一个最好的年代,也是一个最坏的年代。”
移动互联网的快速开展,呈现了许多新机遇,许多创业者伺机而动;跟着职业竞赛加重,互联网盈利逐步消失,许多创业公司九死一生。笔者在草创公司摸爬滚打数年,触摸了林林总总的 Java 微服务架构,从中获得了一些优异的理念,但也发现了一些不合理的现象。现在,笔者总结了一些创业公司存在的 Java 服务端乱象,并测验性地给出了一些不成熟的主张。
运用Controller基类和Service基类
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 承继它,并在函数中可以直接运用这些资源。
- Service 基类
常见的 Service 基类如下:
/** 根底服务类 */
publicclassBaseService{
/** 注入DAO相关 */
/** 用户DAO */
@Autowired
protectedUserDAO userDAO;
...
/** 注入服务相关 */
/** 短信服务 */
@Autowired
protectedSmsService smsService;
...
/** 注入参数相关 */
/** 体系称号 */
@Value( "${example.systemName}")
protectedString systemName;
...
/** 静态常量相关 */
/** 超级用户标识 */见红后多久会生
protectedstaticfinal longSUPPER_USER_ID = 0L;
...
/** 荣威350服务函数相关 */
/** 获取用户函数 */
protectedUserDO getUser(Long userId){...}
...
/** 静态函数相关 */
/** 获取用户称号 */
protectedstaticString getUserName(UserDO user){...}
...
}
常见的 Service 基类首要包含注入 DAO、注入服务、注入参数、静态常量、服务函数、静态函数等,便于一切的 Service 承继它,并在函数中可以直接运用这些资源。
2、证明基类必要性
首要,了解一下里氏替换准则:
里氏代换准则(Liskov Substitution Principle,简称LSP):一切引证基类(父类)的当地有必要能透明地运用其子类的目标。
里氏代换准则(Liskov Substitution Principle,简称LSP):一切引证基类(父类)的当地有必要能透明地运用其子类的目标。
其次,了解一下基类的长处:
- 子类具有父类的一切办法和特点,然后减少了创立子类的作业量;
- 提高了代码的重用性,子类具有父类的一切功用;
- 提高了代码的扩展性,子类可以增加自己的功用。
所以,咱们可以得出以下定论:
- Controller 基类和 Service 基类在整个项目中并没有直接被运用,也就没有可运用其子类替换基类的场景,所以不满意里氏替换准则;
- Controller 基类和 Service 基类并没云檀有笼统接口函数或虚函数,即一切承继基类的子类间没有相关共性,直接导致在项目中依然运用的是子类;
- Controller 基类和 Service 基类只重视了重用性,即子类可以轻松运用基类的注入DAO、注入服务、注入参数、静态常量、服务函数、静态函数晕等资源。可是,疏忽了这些资源的必要性,即这些资源并不是子类一切必要的,反而给子类带来了加载时的功用损耗。
综上所述,Controller 基类和 Service 基类仅仅一个杂凑类,并不是一个真实意义上的基类,需求进行拆分。
3、拆分基类的办法
由于 Service 基类比 Controller 基类更典型,本文以 Service 基类举例阐明怎么来拆分“基类”。
- 把注入实例放入完成类
依据“运用即引进、无用则删去”准则,在需求运用的完成类中注入需求运用的DAO、服务和参数。
/** 用户服务类 */
@ Service
public class UserService {
/** 用户DAO */
@ Autowired
private UserDAO userDAO;
/** 短信服务 */
@ Autowired
private SmsService smsService;
/** 体系称号 */
@ Value("${ example.systemName}")
privateStringsystemName;
...
}
- 把静态常量放入常量类
关于静态常量,可以把它们封装到对应的常量类中,在需求时直接运用即可。
/** 比如常量类 */
publicclassExampleConstants{
/** 超级用户标识 */
publicstaticfinal longSUPPER_USER_ID = 0L;
...
}
- 把服务函数放入服务类
关于服务函数,可以把它们封装到对应的服务类中。在其他服务类运用时,可以注入该服务类实例,然后经过实例调用服务函数。
/** 用户服务类 */
@Service
publicclassUserService{
/** 获取用户函数 */
publicUserDO getUser(Long userId){...}
...
}
/** 公司服务类 */
@Service
publicclassCompanyService{
/** 用户服务 */
@Autowired
privateUserService userService;
/** 获取办理员 */
publicUserDO getManager(Long companyId){
CompanyDO company = ...;
returnuserService.getUser(company.getManagerId);
}
...
}
- 把静态函数放入东西类
关于静态函数,可以把它们封装到对应的东西类中,在需求时直接运用即可。
/** 用户辅佐类 */
publicclassUserHelper{
/** 获取用户称号 */
publicstaticString getUserName(UserDO user){...}
...
}
把事务代码写在 Controller 中
1、现象描绘
咱们会经常会在 Controller 类中看到这样的代码:
/** 用户操控器类 */
@Controller
@RequestMapping("/user")
publicclassUserController{
/** 用户DAO */
@Autowired
privateUserDAO userDAO;
/** 获取用户函数 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
publicResult<UserVO> getUser( @RequestParam(name = "userId", required = true)LonguserId) {
// 获取用户信息
UserDO userD葱花饼O = userDAO.getUser(userId);
if(Objects.isNull(userDO)) {
returnnull;
}
// 复制并回来用户
UserVO userVO = new UserVO;
BeanUtils.copyProperties(userDO, userVO);
returnResult.success(userVO);
}
...
}
编写人员给出的理由是:一个简略的接口函数,这么写也能满意需求,没有必要去封装成一个服务函数。
2、一个特其他事例
事例代码如下:
/** 测验操控器类 */
@Controller
婚纱,Java服务器端像大市场点相同乱-安博电竞 官网_安博电竞进口_安博电竞网页版@RequestMapping("/test")
publicclassTestController{
/** 体系称号 */
@Value("${example.systemName}")
privateString systemN婚纱,Java服务器端像大市场点相同乱-安博电竞 官网_安博电竞进口_安博电竞网页版ame;
/** 拜访函数 */
@RequestMapping(path = "/access", method = RequestMethod.GET)
publicString access {
returnString.format( "体系(%s)欢迎您拜访!", systemName);
}
}
拜访成果如下:
curl http: //localhost:8080/t酷7k7eest/acc婚纱,Java服务器端像大市场点相同乱-安博电竞 官网_安博电竞进口_安博电竞网页版ess
体系( null)欢迎您拜访!
为什么参数systemName(体系称号)没有被注入值?《Spring Documentation》给出的解说是:
Note that actual pro婚纱,Java服务器端像大市场点相同乱-安博电竞 官网_安博电竞进口_安博电竞网页版cessing 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.
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 contai港币人民币ner, even if both containers are part of the same hierarchy.
意思是说:@Value是经过BeanPostProcessor来处理的,而WebApplicationContex和ApplicationContext是独自处理的,所以WebApplicationContex 不能运用父容器的特点值。
所以,Controller 不满意 Service 的需求,不要把事务代码写在 Controller 类中。
3、服务端三层架构
SpringMVC 服务端选用经典的三层架构,即体现层、事务层、耐久层,别离选用@Controller、@Service、@Repository进行类注解。
体现层(Presentation):又称操控层(Controller),担任接纳客户端恳求,并向客户端呼应成果,一般选用HTTP协议。
事务层(Business):又称服务层(Service),担任事务相关逻辑处理,依照功用分为服务、作业等婚纱,Java服务器端像大市场点相同乱-安博电竞 官网_安博电竞进口_安博电竞网页版。
耐久层(Persistence):又称库房层(Repository),担任数据的耐久化,用于事务层拜访缓存和数据库。
所以,把事务代码写入到Controller类中,是不契合SpringMVC服务端三层架构标准的。
把耐久层代码写在 Service 中
把耐久层代码写在 Service 中,从功用上来看并没有什么问题,这也是许多人欣然承受的原因。
1、引起以下首要问题
- 事务层和耐久层稠浊在一同,不契合SpringMVC服务端三层架构标准;
- 在事务逻辑中组装句子、主键等,增加了事务逻辑的杂乱度;
- 在事务逻辑中直接运用第三方中间件,不便于第三方耐久化中间件的替换;
- 同一目标的耐久层代码涣散在各个事务逻辑中,背离了面临目标的编程思维;
- 在写单元测验用例时,无法对耐久层接口函数直接测验。
这儿以数据库耐久化中间件 Hibernate 的直接查询为例。
现象描绘:
/** 用户服务类 */
@Service
public classUserService{
/** 会话工厂 */
@Autowired
private SessionFactory sessionFactory;
/** 依据工号获取用户函数 */
public UserVO g方炯斌etUserByEmpId( StringempId) {
// 组装HQL句子
Stringhql = "from t_user where emp_id = '"+ empId + "'";
// 履行数据库查询
Query query = sessionFactory.getCurrentSession.createQuery(hql);
List<UserDO> userList = query.list;
if(CollectionUtils.isEmpty(userList)) {
returnnull;
}
// 转化并回来用户
UserVO userVO = newUserVO;
BeanUtils.copyProperties(userList. get( 0), userVO);
returnuserVO;
}
}
主张计划:
/** 用户DAO类 */
@Repository
publicclassUserDAO{
/** 会话工厂 */
@Autowired
privateSessionFactory sessionFactory;
/** 依据工号获取用户函数 */
publicUserDO 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)) {
returnnull;
}
// 回来用户信息
returnuserList. get( 0);
}
}
/** 用户服务类 */
@Service
publicclassUserService{
/** 用户DAO */
@Autowired
privateUserDAO userDAO;
/** 依据工号获取用户函数 */
publicUserVO getUserByEmpId(String empId) {
// 依据工号查询用户
UserDO userDO = userDAO.getUserByEmpId(empId);
if(Objects.isNull(userDO)) {
returnnull;
保健品}
// 转化并回来用户
UserVO userVO = newUserVO;
BeanUtils.copyProperties(userDO, userVO);
returnuserVO;
}
}
关于插件:
阿里的 AliGenerator 是一款根据 MyBatis Generator 改造的 DAO 层代码主动生成东西。运用 AliGenerator 生成的代码,在履行杂乱查询的时分,需求在事务代码中组装查询条件,使事务代码显得特别臃肿。
/** 用户服务类 */
@Service
public classUserService{
/** 用户DAO */
@Autowired
private UserDAO 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)) {
returnnull;
}
// 转化并回来用户
UserVO userVO = newUserVO;
BeanUtils.copyProperties(userList. get( 0), userVO);
returnuserVO;
}
}
个人不喜爱用 DAO 层代码生成插件,更喜爱用原汁原味的 MyBatis XML 映射,首要原因如下:
- 会在项目中导入一些不契合标准的代码;
- 只需求进行一个简略查询,也需求导入一整套杂乱代码;
- 进行杂乱查询时,组装条件的代码杂乱且不直观,不如在XML中直接编写SQL句子;
- 改变表格后需求从头生成代码并进行掩盖,可能会不小心删去自界说函数。
当然,已然挑选了运用 DAO 层代码生成插件,在享用便当的一同也应该承受插件的缺陷。
3、把 Redis 代码写在 Service 中现象描绘:
/** 用户服务类 */
@Service
public classUserS小敏原唱这条路一同走ervice{
/** 用户DAO */
@Autowired
private UserDAO userDAO;
/** Redis模板 */
@Autowired
private RedisTemplate< String, String> redisTemplate;
/** 用户主键形式 */
private staticfinalStringUSER_KEY_PATTERN = "hash::user::%s";
/** 保存用户函数 */
public voidsaveUser(UserVO user) {
// 转化用户信息
UserDO userDO = transUser(user);
// 保存Redis用户
StringuserK石灵明ey = 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
public classUserRedis{
/** Redis模板 */
@Autowired
private RedisTemplate< String, String> redisTemplate;
/** 主键形式 */
private staticfinalStringKEY_PATTERN = "hash::user::%s";
/** 保存用户函数 */
public UserDO save(UserDO user) {
Stringkey = 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
public classUserService{
/** 用户DAO */
@Autowired
private UserDAO userDAO;
/** 用户Redis */
@Autowired
private UserRedis userRedis;
/** 保存用户函数 */
public voidsaveUser(UserVO user) {
// 转化用户信息
UserDO userDO = transUser(user);
// 保存Redis用户
userRedis.save(userDO);
// 保存数据库用户
userDAO.save(userDO);
}
}
把一个 Redis 目标相关操作接口封装为一个 DAO 类,契合面临目标的编程思维,也契合 SpringMVC 服务端三层架构标准,更便于代码的办理和保护。
把数据库模型类露出给接口
1、现象描绘
/** 用户DAO类 */
@Repository
publicclassUserDAO{
/** 获取用户函数 */
publicUserDO getUser( LonguserId) {...}
}
/** 用户服务类 */
@Service
publicclassUserService{
/** 用户DAO */
@Autowired
privateUserDAO userDAO;
/** 获取用户函数 */
publicUserDO getUser( LonguserId) {
returnuserDAO.getUser(userId);
}
}
/** 用户操控器类 */
@Controller
@RequestMapping("/user")
publicclassUserController{
/** 用户服务 */
@Autowired
privateUserService userServicdonee;
/** 获取用户函数 */
@RequestMapping(path = "/getUser", maccompanyethod = RequestMethod.GET)
publicResult<UserDO> getUser( @RequestParam(name = "userId", m24狙击步枪required = true)LonguserId) {
UserDO user = userService.getUser(userId);
returnResult.success(user);
}
}
上面的代码,看上去是满意 SpringMVC 服务端三层架构的,仅有的问题便是把数据库模型类 UserDO 直接露出给了外部接口。
2、存在问题及解决计划
存在问题:
- 直接露出数据库表格规划,给竞赛对手竞品剖析带来便利;
- 假如数据库查询不做字段约束,会导致接口数据巨大,糟蹋用户的名贵流量;
- 假如数据库查询不做字段约束,简单把灵敏字段露出给接口,导致呈现数据的安全问题;
- 假如数据库模型类不能满意接口需求,需求在数据库模型类中增加其他字段,导致数据库模型类跟数据库字段不匹配问题;
- 假如没有保护好接口文档,经过阅览代码是无法分辨出数据库模型类中哪些字段是接口运用的,导致代码的可保护性变差。
解决计划:
- 从办理制度上要求数据库和接口的模型类彻底独立;
- 从项目结构上约束开发人员把数据库模型类露出给接口。
下面,将介绍怎么更科学地建立 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、一条不太主张的主张有人会问:接口模型和耐久层模型别离,接口界说了一个查询数据模型VO类,耐久层也需求界说一个查询数据模型DO类;接口界说了一个回来数据模型VO类,耐久层咪咕直播也需求界说一个回来数据模型DO类……这样,关于项目前期快速迭代开发十分晦气。能不能只让接口不露出耐久层数据模型,而可以让耐久层运用接口的数据模型?
假如从SpringMVC服务端三层架构来说,这是不答应的,由于它会影响三层架构的独立性。可是,假如从快速迭代开发来说,这是答应的,由于它并不会露出数据库模型类。所以,这是一条不太主张的主张。
/** 用户DAO类 */
@Repository
publicclassUserDAO{
/** 计算用户函数 */
publicLongcountByParameter(QueryUserParameterVO parameter) {...}
/** 查询用户函数 */
publicList<UserVO> quer模拟城市5yByParameter(QueryUserParameterVO par婚纱,Java服务器端像大市场点相同乱-安博电竞 官网_安博电竞进口_安博电竞网页版ameter) {...}
}
/** 用户服务类 */
@Service
publicclassUserService{
/** 用户DAO */
@Autowired
privateUserDAO userDAO;
/** 查询用户函数 */
publicPageData<UserVO> queryUser(QueryUserParameterVO parameter) {
LongtotalCount = userDAO.countByParameter(parameter);
List<UserVO> userList = null;
if(Objects.nonNull(totalCount) && totalCount.compareTo( 0L) > 0) {
userList = userDAO.queryByParameter(parameter);
}
returnnew PageData<>(totalCount, userList);
}
}
/** 用户操控器类 */
@Controller
@RequestMapping("/user")
publicclassUserController{
/** 用户服务 */
@Autowired
privateUserService userService;
/** 查询用户函数(parameter中包含分页参数startIndex和pageSize) */
@RequestMapping(path = "/queryUser", method = RequestMethod.POST)
publicResult<PageData<UserVO>> queryUser( @Valid@RequestBodyQueryUserParameterVO parameter) {
PageData<UserVO> pageData = userService.queryUser(parameter);
returnResult.success(pageData);
}
}
“仁者见仁、智者见智”,每个人都有自己的主意,而文章的内容也仅仅我的一家之言。
谨以此文献给那些我作业过的创业公司,是您们从前甩手让我去整改乱象,让我从中获益颇深并得以技能生长。
作者简介:陈昌毅,诨名常意,高德地图技能专金士顿家,2018年参加阿里巴巴,一向从事地图数据收集的相关作业。
声明:本文系作者投稿,版权归作者一切。
婚纱,Java服务器端像大市场点相同乱-安博电竞 官网_安博电竞进口_安博电竞网页版