前面的课程中,我主要给大家讲解了 Spring Boot 中常用的一些技术点,这些技术点在实际项目中可能不会全部用得到,因为不同的项目可能使用的技术不同,但是希望大家都能掌握如何使用,并能自己根据实际项目中的需求进行相应的扩展。
结合前面的课程,本节课手把手带领大家搭建一个实际项目开发中可用的 Spring Boot 架构。整个项目工程如下图所示,学习的时候,可以结合我的源码,这样效果会更好。
1. 统一的数据封装
由于封装的 json 数据的类型不确定,所以在定义统一的 json 结构时,我们需要用到泛型。统一的 json 结构中属性包括数据、状态码、提示信息即可,构造方法可以根据实际业务需求做相应的添加即可,一般来说,应该有默认的返回结构,也应该由用户指定的返回结果。如下:
/**
* <h3>springboot-study</h3>
* <p>统一返回对象</p>
* @author : zhang.bw
* @date : 2020-07-17 21:37
**/
public class JsonResult<T> {
private T data;
private String code;
private String msg;
/**
* 若没有数据返回,默认状态码为0,提示信息为:操作成功!
*/
public JsonResult() {
this.code = "0";
this.msg = "操作成功!";
}
/**
* 若没有数据返回,可以人为指定状态码和提示信息
* @param code
* @param msg
*/
public JsonResult(String code, String msg) {
this.code = code;
this.msg = msg;
}
/**
* 有数据返回时,状态码为0,默认提示信息为:操作成功!
* @param data
*/
public JsonResult(T data) {
this.data = data;
this.code = "0";
this.msg = "操作成功!";
}
/**
* 有数据返回,状态码为0,人为指定提示信息
* @param data
* @param msg
*/
public JsonResult(T data, String msg) {
this.data = data;
this.code = "0";
this.msg = msg;
}
/**
* 使用自定义异常作为参数传递状态码和提示信息
* @param msgEnum
*/
public JsonResult(BusinessMsgEnum msgEnum) {
this.code = msgEnum.code();
this.msg = msgEnum.msg();
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
大家可以根据自己项目中所需要的一些东西,合理的修改统一结构中的字段信息。
2. json的处理
Json 处理工具很多,比如阿里巴巴的 fastjson,不过 fastjson 对有些未知类型的 null 无法转成空字符串,这可能是 fastjson 自身的缺陷,可扩展性也不是太好,但是使用起来方便,使用的人也蛮多的。这节课里面我们主要集成 Spring Boot 自带的 jackson。主要是对 jackson 做一下对 null 的配置即可,然后就可以在项目中使用了。
/**
* <h3>springboot-study</h3>
* <p>jacksonConfig</p>
* @author : zhang.bw
* @date : 2020-07-17 21:37
**/
@Configuration
public class JacksonConfig {
@Bean
@Primary
@ConditionalOnMissingBean(ObjectMapper.class)
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
@Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString("");
}
});
return objectMapper;
}
}
3. swagger2在线可调式接口
有了 swagger,开发人员不需要给其他人员提供接口文档,只要告诉他们一个 Swagger 地址,即可展示在线的 API 接口文档,除此之外,调用接口的人员还可以在线测试接口数据,同样地,开发人员在开发接口时,同样也可以利用 Swagger 在线接口文档测试接口数据,这给开发人员提供了便利。使用 swagger 需要对其进行配置:
/**
* <h3>springboot-study</h3>
* <p>swagger配置</p>
* @author : zhang.bw
* @date : 2020-07-17 21:37
**/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
// 指定构建api文档的详细信息的方法:apiInfo()
.apiInfo(apiInfo())
.select()
// 指定要生成api接口的包路径,这里把controller作为包路径,生成controller中的所有接口
.apis(RequestHandlerSelectors.basePackage("com.bowen.controller"))
.paths(PathSelectors.any())
.build();
}
/**
* 构建api文档的详细信息
* @return
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
// 设置页面标题
.title("Spring Boot 搭建实际项目开发框架")
// 设置接口描述
.description("")
// 设置联系方式
.contact(")
// 设置版本
.version("1.0")
// 构建
.build();
}
}
到这里,可以先测试一下,写一个 Controller,弄一个静态的接口测试一下上面集成的内容。
/**
* <h3>springboot-study</h3>
* <p>UserController</p>
* @author : zhang.bw
* @date : 2020-07-17 21:37
**/
@RestController
@Api(value = "用户信息接口")
public class UserController {
@Resource
private UserService userService;
@GetMapping("/getUser/{id}")
@ApiOperation(value = "根据用户唯一标识获取用户信息")
public JsonResult<User> getUserInfo(@PathVariable @ApiParam(value = "用户唯一标识") Long id) {
User user = userService.getUser(id);
return new JsonResult<>(user);
}
@GetMapping("/getUserByName/{name}")
@ApiOperation(value = "根据用户名获取用户信息")
public JsonResult<User> getUserByName(@PathVariable String name) {
User user = userService.getUserByName(name);
return new JsonResult<>(user);
}
@GetMapping("/getAll")
@ApiOperation(value = "获取所有用户信息")
public JsonResult<List<User>> getAll() {
List<User> list = userService.getAll();
return new JsonResult<>(list);
}
@ApiOperation(value = "测试jackson对null的处理")
@GetMapping("/map")
public Map<String, Object> getMap() {
Map<String, Object> map = new HashMap<>(3);
User user = new User(1L, "猿人", null);
map.put("作者信息", user);
map.put("博客地址", "https://blog.csdn.net/zbw125");
map.put("CSDN地址", null);
map.put("微信公众号", "猿码天地");
return map;
}
@ApiOperation(value = "测试全局异常的处理")
@GetMapping("/exception")
public JsonResult testException() {
// 抛出异常,全局异常会捕获,然后做处理
int i = 1 / 0;
return new JsonResult();
}
}
然后启动项目,在浏览器中输入 localhost:8080/swagger-ui.html 即可看到 swagger 接口文档页面,调用一下上面这个接口,即可看到返回的 json 数据。
4. 持久层集成
每个项目中是必须要有持久层的,与数据库交互,这里我们主要来集成 mybatis,集成 mybatis 首先要在 application.yml 中进行配置。
# 服务端口号
server:
port: 8080
context-path: /
spring:
application:
name: springboot-framework
datasource: # 数据库配置
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_framework?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
maximum-pool-size: 10 # 最大连接池数
max-lifetime: 1770000
mybatis:
# 指定别名设置的包为所有entity
type-aliases-package: com.bowen.entity
configuration:
map-underscore-to-camel-case: true # 驼峰命名规范
mapper-locations: # mapper映射文件位置
- classpath:mapper/*.xml
配置好了之后,接下来我们来写一下 dao 层,实际中我们使用注解比较多,因为比较方便,当然也可以使用 xml 的方式,甚至两种同时使用都行,这里我们主要使用注解的方式来集成,关于 xml 的方式,大家可以查看前面课程,实际中根据项目情况来定。
/**
* <h3>springboot-study</h3>
* <p>userMapper</p>
* @author : zhang.bw
* @date : 2020-07-17 21:37
**/
public interface UserMapper {
@Select("select * from user where id = #{id}")
@Results({
@Result(property = "username", column = "user_name"),
@Result(property = "password", column = "password")
})
User getUser(Long id);
@Select("select * from user where id = #{id} and user_name=#{name}")
User getUserByIdAndName(@Param("id") Long id, @Param("name") String username);
@Select("select * from user")
List<User> getAll();
/**
* 使用xml方式
*/
User getUserByName(String username);
}
关于 service 层我就不在文章中写代码了,大家可以结合我的源代码学习,这一节主要带领大家来搭建一个 Spring Boot 空框架。最后别忘了在启动类上添加注解扫描 @MapperScan("com.bowen.dao")
5. 拦截器
拦截器在项目中使用的是非常多的(但不是绝对的),比如拦截一些置顶的 url,做一些判断和处理等等。除此之外,还需要将常用的静态页面或者 swagger 页面放行,不能将这些静态资源给拦截了。首先先自定义一个拦截器。
/**
* <h3>springboot-study</h3>
* <p>自定义拦截器</p>
* @author : zhang.bw
* @date : 2020-07-17 21:37
**/
public class MyInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(MyInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.info("执行方法之前执行(Controller方法调用之前)");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.info("执行完方法之后执行(Controller方法调用之后),但是此时还没进行视图渲染");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.info("整个请求都处理完咯,DispatcherServlet也渲染了对应的视图咯,此时我可以做一些清理的工作了");
}
}
然后将自定义的拦截器加入到拦截器配置中。
/**
* <h3>springboot-study</h3>
* <p>自定义拦截器的配置,实现WebMvcConfigurer不会导致Spring Boot对mvc的自动配置失效</p>
* @author : zhang.bw
* @date : 2020-07-17 21:37
**/
@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 实现WebMvcConfigurer不会导致静态资源被拦截
registry.addInterceptor(new MyInterceptor())
// 拦截所有url
.addPathPatterns("/**")
// 放行swagger
.excludePathPatterns("/swagger-resources/**");
}
}
在 Spring Boot 中,我们通常会在如下目录里存放一些静态资源:
classpath:/static
classpath:/public
classpath:/resources
classpath:/META-INF/resources
上面代码中配置的 /** 是对所有 url 都进行了拦截,但我们实现了 WebMvcConfigurer 接口,不会导致 Spring Boot 对上面这些目录下的静态资源实施拦截。但是我们平时访问的 swagger 会被拦截,所以要将其放行。swagger 页面在 swagger-resources 目录下,放行该目录下所有文件即可。
然后在浏览器中输入一下 swagger 页面,若能正常显示 swagger,说明放行成功。同时可以根据后台打印的日志判断代码执行的顺序。
6. 全局异常处理
全局异常处理是每个项目中必须用到的东西,在具体的异常中,我们可能会做具体的处理,但是对于没有处理的异常,一般会有一个统一的全局异常处理。在异常处理之前,最好维护一个异常提示信息枚举类,专门用来保存异常提示信息的。如下:
/**
* <h3>springboot-study</h3>
* <p>业务异常提示信息枚举类</p>
* @author : zhang.bw
* @date : 2020-07-17 21:37
**/
public enum BusinessMsgEnum {
/** 参数异常 */
PARMETER_EXCEPTION("102", "参数异常!"),
/** 等待超时 */
SERVICE_TIME_OUT("103", "服务调用超时!"),
/** 参数过大 */
PARMETER_BIG_EXCEPTION("102", "输入的图片数量不能超过50张!"),
/** 500:发生异常 */
UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!");
/**
* 消息码
*/
private String code;
/**
* 消息内容
*/
private String msg;
private BusinessMsgEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String code() {
return code;
}
public String msg() {
return msg;
}
}
在全局统一异常处理类中,我们一般会对自定义的业务异常最先处理,然后去处理一些常见的系统异常,最后会来一个一劳永逸(Exception 异常)。
/**
* <h3>springboot-study</h3>
* <p>全局异常处理Handler</p>
* @author : zhang.bw
* @date : 2020-07-17 21:37
**/
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 拦截业务异常,返回业务异常信息
* @param ex
* @return
*/
@ExceptionHandler(BusinessErrorException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleBusinessError(BusinessErrorException ex) {
String code = ex.getCode();
String message = ex.getMessage();
return new JsonResult(code, message);
}
/**
* 空指针异常
* @param ex NullPointerException
* @return
*/
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleTypeMismatchException(NullPointerException ex) {
logger.error("空指针异常,{}", ex.getMessage());
return new JsonResult("500", "空指针异常了");
}
/**
* 缺少请求参数异常
* @param ex HttpMessageNotReadableException
* @return
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public JsonResult handleHttpMessageNotReadableException(
MissingServletRequestParameterException ex) {
logger.error("缺少请求参数,{}", ex.getMessage());
return new JsonResult("400", "缺少必要的请求参数");
}
/**
* 系统异常 预期以外异常
* @param ex
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleUnexpectedServer(Exception ex) {
logger.error("系统异常:", ex);
return new JsonResult(BusinessMsgEnum.UNEXPECTED_EXCEPTION);
}
}
其中,BusinessErrorException 是自定义的业务异常,继承一下 RuntimeException 即可,具体可以看我的源代码,文章中就不贴代码了。 在 UserController 中有个 testException 方法,用来测试全局异常的,打开 swagger 页面,调用一下该接口,可以看出返回用户提示信息:”系统发生异常,请联系管理员!“。当然了,实际情况中,需要根据不同的业务提示不同的信息。
7. 总结
本文主要手把手带领大家快速搭建一个项目中可以使用的 Spring Boot 空框架,主要从统一封装的数据结构、可调式的接口、json的处理、模板引擎的使用(代码中体现)、持久层的集成、拦截器和全局异常处理。一般包括这些东西的话,基本上一个 Spring Boot 项目环境就差不多了,然后就是根据具体情况来扩展了。