Java 服务端乱象大盘点
The following article is from 阿里巴巴中间件 Author 陈昌毅
使用Controller基类和Service基类
1、现象描述
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) {...}
...
}
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) {...}
...
}
2、论证基类必要性
里氏代换原则(Liskov Substitution Principle,简称LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
子类拥有父类的所有方法和属性,从而减少了创建子类的工作量;
提高了代码的重用性,子类拥有父类的所有功能;
提高了代码的扩展性,子类可以添加自己的功能。
Controller 基类和 Service 基类在整个项目中并没有直接被使用,也就没有可使用其子类替换基类的场景,所以不满足里氏替换原则;
Controller 基类和 Service 基类并没有抽象接口函数或虚函数,即所有继承基类的子类间没有相关共性,直接导致在项目中仍然使用的是子类;
Controller 基类和 Service 基类只关注了重用性,即子类能够轻松使用基类的注入DAO、注入服务、注入参数、静态常量、服务函数、静态函数等资源。但是,忽略了这些资源的必要性,即这些资源并不是子类所必须的,反而给子类带来了加载时的性能损耗。
3、拆分基类的方法
把注入实例放入实现类
@Service
public class UserService {
/** 用户DAO */
@Autowired
private UserDAO userDAO;
/** 短信服务 */
@Autowired
private SmsService smsService;
/** 系统名称 */
@Value("${example.systemName}")
private String systemName;
...
}
把静态常量放入常量类
public class ExampleConstants {
/** 超级用户标识 */
public static final long SUPPER_USER_ID = 0L;
...
}
把服务函数放入服务类
@Service
public class UserService {
/** 获取用户函数 */
public UserDO getUser(Long userId) {...}
...
}
/** 公司服务类 */
@Service
public class CompanyService {
/** 用户服务 */
@Autowired
private UserService userService;
/** 获取管理员 */
public UserDO getManager(Long companyId) {
CompanyDO company = ...;
return userService.getUser(company.getManagerId());
}
...
}
把静态函数放入工具类
public class UserHelper {
/** 获取用户名称 */
public static String getUserName(UserDO user) {...}
...
}
把业务代码写在 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);
}
...
}
@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);
}
}
系统(null)欢迎您访问!
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.
3、服务端三层架构
把持久层代码写在 Service 中
1、引起以下主要问题
业务层和持久层混杂在一起,不符合SpringMVC服务端三层架构规范;
在业务逻辑中组装语句、主键等,增加了业务逻辑的复杂度;
在业务逻辑中直接使用第三方中间件,不便于第三方持久化中间件的替换;
同一对象的持久层代码分散在各个业务逻辑中,背离了面对对象的编程思想;
在写单元测试用例时,无法对持久层接口函数直接测试。
2、把数据库代码写在Service中
现象描述:
@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<UserDO> userList = query.list();
if (CollectionUtils.isEmpty(userList)) {
return null;
}
// 转化并返回用户
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userList.get(0), userVO);
return userVO;
}
}
建议方案:
@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<UserDO> 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;
}
}
关于插件:
@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<UserDO> userList = userDAO.selectByParam(userParam);
if (CollectionUtils.isEmpty(userList)) {
return null;
}
// 转化并返回用户
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userList.get(0), userVO);
return userVO;
}
}
会在项目中导入一些不符合规范的代码;
只需要进行一个简单查询,也需要导入一整套复杂代码;
进行复杂查询时,拼装条件的代码复杂且不直观,不如在XML中直接编写SQL语句;
变更表格后需要重新生成代码并进行覆盖,可能会不小心删除自定义函数。
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);
}
}
建议方案:
@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);
}
}
把数据库模型类暴露给接口
1、现象描述
@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<UserDO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
UserDO user = userService.getUser(userId);
return Result.success(user);
}
}
2、存在问题及解决方案
间接暴露数据库表格设计,给竞争对手竞品分析带来方便;
如果数据库查询不做字段限制,会导致接口数据庞大,浪费用户的宝贵流量;
如果数据库查询不做字段限制,容易把敏感字段暴露给接口,导致出现数据的安全问题;
如果数据库模型类不能满足接口需求,需要在数据库模型类中添加别的字段,导致数据库模型类跟数据库字段不匹配问题;
如果没有维护好接口文档,通过阅读代码是无法分辨出数据库模型类中哪些字段是接口使用的,导致代码的可维护性变差。
从管理制度上要求数据库和接口的模型类完全独立;
从项目结构上限制开发人员把数据库模型类暴露给接口。
3、项目搭建的三种方式
第1种:共用模型的项目搭建
第2种:模型分离的项目搭建
第3种:服务化的项目搭建
4、一条不太建议的建议
有人会问:接口模型和持久层模型分离,接口定义了一个查询数据模型VO类,持久层也需要定义一个查询数据模型DO类;接口定义了一个返回数据模型VO类,持久层也需要定义一个返回数据模型DO类……这样,对于项目早期快速迭代开发非常不利。能不能只让接口不暴露持久层数据模型,而能够让持久层使用接口的数据模型?
@Repository
public class UserDAO {
/** 统计用户函数 */
public Long countByParameter(QueryUserParameterVO parameter) {...}
/** 查询用户函数 */
public List<UserVO> queryByParameter(QueryUserParameterVO parameter) {...}
}
/** 用户服务类 */
@Service
public 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);
}
}