从零搭起一个Java Web架子
写在前面
老规矩,先来个写在前面!上个月做完了那套前后端分离的小项目。学了学Spring Boot +
Maven聚合。便简单重构了一下那套,基于SSM搭的后端架构。先简单说一下之前的几个痛点吧!
- 异常处理
@RequestMapping("/pushComment")
public PublicResult pushComment(@RequestBody PushCommentVo bean, HttpServletRequest request) {
try {
// 获取登录的user
User user = (User) request.getSession().getAttribute("user");
if (user != null) { // 登录过了 ,可以操作
// 保存评论
Comment comment = commentService.save(bean);
return new PublicResult(true, Code.PUSH_OK, comment, "评论成功");
} else {
return new PublicResult(false, Code.PUSH_ERROR, null, "请登录");
}
} catch (Exception e) {
return new PublicResult(false, Code.PUSH_ERROR, ExceptUtil.getSimpleException(e), "评论失败");
}
}
拿发布评论来举个例子。一万个接口,就写了一万个trycatch。并且返回的异常也只是做了
简单的封装,返回的了简单的异常信息。
- 返回的Json过于草率
// 本类是拿来返回前端数据的实体类
@Data // 生成get和set
@AllArgsConstructor // 生成全参构造
@NoArgsConstructor // 生成空构造方法
@ToString // 生成toString方法
public class PublicResult {
// 状态
private Boolean success;
// 操作结果编码
private Integer code;
// 操作数据结果
private Object data;
// 返回消息
private String message;
}
其实也没有什么问题,反正就是什么都往里面装呗!对象皆可装、不是对象也可以装。
返回前端的东西写起来很方便。但是返回的信息,有些接口会过于冗余,比如客户端调用
一个接口,他可能只是想要一个描述信息。但是其他的东西全返回了。代码看起来不好看。
实体类开局,从数据库走到前台页面
一个实体类对应一张表。那个实体类做的事情有些多。映射数据库字段、作为返回前端
Result。还可能兼职接收前端传过来的参数。这就导致了一个大问题。数据库映射的数据,
前端可能不需要那么多,而且有些数据,客户端也不应该可见。
"user": {
"id": 4,
"createdTime": 1647146326000,
"email": "zhiyan@qq.com",
"password": null,
"name": "志颜",
"birthday": 1011456000000,
"address": "贵州",
"phone": "13142221",
"photo": "http://localhost:8080/xhywblog/upload/img/b2c5be86-b0f8-4b1b-b776-4f931c429d3b.jpeg",
"job": "FWF实习生",
"intro": "天气不太行!!",
"trait": "账号老被偷",
"interests": "看晨曦偷我账号",
"gender": 1,
"dynamicCount": 8,
"friendsCount": 0,
"fansCount": 0,
"profileUrl": "http://120.25.125.57:8080/xhywblog/users/u4",
"followMe": 0,
"following": 0,
"background": null
}
譬如这是一条动态里注入的用户信息。前端可能就只是需要【昵称(name)、头像(photo)、
用户链接(profileUrl)】。但是返回了一万个字段,前端都用不到,大大影响了用户的请求
速度,前端写代码看着这数据也ex。
- 没有充分利用Mybatis plus的特点【单表特顶】
// BaseMapper<User> 继承Mybatis-plus 的类 ,泛型:表的实体对象
public interface UserDao extends BaseMapper<User> {
// Mybatis-plus默认提供了很多操作数据库的方法, 可以点进去看看,也可以百度一下。
// 需求不够自己在后面添加新接口。
// 刷新默认缓存的查询语句
User getUser(Integer userId);
}
当时只用在了Mapper这一层,都不知道他在service层也做了很多封装。
基于Session并且没有使用Filter的校验
这里就不吐槽现在用Session来做登录验证的弊端了,当时做这个项目,还没有了解过Token。
头铁的用Session,踩了许多坑。可见信息的重要性。主要是这里验证得太死了,每一次验证。
总是要写上几句代码。
// 获取登录的user
User user = (User) request.getSession().getAttribute("user");
if (user != null) { // 登录过了 ,可以操作
....
} else {
....
}
大重构
这里切合一下文章标题、从零搭建起一套自己喜欢的架子。就不拿之前那套Wblog项目来举例了。
一、聚合工程
先看一张图!这是一个新项目的一套架子。没有按功能模块划分。

