第18课:Spring

第18课:Spring Boot 搭建实际项目开发中的架构

前面的课程中,我主要给大家讲解了 Spring Boot 中常用的一些技术点,这些技术点在实际项目中可能不会全部用得到,毕竟不同的项目使用的技术也不同,但还是希望大家都能够掌握,并能根据实际项目需求进行相应的扩展。

不知大家是否了解单片机,单片机中有个最小的系统,将它搭建好后,便可在此基础上做进一步扩展。本文,我们就来搭建这样一个“Spring Boot 最小系统架构”。在它的基础上,我们今后可根据实际需求再做相应的扩展。

从零开始搭建一个环境,主要考虑这几点:统一封装的数据结构、可调试的接口、JSON 的处理、模板引擎的使用、持久层的集成、拦截器(可选)和全局异常处理。完成了这几块工作,一个 Spring Boot 项目环境基本就差不多了,之后就是根据具体情况来扩展了。

注意:前后端分离的项目,无需使用模板引擎。非前后端分离的项目,则可能要使用。如何使用,可以参考第07课,本文就不再介绍了,代码中会有,可下载查看。

接下来,我就手把手带领大家搭建一个实际项目开发中可用的 Spring Boot 架构。整个项目工程如下图所示,大家可以结合我提供的源码学习。

工程架构

统一的数据封装

要封装的数据类型往往不确定,这时我们要定义统一的 JSON 结构,就需要用到泛型。统一的 JSON 结构包括数据、状态码、提示信息这三个属性即可,而构造方法可以根据实际业务需求再做添加。此外,一般还要有默认的返回结构,及用户指定的返回结构。定义过程,请参照以下代码:

/**
 * 统一返回对象
 * @author shengwu ni
 * @param <T>
 */
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();
    }

    // 省去get和set方法
}

大家可根据项目的实际需要,修改统一结构中的字段信息。

JSON 的处理

JSON 处理工具有很多,比如阿里巴巴的 Fastjson,不过 Fastjson 无法将未知类型 Null 转成空字符串,可扩展性也不是太好,不过使用方便,使用的人还是蛮多的。

这一节,我们主要集成 Spring Boot 自带的 Jackson。在 Jackson 中对 Null 做下配置,就可在项目中使用了。配置过程如下:

/**
 * jacksonConfig
 * @author shengwu ni
 */
@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;
    }
}

这里先不测试,等下面配置好了 Swagger2,我们再一起测试。

Swagger2 在线可调试接口

作为流行的 API 开发工具,Swagger 减小了接口定义的沟通成本,只需一个 Swagger 地址,开发人员即可在线查看 API 接口文档、在线测试接口数据,给开发者提供了很大的便利。

使用 Swagger 之前,我们需要做一些配置工作。配置过程如下:

/**
 * swagger配置
 * @author shengwu ni
 */
@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.itcodai.course18.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    /**
     * 构建api文档的详细信息
     * @return
     */
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                // 设置页面标题
                .title("Spring Boot搭建实际项目中开发的架构")
                // 设置接口描述
                .description("跟武哥一起学Spring Boot第18课")
                // 设置联系方式
                .contact("倪升武," + "微信公众号:程序员私房菜")
                // 设置版本
                .version("1.0")
                // 构建
                .build();
    }
}

配置完成后,我们可以测试一下。编写一个 Controller 及静态接口,测试下上面集成的内容:

@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 = new User(id, "倪升武", "123456");
        return new JsonResult<>(user);
    }
}

启动项目,在浏览器中输入:localhost:8080/swagger-ui.html,可以看到 Swagger 接口文档页面。调用上面编写的接口,即可看到返回的 JSON 数据。

持久层集成

每个项目必须要有持久层,以便与数据库交互。本实例中,我们选择集成 MyBatis。集成 MyBatis,首先要在 application.yml 中进行配置:

# 服务端口号
server:
  port: 8080

# 数据库地址
datasource:
  url: localhost:3306/blog_test

spring:
  datasource: # 数据库配置
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://${datasource.url}?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
    username: root
    password: 123456
    hikari:
      maximum-pool-size: 10 # 最大连接池数
      max-lifetime: 1770000

mybatis:
  # 指定别名设置的包为所有entity
  type-aliases-package: com.itcodai.course18.entity
  configuration:
    map-underscore-to-camel-case: true # 驼峰命名规范
  mapper-locations: # mapper映射文件位置
    - classpath:mapper/*.xml

配置好后,我们来编写 DAO 层。实际工作中,注解使用方便,用得较多,当然也可以选用 XML 方式,甚至两种同时使用。

本实例中,我们采用注解的方式来集成。XML 方式,之前的课程已有讲解,大家可以自行查看。

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();
}

Service 层的代码,文章中就不写了,大家可以在我的源代码中查看。

这一节完成了 Spring Boot 空架构的搭建。最后别忘了在启动类上添加注解扫描: @MapperScan("com.itcodai.course18.dao")

拦截器

拦截器在项目中使用得非常多(但也不是绝对的),比如拦截一些置顶的 URL,进行一些判断与相应的处理等等。除此之外,还需放行常用的静态页面或者 Swagger 页面,不对这些静态资源做拦截。

首先自定义一个拦截器:

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也渲染了对应的视图咯,此时我可以做一些清理的工作了");
    }
}

将自定义的拦截器加入到拦截器配置中:

@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 实现 WebMvcConfigurer 不会导致静态资源被拦截
        registry.addInterceptor(new MyInterceptor())
                // 拦截所有 URL
                .addPathPatterns("/**")
                // 放行 Swagger
                .excludePathPatterns("/swagger-resources/**");
    }
}

上面代码中所配置的 /** 将对所有 URL 进行拦截。代码实现了 WebMvcConfigurer 接口,则会阻止 Spring Boot 对下面目录中存放的静态资源 URL 实施拦截。

classpath:/static   
classpath:/public   
classpath:/resources   
classpath:/META-INF/resources  

Spring Boot 对我们平时访问的 Swagger 也会拦截,这时配置 .excludePathPatterns,将放行存放在 swagger-resources 目录下的所有 Swagger 页面。

在浏览器中输入 Swagger 页面地址,若能正常显示,说明放行成功。同时可以根据后台打印的日志判断代码执行的顺序。

全局异常处理

全局异常处理,每个项目开发中都会用到。具体的异常,程序一般会提供具体的处理方法,对于没有相应处理方法的异常,也会提供一个统一的全局异常处理方法。异常处理之前,最好提供一个异常提示信息枚举类,专门用来保存异常提示信息,形式如下:

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 处理其他所有异常。

@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
     * @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 页面,调用该接口,可以看到这样的用户提示信息:“系统发生异常,请联系管理员!”。实际开发中,可以根据不同的业务需求展示不同的提示信息。

总结

本文手把手带大家快速搭建了一个 Spring Boot 空架构,主要包括对统一封装的数据结构、可调试的接口、JSON 的处理、模板引擎的使用(代码中体现)、持久层的集成、拦截器和全局异常处理这几个部分的讲解。拥有了这几大部分,一个基本的 Spring Boot 项目环境就差不多了,然后再根据具体业务情况做扩展就可以了。

课程源代码下载地址:

戳我下载

上一篇
下一篇
目录