最全那些年,我们见过的 Java 服务端乱象(1),java面试问题大全及答案大全基础

写在最后

还有一份JAVA核心知识点整理(PDF):JVM,JAVA集合,JAVA多线程并发,JAVA基础,Spring原理,微服务,Netty与RPC,网络,日志,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算…

image

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

}

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

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进行类注解。

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

表现层(Presentation):又称控制层(Controller),负责接收客户端请求,并向客户端响应结果,通常采用HTTP协议。

业务层(Business):又称服务层(Service),负责业务相关逻辑处理,按照功能分为服务、作业等。

持久层(Persistence):又称仓库层(Repository),负责数据的持久化,用于业务层访问缓存和数据库。

所以,把业务代码写入到Controller类中,是不符合SpringMVC服务端三层架构规范的。

3.把持久层代码写在 Service 中

========================

把持久层代码写在 Service 中,从功能上来看并没有什么问题,这也是很多人欣然接受的原因。

3.1.引起以下主要问题

================

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

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

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

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

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

3.2.把数据库代码写在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 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 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 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 */

@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 服务端三层架构规范,更便于代码的管理和维护。

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

==================

4.1.现象描述

============

/** 用户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 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)都依赖该模型项目,关系图如下:

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

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

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

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

=================

模型分离的项目搭建,单独搭建API项目(example-api),抽象出对外接口及其模型VO类。业务层项目(example-service)实现了这些接口,并向表现层项目(example-webapp)提供服务。表现层项目(example-webapp)只调用API项目(example-api)定义的服务接口。

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

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

**风险:**表现层项目(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)中定义的接口函数。

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

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

最后

手绘了下图所示的kafka知识大纲流程图(xmind文件不能上传,导出图片展现),但都可提供源文件给每位爱学习的朋友

image.png

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

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

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

最后

手绘了下图所示的kafka知识大纲流程图(xmind文件不能上传,导出图片展现),但都可提供源文件给每位爱学习的朋友

[外链图片转存中…(img-OT8XWqaz-1715615567257)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 12
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值