更方便管理项目
- 父工程只需要说明依赖版本、也可以不声明。
- 子工程间可以相互依赖
- 子工程会继承父工程的pom.xml文件
项目耦合更小了
二、返回结果
- 最基础的 JsonVo【状态码、消息描述】
@Data
@Api("返回结果")
public class JsonVo {
private static final int CODE_OK = CodeMsg.OPERATE_OK.getCode();
private static final int CODE_ERROR = CodeMsg.BAD_REQUEST.getCode();
@ApiModelProperty("代码【0代表成功,其他代表失败】")
private Integer code;
@ApiModelProperty("消息描述")
private String msg;
}
- 有数据的 DataJsonVo【Data + code + msg】
@EqualsAndHashCode(callSuper = true)
@Data
public class DataJsonVo<T> extends JsonVo {
// 返回的数据
private T data;
}
- 分页用的(1) PageJsonVo 【Data + code + msg + count】
@EqualsAndHashCode(callSuper = true)
@Data
public class PageJsonVo<T> extends DataJsonVo<List<T>> {
@ApiModelProperty("总数")
private Long count;
}
- 分页用的(2)较为详细的 PageVo【Data + count + pages】
@ApiModel("分页结果")
@Data
public class PageVo<T> {
@ApiModelProperty("总数")
private Long count;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("数据")
private List<T> data;
}
- 上面四个就是基本的返回结果了。没有贴构造方法。下面是写了一个统一格式的Json工具类
public class JsonVos {
// 消息 + 错误码【400】
public static JsonVo error(String msg) {
return new JsonVo(false, msg);
}
// 枚举中:【消息 + 错误码】
public static JsonVo error(CodeMsg codeMsg) {
return new JsonVo(codeMsg);
}
// 消息 + 错误码
public static JsonVo error(int code, String msg) {
return new JsonVo(code, msg);
}
// 错误码【400】
public static JsonVo error() {
return new JsonVo(false);
}
// 成功码【0】
public static JsonVo ok() {
return new JsonVo();
}
// 枚举中:【消息 + 成功码】
public static JsonVo ok(CodeMsg codeMsg) {
return new JsonVo(codeMsg);
}
// 消息 + 成功码【0】
public static JsonVo ok(String msg) {
return new JsonVo(true, msg);
}
// 数据 + 消息 + 成功码【0】
public static <T> DataJsonVo<T> ok(T data) {
return new DataJsonVo<>(data);
}
// 数据 + 消息 + 成功码【0】
public static <T> DataJsonVo<T> ok(T data, String msg) {
return new DataJsonVo<>(msg, data);
}
// 详细的分页:【数据 + 总数 + 成功码:0】
public static <T> PageJsonVo<T> ok(PageVo<T> pageVo) {
PageJsonVo<T> pageJsonVo = new PageJsonVo<>();
pageJsonVo.setCount(pageVo.getCount());
pageJsonVo.setData(pageVo.getData());
return pageJsonVo;
}
// 抛异常【异常信息】
public static <T> T raise(String msg) throws CommonException {
throw new CommonException(msg);
}
// 抛异常【枚举中:码 + 信息】
public static <T> T raise(CodeMsg codeMsg) throws CommonException {
throw new CommonException(codeMsg);
}
}
三、异常处理
- 自定义异常类【用于自己手动抛出异常信息】
@EqualsAndHashCode(callSuper = true)
@Data
public class CommonException extends RuntimeException {
private int code;
public CommonException() {
this(CodeMsg.BAD_REQUEST.getCode(), null);
}
public CommonException(String msg) {
this(msg, null);
}
public CommonException(int code, String msg) {
this(code, msg, null);
}
public CommonException(String msg, Throwable cause) {
this(CodeMsg.BAD_REQUEST.getCode(), msg, cause);
}
public CommonException(int code, String msg, Throwable cause) {
super(msg, cause);
this.code = code;
}
public CommonException(CodeMsg codeMsg) {
this(codeMsg, null);
}
public CommonException(CodeMsg codeMsg, Throwable cause) {
this(codeMsg.getCode(), codeMsg.getMsg(), cause);
}
public int getCode() {
return code;
}
}
- 异常拦截处理类【会拦截controller过来所有的异常】
@RestControllerAdvice
@Slf4j
public class CommonExceptionHandler {
// 拦截所有异常,判断是什么异常
@ExceptionHandler(Throwable.class)
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public JsonVo handle(Throwable t) {
// 先打印日志
log.error("handle", t);
// 一些可以直接处理的异常
if (t instanceof CommonException) {
return handle((CommonException) t);
} else if (t instanceof BindException) {
return handle((BindException) t);
} else if (t instanceof ConstraintViolationException) {
return handle((ConstraintViolationException) t);
} else if (t instanceof MethodArgumentNotValidException) {
return handle((MethodArgumentNotValidException) t);
} else if (t instanceof ServletException) {
// 可以继续拓展、异常
}
// 处理cause异常(导致产生t的异常)
Throwable cause = t.getCause();
if (cause != null) {
return handle(cause);
}
// 其他异常(没有cause的异常)
return JsonVos.error();
}
// 处理一般性异常CommonException:返回前台的是错误码和异常信息
private JsonVo handle(CommonException ce) {
return JsonVos.error(ce.getCode(), ce.getMessage());
}
// 处理后端验证框架的异常BindException和ConstraintViolationException:返回前台的是错误码和异常信息
private JsonVo handle(BindException be) {
List<ObjectError> errors = be.getBindingResult().getAllErrors();
/*
1、函数式编程的方式:stream -> 【将后一类型的集合,转换成前一类型的集合】
2、lambda表达式的简化方法引用 ObjectError::getDefaultMessage
3、返回的是将错误信息拼接装在一个数组里
4、StringUtils:spring boot的工具类,将数组里的元素用逗号拼接成字符串
*/
List<String> defaultMsgs = Streams.map(errors, ObjectError::getDefaultMessage);
String msg = StringUtils.collectionToDelimitedString(defaultMsgs, ", ");
return JsonVos.error(msg);
}
private JsonVo handle(ConstraintViolationException cve) {
List<String> msgs = Streams.map(cve.getConstraintViolations(), ConstraintViolation::getMessage);
String msg = StringUtils.collectionToDelimitedString(msgs, ", ");
return JsonVos.error(msg);
}
private JsonVo handle(MethodArgumentNotValidException mae) {
List<String> msgs = Streams.map(mae.getBindingResult().getAllErrors(), ObjectError::getDefaultMessage);
String msg = StringUtils.collectionToDelimitedString(msgs, ", ");
return JsonVos.error(msg);
}
}
主要思路是:
- 检查异常类型
①:有对应处理方法,按指定方法执行【给出提示】
②:没有就检查一下导致异常的原因【将异常原因传入本方法,再次检查】
③:若产生异常原因都调用完,还没有找到对应处理方案,那么直接返回一个错误码400
四、后端校验
- 为什么要加入后端校验?
- 写代码的时候,可以和前端沟通好,可以让前端对数据进行合法性校验。看似没问题,
可若是别人知道你的请求接口,知道你的参数。可以通过一些手段,一样的可以请求你的接口。
若后端没做好校验的话,系统就会抛出异常,更有甚者直接会使系统崩掉。
- 所以说,验证可以说是一个必须的步骤。
Validating data is a common task that occurs throughout all
application layers, from the presentation to the persistence
layer. Often the same validation logic is implemented in each
layer which is time consuming and error-prone. To avoid duplication
of these validations, developers often bundle validation
logic directly into the domain model, cluttering domain
classes with validation code which is really metadata about
the class itself.

