Java开发规范
一,前言
为什么我们需要规范?
在现在的环境中,我们的开发一般都是多人协作的,而每个人的风格,习惯,水平,是高低不同的,如果不重视开发规范,等到一个软件经过几波人开发维护的时候,就会失控,就会形成"屎山",后面的继任者难免会默默问候前任的家人。且受制于个人的水平影响会出现一些难以排查的BUG在里面。而规范就是避免大家在屎山上翻天倒地,是提高效率,提高代码质量的最有效的方式,因此我们每个人都要重视规范问题,那么你心中有属于你自己的一整套规范吗?如果没有,请在你的心中建立起你自己的规范标准,毕竟,良好的习惯总是会让人受益的。
二,项目结构
该部分将和大家一起探讨一下各层划分和一些注意事项。
- Controller 层
- Service 层
- Mapper 层
1,Controller 层
controller层是负责接收请求,封装参数,然后分发给service层处理业务逻辑,接收service层处理结果,最后返回给前端。
在这一层处理的动作有几个:
- 入参封装
- 调用service层
- 返回结果
-
入参
对于参数的封装,实际上框架已经为我们处理完成了,一般情况下我们不需要再去做什么多余的动作封装参数,这里涉及到两个两个地方需要我们去注意:
- 参数的结构
- 参数的校验
第一个是参数的结构,实际上,我们需要把参数这一领域对象单独剥离出来,已区分
vo,dto等不同的领域对象,我们就命名为param,该领域对象的命名均以Param结尾,例:SaveUserParam, CreateMeetingParam。参数这一领域对象需要注意的是,不同接口的Param请不要为了方便全而部封装到一个Param对象里面去,尽可能的保证Param里面的字段都是这个接口所需要,如果有几个接口都依赖同一个param,那么将导致的是在某一个接口中不能快速的辨别出这个接口对应的业务逻辑到底需要什么参数,这将增加维护成本,且对继任者非常不友好。当然,对于一些常规的入参我们可以提前封装好放入到common包中加以复用。例如以下几个参数均是通用的,就可以封装到comm包中去。/** * ID入参 * * @author yb * @date 2021/12/14 */ @Data public class IdParam implements Serializable{ private static final long serialVersionUID = 1056308970191184678L; @NotNull(message = "id不能为空") private Long id; }/** * ID List入参 * * @author yb * @date 2021/12/14 */ @Data public class IdsParam implements Serializable{ private static final long serialVersionUID = 5569581205515805881L; @NotNull(message = "ids不能为空") private List<Long> ids; }/** * 分页入参 ,如果还有其它查询条件,可以再新建新的入参 XXXPageParam 来继承这个类,增加具体的条件字段即可 * * @author yb * @date 2021/12/14 */ @Data public class PageParam implements Serializable{ private static final long serialVersionUID = -7061744491023596432L; @NotNull(message = "limit不能为空") @Min(value = 1, message = "limit不能小于1") private Integer limit; @NotNull(message = "page不能为空") @Min(value = 1, message = "页数不能小于1") private Integer page; }第二个是参数校验,对于参数的校验,请不要在
controller层使用if...之类的校验方式,尽量保持我们代码的整洁度,相信整洁的代码风格会让自己和别人赏心悦目的。对于参数的校验我们需要在Param对象中完成校验,就如同上面的@NotNull之类的注解完成校验,这依赖于implementation "javax.validation:validation-api",对于新手特别注意是的@NotNull和@NotBlank的使用场景,如果你的参数是String类型,那么请使用@NotBlank。在此,可能会出现一下特别的情况,例如我们需要校验开始时间不能大于结束时间这些的自定义校验规则,我们可以在
param内提供实例方法valid()去校验,然后在controller层调用param.valid()方法进行校验。例:/** * 创建入参 * * @author yb * @date 2021/12/14 */ @Data public class CreateParam implements Serializable{ private static final long serialVersionUID = -6315517102075213753L; @NotNull(message = "开始时间不能为空") private LocalDateTime startTime; @NotNull(message = "结束时间不能为空") private LocalDateTime endTime; public void valid() { if (startTime.isAfter(endTime)) { throw new BusinessException("开始时间不能小于结束时间"); } } }@RestController @RequestMapping("/clientApplication") public class ClientApplicationController { @Autowired private ClientApplicationService clientApplicationService; /** * 新增或修改 * * @param param 入参 * @return vo */ @PostMapping("/save") public void save(@RequestBody @Validated SaveClientApplicationParam param) { // 自定义参数校验 param.valid(); clientApplicationService.save(param); } } -
调用
service层并没有什么需要注意的地方,常规调用即可。 -
返回结果,如同入参一样,单独剥离出来,就是
Vo对象,这里需要注意的是不同的接口应该尽量返回不同的VO对象,原因也和入参一样,别人不能快速分辨出那些字段是这个接口返回的。对于调用者来说,我们需要保持一致的数据结构,例:
{ "code":200, "message":"success", "timestamp":"2021-12-20 00:23:59" "data":{ "id":10001, "name":"测试" } }对应的实体结构如下:
/** * 返回结果统一封装 * * @author yb * @date 2021/12/7 */ @Data public class ResultMsg<T> implements Serializable{ /** * 状态码 */ private Integer code; /** * 消息 */ private String message; /** * 时间戳 */ private Date timestamp; /** * 数据 */ private T data; public static ResultMsg<String> success() { ResultMsg<String> resultMsg = new ResultMsg<>(); resultMsg.setCode(0); resultMsg.setMessage("success"); resultMsg.setTimestamp(new Date()); return resultMsg; } public static <T> ResultMsg<T> success(T data) { ResultMsg<T> resultMsg = new ResultMsg<>(); resultMsg.setCode(200); resultMsg.setMessage("success"); resultMsg.setTimestamp(new Date()); resultMsg.setData(data); return resultMsg; } public static ResultMsg<String> error() { ResultMsg<String> resultMsg = new ResultMsg<>(); resultMsg.setCode(500); resultMsg.setMessage("error"); resultMsg.setTimestamp(new Date()); return resultMsg; } public static ResultMsg<String> error(String msg) { ResultMsg<String> resultMsg = new ResultMsg<>(); resultMsg.setCode(500); resultMsg.setMessage(msg); resultMsg.setTimestamp(new Date()); return resultMsg; } public static ResultMsg<String> error(Integer code, String msg) { ResultMsg<String> resultMsg = new ResultMsg<>(); resultMsg.setCode(code); resultMsg.setMessage(msg); resultMsg.setTimestamp(new Date()); return resultMsg; } }在
controller层中我们不必每个接口都指定ResultMsg<UserVo>这样的返回类型,如果有返回值,直接返回UserVo对象,对于ResultMsg<T>的封装可以实现ResponseBodyAdvice<Object>接口配合@ControllerAdvice注解进行统一的返回实现,尽量保证我们代码的简单度和整洁度。对于分页返回值也可以进行统一的封装放入
common包中进行复用,例:/** * 统一分页数据返回VO * * @author yb * @date 2021/12/14 */ @Data @NoArgsConstructor @AllArgsConstructor public class PageVo<T> implements Serializable { /** * 总条数 */ private int total = 0; /** * 数据 */ private List<T> data; }
2,Service 层
service层通常是接收controller层的参数,进行具体的业务逻辑处理,并返回最终的结构。它的动作主要有下面几个:
- 接收controller层入参
- 业务处理
- 返回处理结果
-
入参
我们在controller层封装了
param入参对象,那么在service层怎么去接收呢?当我们service层所需要的参数小于3个时,我们就可以分别用三个字段来接收参数,如果大于三个参数时,我们可以传入param进行接收,例:/** * 入参 小于两个参数 * * @author yb * @date 2021/12/14 */ public class CreateUserParam implements Serializable{ @NotBlank(message = "姓名不能为空") private String name; @NotBlank(message = "性别不能为空") private String sex; } @RestController @RequestMapping("/clientApplication") public class ClientApplicationController { @Autowired private ClientApplicationService clientApplicationService; /** * 新增或修改 * * @param param 入参 * @return vo */ @PostMapping("/save") public void save(@RequestBody @Validated CreateUserParam param) { clientApplicationService.save(param.getName(),param.getSex()); } } @Service public class ClientApplicationService { // 使用两个参数接收 public void save(String name,String sex){ // TODO ... } }为什么呢?因为
service层的方法,不只是在controller层进行调用,也可能是其它service层进行调用,如果是其它service层调用,就不需要new CreateUserParam()进行入参了。因此在一定程度上,我们在service层的方法入参上尽可能保证数据的原始类型。 -
业务处理 ,依赖注入问题
接收入参后,我们就着手于具体的业务处理流程,在处理业务流程之前,我们先说一下依赖注入的问题,
service调用mapper层进行数据持久化,一般的,一个service层只能依赖一个mapper对象,不能依赖于第二个mapper对象,如果需要用到其它的mapper对象进行持久化,那么请引入 需要的mapper对应的service层依赖并在其提供出方法进行调用,mapper对象请不要到处注入,到处引用,保证mapper层的简单性,对数据的入口进行控制,便于后期的维护,扩展及代码的复用性。对于
lombok框架提供的@RequiredArgsConstructor注解注入依赖,极其容易出现循环依赖问题,因此不推荐使用该方式进行依赖注入,请使用@Autowired或构造器的方式进行依赖注入。在具体的业务处理流程中,我们最需要关心的是业务的流程处理,而其它的东西是我们不需要过多关心的且需要我们在
service层保证代码的简洁性。在业务处理过程中,我们可以把数据的流转过程放到POJO对象(仅限于Vo和Param对象)中进行处理,避免service层中出现过多我们不需关心的伪业务逻辑。例:public class ClientApplicationService { @Autowired private UserDao userDao; public UserVo save(SaveUserParam param){ User user; if(Objects.isNull(param.getId())){ // 如果ID为空,则为新增 // 将param对象流转为实体对象,在param中进行流转 user = param.transformCreate(); userDao.create(user); } else { // 如果ID不为空,则为修改 // 将param对象流转为实体对象,在param中进行流转 user = param.transformUpdate(); userDao.update(user); } // 实体对象流转为Vo对象,在vo中进行流转 return UserVo.transform(user); } } public class SaveUserParam implements Serializable{ private Long id; @NotBlank(message = "姓名不能为空") private String name; @NotBlank(message = "年龄不能为空") private String age; public User transformCreate(){ // 数据流转 User user = new User(); user.setName(name); user.setAge(age); user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(LocalDateTime.now()); return user; } public User transformUpdate(){ // 数据流转 User user = new User(); user.setId(id) user.setName(name); user.setAge(age); user.setUpdateTime(LocalDateTime.now()); return user; } } public class UserVo implements Serializable{ private Long id; private String name; private LocalDateTime createTime; public static UserVo transform(User user){ // 数据流转 if(Objects.isNull(user)){ return null; } UserVo vo = new UserVo(); vo.setId(user.getId()); vo.setName(user.getName()); vo.setCreateTime(user.getCreateTime()); return vo; } public static List<UserVo> transform(List<User> users){ // 数据流转 if (CollectionUtils.isEmpty(users)) { return Collections.emptyList(); } List<UserVo> result = new ArrayList<>(users.size()); for (User user : users) { result.add(transform(user)); } return result; } }如果我们要把
param流转为实体类型,我们可以在param中提供实例方法 transform()进行数据流转。如果我们要把
实体类型流转为vo对象,我们可以在vo中提供静态的 transform()方法进行数据流转。这样我们在
service中就能避免一堆的数据set伪业务逻辑方法,我们只关心具体的,核心的业务处理方法就可以了。对于
transform方法,我们可以借助Idea的插件generateAllSet快速的生成对象的set方法,避免我们手动set的时候容易出错。在业务处理过程中,往往会根据处理条件判断是否进行继续业务处理,如果不满足条件,我们更通常的做法是抛出自己的业务异常
BusinessException,并配置注解@RestControllerAdvice和注解@org.springframework.web.bind.annotation.ExceptionHandler(value = BusinessException.class)进行一个全局的异常捕获处理,统一处理后返回统一的异常错误信息格式返回给前端。一般的,我们经常捕获下面几个常用的异常进行统一的处理即可:- NoHandlerFoundException 404异常
- MethodArgumentNotValidException 方法入参异常,配合Controller层的
@Validated一起使用 - BusinessException 自定义的业务异常处理
- Exception 未知的异常处理
import com.demo.comm.ResultMsg; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.NoHandlerFoundException; import java.util.List; /** * 异常处理 * {@link RestControllerAdvice} 注解返回 ResponseBody * * @author yb * @date 2021/4/5 */ @Slf4j @RestControllerAdvice public class ExceptionHandler { public ExceptionHandler() { } /** * 获取错误提示 * * @param objectErrors 错误LIST * @return 错误提示 */ private static String getErrorMsg(List<ObjectError> objectErrors) { StringBuilder msgBuilder = new StringBuilder(); for (ObjectError objectError : objectErrors) { msgBuilder.append(objectError.getDefaultMessage()).append(","); } String errorMessage = msgBuilder.toString(); return errorMessage.length() > 1 ? errorMessage.substring(0, errorMessage.length() - 1) : errorMessage; } /** * 404 异常处理 * * @param ex 异常 * @return json结果 */ @org.springframework.web.bind.annotation.ExceptionHandler(value = NoHandlerFoundException.class) public ResultMsg<String> noFoundExceptionHandler(Exception ex) { log.error("404异常", ex); return ResultMsg.error(404, "资源未找到"); } /** * 方法入参异常 * * @param ex 异常 * @return json结果 */ @org.springframework.web.bind.annotation.ExceptionHandler(value = MethodArgumentNotValidException.class) public ResultMsg<String> methodArgValidExceptionHandler(MethodArgumentNotValidException ex) { if (log.isDebugEnabled()) { log.warn("方法入参异常", ex); } else { log.error("方法入参异常", ex); } return ResultMsg.error(-2, getErrorMsg(ex.getBindingResult().getAllErrors())); } /** * 业务异常处理 * * @param ex 业务异常 * @return json结果 */ @org.springframework.web.bind.annotation.ExceptionHandler(value = BusinessException.class) public ResultMsg<String> businessExceptionHandler(BusinessException ex) { if (log.isDebugEnabled()) { log.warn("业务异常", ex); } else { log.warn("业务异常:{}", ex.getMsg()); } return ResultMsg.error(ex.getCode(), ex.getMsg()); } /** * 其它异常处理器 * * @param ex 异常 * @return json结果 */ @org.springframework.web.bind.annotation.ExceptionHandler(value = Exception.class) public ResultMsg<String> unKnowExceptionHandler(Exception ex) { log.error("未知异常", ex); return ResultMsg.error(-1, "系统正在升级中,请稍后重试"); } }
3,Mapper 层
mapper层负责对数据的持久化,因此,不应该依赖于其它服务或其它mapper,不包含具体的业务逻辑,它的方法返回对象应该是实体对象或dto对象,不应该出现第三种类型的数据对象。
4,总结
最后,让我们全部串联起来看一下整体的一个处理流程
各层的依赖注入范围
| - | Controller | Service | Mapper |
|---|---|---|---|
| Controller | 不允许 | 允许 | 不允许 |
| Service | 不允许 | 允许 | 允许 |
| Mapper | 不允许 | 不允许 | 不允许 |
应用分层参考:Application Layering
参考,项目结构
>>点击展开查看
.
└── com.demo
├── controller
├── pojo
| ├── domain
| | ├── User.java
| | └── ...
| ├── dto
| | ├── UserDto.java
| | └── ...
| ├── mapper
| | ├── UserMapper.java
| | └── ...
| ├── em: 枚举
| | ├── EnableEnum.java
| | └── ...
| ├── param
| | ├── PageParam.java
| | └── ...
| └── vo
| | ├── PageVo.java
| | └── ...
├── service
| ├── impl
| | ├── UserServiceImpl.java
| | └── ...
| └── XxxService.java
├── common
| ├── annotation
| ├── utils
| └── ...
├── config
| ├── BeanConfig.java
| └── ...
└── DemoApplicationBootstrap.java
二,其它注意事项
-
资源释放
对应一些I/O流,我们需要进行
close()手动关闭:public static void out(BufferedImage bufferedImage, HttpServletResponse response) throws IOException { ServletOutputStream outputStream = null; try { outputStream = response.getOutputStream(); response.setContentType(CONTENT_TYPE); response.setHeader(HEADER_TYPE, HEADER_TYPE_IMAGE); ImageIO.write(bufferedImage, IMAGE_TYPE, outputStream); } catch (Exception e) { log.error("输出图片 写入http响应异常", e); throw new BusinessException("图片异常"); }finally { if(Objects.nonNull(outputStream)){ outputStream.close(); } } }然而,在jdk1.7中增加了一种关闭流的方式
try-with-resource,让我们避免臃肿的代码,条件是这个资源实现了AutoCloseable接口。public static void out(BufferedImage bufferedImage, HttpServletResponse response) { try (ServletOutputStream outputStream = response.getOutputStream()) { response.setContentType(CONTENT_TYPE); response.setHeader(HEADER_TYPE, HEADER_TYPE_IMAGE); ImageIO.write(bufferedImage, IMAGE_TYPE, outputStream); } catch (Exception e) { log.error("输出图片 写入http响应异常", e); throw new BusinessException("图片异常"); } } -
枚举的应用
枚举是一个很好的玩意,对于某个类中的状态我们经常能看到用
Integer类型去维护,状态为1的时候表示被启用了,状态为0的时候表示没有被禁用了...类似这种状态的维护采用
Integer类型是多么的不直观,甚至类设计者不写注释都不知道1代表的是什么意思,0又表示什么意思,这个时候,你需要去用枚举维护这个状态:import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import lombok.Getter; /** * 可用可不用 * * @author qxun * @date 2021/4/5 */ @Getter public enum EnableEnum { /** * */ ENABLE(1, "启用"), DISABLE(0, "禁用"); /** * mybatisplus,标记数据库存的值是code */ @EnumValue @JsonValue private final int code; private final String desc; EnableEnum(int code, String desc) { this.code = code; this.desc = desc; } @JsonCreator public static EnableEnum forValue(Integer code) { return code == ENABLE.code ? ENABLE : DISABLE; } }这样就很清晰的展现出所有的状态,很多ORM框架是支持枚举类型的,因此使用枚举是很容易,简单的。
-
复杂数据的查询,可以分成多次查询,在service层进行数据合并
-
日志的打印
对于debug日志,尽管我们可以在配置文件中配置日志的级别,但是在打印的时候任然需要判断下当前日志的级别,避免在线上环境中多余的字符串拼接操作:
if (log.isDebugEnabled()) { log.debug("用户信息:{}", user); } -
避免隐形的自动拆装箱
public void create(){ createByStatus(false); } public void createByStatus(Boolean status){ ... }例如上面的入参 基本类型
false就会触发自动拆装箱,因此在使用基本类型的时候要注意是否会触及自动拆装箱。public void create(){ createByStatus(Boolean.FALSE); } public void createByStatus(Boolean status){ ... }
END,有机会再补充完善,写的比较粗糙,如果对你有所帮助,那么我将感到很高兴 (๑╹◡╹)ノ