- 从上面可以看出。验证是一个在各个层次间都需要做的,可能会将很多代码与业务混合
在一起。代码非常难看。当然我这里说的验证仅仅是验证数据的合法性。

- 从这张图可以看出使用 Jakarta Bean Validation 3.0,仅仅需要对领域模型Domin
进行bundle,即可在每一层次间进行验证。
- 如何验证?
Hibernate Validator is the reference implementation of Jakarta
Bean Validation. The implementation itself as well as the Jakarta
Bean Validation API and TCK are all provided and distributed under
the Apache Software License 2.0.Hibernate Validator 7 and Jakarta
Bean Validation 3.0 require Java 8 or later.
- 使用 Hibernate Validator做后端验证。这里可以看出,这是基于Jakarta
Bean Validation实现的。直接使用Hibernate Validator一点问题都没有。
引入库之后。直接在属性上使用对应注解。例如。
@Data
public class UserReqVo {
// 【必须是邮箱合法邮箱类型】
@Email
private String email;
// 【不能为空】
@NotBlank
private String password;
// 【只能在 1~5】
@Max(value = 5)
@Min(value = 1)
private Integer level;
}

- 使用以上注解,会自动去验证,若传输的数据不符合条件,会抛出对应的异常信息。
这里在配合咱们自定义的异常处理,就可以给出数据不合法的提示信息。值得一提的是,
默认情况下,验证器会将正使用的数据且加上验证条件的。全部检查一遍,抛出所有不合法的
异常信息。我们肯定是希望,出现异常就直接提示用户对吧。所以,咱们来配置一下,如何快速
失败
- 快速失败
// 若出现不合法数据,直接抛出异常
@Configuration
public class ValidatorCfg {
@Bean
public Validator validator() {
return Validation
.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory().getValidator();
}
}
五、接口文档
- 文档说明
作为前后端分离开发交接的主要手段,通常会手动自己编写文档,重要的因素。
痛点就是,需要手动编写,并且无法直接测试。下面我会用 Swagger 来实现接口文档编写。
- Swagger实现【配置】
@Configuration
public class SwaggerCfg implements InitializingBean {
@Autowired
private Environment environment;
private boolean enable;
@Bean
public Docket sysDocket() {
return groupDocket(
"01_用户", // 分组模块
"/user.*", // 正则表达式,想要的模块。
"用户模块文档", // 模块标题
"登录,注册,修改信息"); // 描述信息
}
@Bean
public Docket skillDocket() {
return groupDocket(
"02_技巧模块",
"/skill.*",
"掌握技巧模块",
"查询,修改,xx,xxx");
}
private Docket groupDocket(String group, // 分组
String regex, // 哪些会被生成
String title, // 模块标题
String description) { // 描述信息
return basicDocket()
.groupName(group)
.apiInfo(apiInfo(title, description))
.select()
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.regex(regex))
.build();
}
private Docket basicDocket() {
// 配置全局swagger的token
RequestParameter token = new RequestParameterBuilder()
.name(TokenFilter.HEADER_TOKEN)
.description("用户登录令牌")
.in(ParameterType.HEADER)
.build();
return new Docket(DocumentationType.SWAGGER_2)
.globalRequestParameters(List.of(token))
.ignoredParameterTypes(
HttpSession.class,
HttpServletRequest.class,
HttpServletResponse.class)
.enable(enable);
}
private ApiInfo apiInfo(String title, String description) {
return new ApiInfoBuilder()
.title(title)
.description(description)
.version("1.0.0")
.build();
}
@Override
public void afterPropertiesSet() throws Exception {
enable = environment.acceptsProfiles(Profiles.of("dev", "test"));
}
做好配置之后,启动项目,就可以通过 协议 + IP + 端口 + swagger-ui/index.html访问文档。
【这是swagger3的地址】每一个Docket就是一个模块。
- 具体使用
@Data
public class UserReqVo {
@ApiModelProperty("用户id【大于0是编辑,否则是保存】")
private Integer id;
@ApiModelProperty(value = "用户邮箱【必须包含@】", required = true)
private String email;
@ApiModelProperty(value = "密码", required = true)
private String password;
@ApiModelProperty("用户用户昵称")
private String nickname;
}



六、Mybatis Plus 小加强
/**
* 这是用 LambdaQueryWrapper eg:wrapper.like(skill::getName, keyword).like(skill::getIntro, keyword)
* 加强 mybatis plus的模糊查询。
* 泛型为【查询结果的类】
* 返回this是为了支持链式编程
* @param <T>
*/
public class MpLambdaQueryWrapper<T> extends LambdaQueryWrapper<T> {
@SafeVarargs
public final MpLambdaQueryWrapper<T> like(Object val, SFunction<T, ?>... funcs) {
if (val == null) return this;
String str = val.toString();
if (str.length() == 0) return this;
// 遍历将每一个条件加上。
return (MpLambdaQueryWrapper<T>) nested((w) -> {
for (SFunction<T, ?> func : funcs) {
w.like(func, str).or();
}
});
}
众所周知,Mybatis plus的单表特别强,但是我们还可以自己做点加强,例如做查询时,可能会
拼接上很多wapper.like(…).like(…),看起来非常不舒服,于是可以自己加强一下。
之后在调用,就可直接调用自己的,mywapper.like(值, 字段名1, 字段名2 …)
同理也可以将QueryWrapper进行加强。
七、PoJo + MapStruct
- PoJo【简单的Java对象】
使用阿里推荐的数据传输方式。只是一种规范,并不做强制要求,而且项目小的时候。
有这么多层Entity,反而复杂。这一般适用于项目较大的情况下。像我这个项目,没有那么大,
就只使用了 Query、VO、PO
先看一下总的图解吧!

- DAO:数据层
- PO:一张数据库表对应一个PO对象
- BO:有些业务可能需要几个表合起来去处理
- DTO:处理完业务之后对PO或者BO的字段进行加工
- VO:通常情况一个页面对应一个VO对象
- Query:接收前端传递的参数
- MapStruct
上面看到了这么多层对象,肯定涉及到对象间的转换。况且,也必须转。因为使用Mybatis
映射的数据是Po,接收保存的数据,也必须是泛型的Po。那这么多字段的数据,怎么转换呢?
肝老牛活?无限set(get)?
显然这种卖力又不需要思考的活,最好交给别人来做【MapStruct】,咱们需要做的,就是
声明一下如何转换,你是想将【A -> B】?还是【B -> A】。顶多在配一个转换的格式说明!!
/*
1、 ReqVo -> Po
2、 Po -> Vo
3、 uses = 使用的转换器
*/
@Mapper(uses = {
MapStructFormatter.class
})
public interface MapStructs {
// 生成实例对象。可以调用下面的方法
MapStructs INSTANCE = Mappers.getMapper(MapStructs.class);
/*
1、Po -> vo 【用来将从数据库查到的数据过滤成 vo返回给前端】
2、可以解决转换类型不匹配、参数名不匹配的问题。
(1)source:源对象
(2)target:目标对象
(3)qualifiedBy:找转换器中的方法
*/
@Mapping(source = "createdTime",
target = "createdTime",
qualifiedBy = MapStructFormatter.Date2Millis.class)
SkillVo po2vo(Skill po);
@Mapping(source = "createdTime",
target = "createdTime",
qualifiedBy = MapStructFormatter.Date2Millis.class)
@Mapping(source = "loginTime",
target = "loginTime",
qualifiedBy = MapStructFormatter.Date2Millis.class)
UserVo po2vo(User po);
LoginVo po2loginVo(User po);
// reqVo -> po 【用来做数据库保存】
Skill reqVo2po(SkillReqVo reqVo);
User reqVo2po(UserReqVo reqVo);
}
写在后面
好了,文章水完了,目的是将上周学的东西,给整合输出一下。也方便自己日后查看。
!可能有些东西我理解得还是很不到位。欢迎大家的指点鸭!!!