Spring Boot 学习记录

记录 Spring Boot 学习过程

Maven 打包注意事项

  • 配置文件变量替换
    • SpringBoot 中使用 Mavenresource-filter 时 配置文件内的变量应该 由 ${project.version} 改为 `@project.version@`

读取配置

  • 在 Bean 上添加注解
    • @Component
    • @ConfigurationProperties(prefix = "sms")
  • 在需要使用的地方添加@Autowired即可
  • 配置部分 application.yml

    1
    2
    3
    4
    5
    6
    7
    8
    sms:
    regionId: 'cn-hangzhou'
    accessKeyId: '****************'
    secret: '****************'
    endpointName: 'cn-hangzhou'
    product: 'Sms'
    domain: 'sms.aliyuncs.com'
    sign: 'XX科技'
  • 代码部分 SmsSetting.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Component
    @ConfigurationProperties(prefix = "sms")
    public class SmsSetting {
    private String regionId;
    private String accessKeyId;
    private String secret;
    private String endpointName;
    private String product;
    private String domain;
    private String sign;
    }

读取自定义文件配置

  • 在 Bean 上添加注解
    • @Component
    • @ConfigurationProperties(prefix = "git.commit", exceptionIfInvalid = false)
    • @PropertySource("classpath:git.properties")
  • @PropertySource 配置的是文件路径
  • 配置文件部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #Generated by Git-Commit-Id-Plugin
    #Fri Sep 15 14:03:50 CST 2017
    git.branch=release
    git.build.host=SDWM--20090101U
    git.build.time=2017-09-15T14\:03\:50+0800
    git.build.user.email=admin@yumc.pw
    git.build.user.name=502647092
    git.build.version=1.5.0
    git.closest.tag.commit.count=
    git.closest.tag.name=
    git.commit.id=45066a3aba4a698f99e236ac2b90843a97045467
    git.commit.id.abbrev=45066a3
    git.commit.id.describe=45066a3-dirty
    git.commit.id.describe-short=45066a3-dirty
    git.commit.message.full=feat\: \u610F\u89C1\u53CD\u9988\u6DFB\u52A0\u9A8C\u8BC1\u7801
    git.commit.message.short=feat\: \u610F\u89C1\u53CD\u9988\u6DFB\u52A0\u9A8C\u8BC1\u7801
    git.commit.time=2017-09-15T09\:47\:01+0800
    git.commit.user.email=admin@yumc.pw
    git.commit.user.name=502647092
    git.dirty=true
    git.remote.origin.url=http\://192.168.0.18\:3000/DesignPlatform/DesignPlatformService
    git.tags=
  • 代码部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    @Data
    @Component
    @ConfigurationProperties(prefix = "git.commit", exceptionIfInvalid = false)
    @PropertySource("classpath:git.properties")
    public class GitInfo {
    private Id id;
    private Message message;
    private User user;

    @Data
    public static class Id {
    String abbrev;
    String describe;
    String describeShort;
    }

    @Data
    public static class Message {
    private String full;
    // 由于 short 是Java关键词 所以这里改为大写
    // Spring 可以识别 标准驼峰 横杠 和 全大写
    private String Short;
    }

    @Data
    public static class User {
    private String email;
    private String name;
    }
    }

参数验证

  • 需要验证的 Entity 字段添加验证注解 @NotEmpty 还有更多的注解 详见 验证注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Demand {
    private Integer id;
    @NotEmpty(message = "需求标题不得为空")
    private String title;
    @NotNull(message = "需求样式不得为空")
    private Integer styleId;
    private String styleName;
    @NotNull(message = "需求保证金不得为空")
    private BigDecimal budget;
    private Integer budgetStatus;
    private Short auditStatus;
    @NotNull(message = "需求分类不得为空")
    private Integer classId;
    }
  • 需要验证的参数添加 @Valid 注解 随后 紧跟 一个 BindingResult 用于处理错误

    必须在验证的参数后添加一个 BindingResult 不然 Spring 会直接返回 400 错误

    1
    2
    3
    4
    5
    public String add(@Valid DemandForm demandForm, BindingResult result) {
    if(result.hasErrors()){
    return "验证错误: " + result.getAllErrors().get(0).getDefaultMessage()
    }
    }

验证注解

参数解析

SpringBoot自带的参数解析

  • 创建一个 BeanKit

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    package com.sixi.ktpd.kits;

    import ...;

    @Component
    public class BeanKit implements ApplicationContextAware {

    private static ApplicationContext context;

    private BeanKit() {}

    public static ApplicationContext getApplicationContext() {
    return context;
    }

    public static <T> T get(String name) {
    return (T) context.getBean(name);
    }

    public static <T> T getOfType(Class<T> clazz) {
    return context.getBean(clazz);
    }

    public static <T> Collection<T> getListOfType(Class<T> clazz) {
    return context.getBeansOfType(clazz).values();
    }

    @Override
    public void setApplicationContext(ApplicationContext context) {
    this.context = context;
    }
    }
  • 在继承 WebMvcConfigurerAdapter 的配置文件内重写这个方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package pw.yumc.demo.config;
    import ...;
    /**
    * Created with IntelliJ IDEA
    *
    * @author 喵♂呜
    * Created on 2017/5/27 13:50.
    */
    @Configuration
    public class Configurer extends WebMvcConfigurerAdapter {
    /**
    * 自动扫描相关组件并且注册
    */
    @Override
    public void addFormatters(FormatterRegistry registry) {
    for (Converter<?, ?> converter : BeanKit.getListOfType(Converter.class)) {
    registry.addConverter(converter);
    }
    for (GenericConverter converter : BeanKit.getListOfType(GenericConverter.class)) {
    registry.addConverter(converter);
    }
    for (Formatter<?> formatter : BeanKit.getListOfType(Formatter.class)) {
    registry.addFormatter(formatter);
    }
    }
    }

自定义参数解析

  • 由于需求中经常用到JSON传递数据 每次都需要手动解析一次

    1
    2
    3
    public Map submit(HttpServletRequest request){
    JSON.parseObject(request.getParameter("json"))
    }
  • Spring 支持自定义参数解析的 首先 创建一个类 实现 org.springframework.web.method.support.HandlerMethodArgumentResolver 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package pw.yumc.demo.resolver.argument;
    import ...;
    /**
    * Created with IntelliJ IDEA
    *
    * @author 喵♂呜
    * Created on 2017/6/8 13:50.
    */
    public class JSONArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
    Class<?> paramType = parameter.getParameterType();
    return JSONObject.class.isAssignableFrom(paramType) || JSONArray.class.isAssignableFrom(paramType);
    }
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory)throws Exception {
    Class<?> paramType = parameter.getParameterType();
    String json = webRequest.getParameter(parameter.getParameterName());
    if (JSONObject.class.isAssignableFrom(paramType)) {
    return JSON.parseObject(json);
    } else if (JSONArray.class.isAssignableFrom(paramType)) {
    return JSON.parseArray(json);
    } else {
    Method method = parameter.getMethod();
    throw new UnsupportedOperationException("Unknown parameter type: " + paramType + " in method: " + method);
    }
    }
    }
  • Spring-Boot 配置类里添加这个解析器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package pw.yumc.demo.config;
    import ...;
    /**
    * Created with IntelliJ IDEA
    *
    * @author 喵♂呜
    * Created on 2017/5/27 13:50.
    */
    @Configuration
    public class Configurer extends WebMvcConfigurerAdapter {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    argumentResolvers.add(new JSONArgumentResolver());
    }
    }
  • 最后在方法上就可以直接使用了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package pw.yumc.demo.config;
    import ...;
    /**
    * Created with IntelliJ IDEA
    *
    * @author 喵♂呜
    * Created on 2017/5/27 13:50.
    */
    @RestController
    @RequestMapping
    public class Controller {
    @RequestMapping("/submit")
    public Map submit(JSONObject json){
    }
    }

转换 Json 到实体类

  • 在SpringBoot中配置消息转换器即可转换Json为实体类
  • 创建一个配置文件继承于 WebMvcConfigurerAdapter (如果有直接覆盖方法即可)

    • WebMvcConfig.java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      package pw.yumc.spring.core.config;

      import ...;

      /**
      * Created with IntelliJ IDEA
      *
      * @author 喵♂呜
      * Created on 2017/12/20 16:01.
      */
      @Configuration
      public class CoreConfig extends WebMvcConfigurerAdapter {
      /**
      * 消息类型转换
      */
      @Override
      public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
      converters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
      //1.需要定义一个convert转换消息的对象;
      FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
      //2:添加fastJson的配置信息;
      FastJsonConfig fastJsonConfig = new FastJsonConfig();
      fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
      ////3处理中文乱码问题
      //List<MediaType> fastMediaTypes = new ArrayList<>();
      //fastMediaTypes.add(MediaType.APPLICATION_JSON);
      //fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
      //fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes);
      //4.在convert中添加配置信息.
      fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
      converters.add(fastJsonHttpMessageConverter);
      }
      }
  • 创建两个简单的实体类

    • Order.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      package pw.yumc.web.open.domain.entity;

      import ...;

      /**
      * Created with IntelliJ IDEA
      *
      * @author 喵♂呜
      * Created on 2017/12/20 14:40.
      */
      @Data
      public class Order {
      private Integer id;
      private Integer userId;
      private BigDecimal amount;
      List<Product> products;
      }
    • Product.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      package pw.yumc.web.open.domain.entity;

      import ...;

      /**
      * Created with IntelliJ IDEA
      *
      * @author 喵♂呜
      * Created on 2017/12/20 14:41.
      */
      @Data
      public class Product {
      private Integer id;
      private String name;
      private BigDecimal price;
      }
  • 创建一个测试控制器

    • TestController.java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      package pw.yumc.web.open.controller;

      import ...;
      /**
      * Created with IntelliJ IDEA
      *
      * @author 喵♂呜
      * Created on 2017/12/20 10:42.
      */
      @RestController
      @RequestMapping("/test")
      public class TestController {
      @PostMapping("/order")
      public Order order(@RequestBody Order order) {
      return order;
      }
      }
  • 使用CURL测试请求

    • 测试命令

      1
      2
      3
      curl -sX POST http://127.0.0.1:8080/test/order \
      -H 'Content-Type: application/json' \
      -d '{ "id":111,"userId":1,"amount":100,"products":[{"id":123,"name":"testproduct","price":23}]}'
    • 测试结果

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      $ curl -sX POST http://172.30.34.3:1221/test/order \
      > -H 'Content-Type: application/json' \
      > -d '{"id":111,"userId":1,"amount":100,"products":[{"id":123,"name":"testproduct","price":23}]}'
      {
      "amount":100,
      "id":111,
      "products":[
      {
      "id":123,
      "name":"testproduct",
      "price":23
      }
      ],
      "userId":1
      }

异常处理

全局处理异常

  • 新建一个类 GlobeExceptionHandler 继承 ResponseEntityExceptionHandler 添加 @ControllerAdvice 注解
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @ControllerAdvice
    public class GlobeExceptionHandler extends ResponseEntityExceptionHandler {
    @ExceptionHandler(Throwable.class)
    @ResponseBody
    public String handleControllerException(Throwable ex) {
    // 异常逻辑处理
    ...
    // 这里的返回值和控制器用法一样
    return "后台发生异常 " + ex.getClass().getName() + ": " + ex.getMessage();
    }
    }

404异常统一处理

  • 默认情况下 SpringBoot 是不会抛出 404 的异常的
  • 首先设置 DispatcherServlet 分发器的 throwExceptionIfNoHandlerFoundtrue 相关代码如下
  • 代码位于: org.springframework.web.servlet.DispatcherServlet#noHandlerFound

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /**
    * No handler found -> set appropriate HTTP response status.
    * @param request current HTTP request
    * @param response current HTTP response
    * @throws Exception if preparing the response failed
    */
    protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (pageNotFoundLogger.isWarnEnabled()) {
    pageNotFoundLogger.warn("No mapping found for HTTP request with URI [" + getRequestUri(request) +
    "] in DispatcherServlet with name '" + getServletName() + "'");
    }
    if (this.throwExceptionIfNoHandlerFound) {
    throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
    new ServletServerHttpRequest(request).getHeaders());
    }
    else {
    response.sendError(HttpServletResponse.SC_NOT_FOUND);
    }
    }
  • 可以看到 设置 throwExceptionIfNoHandlerFoundtrue 之后 Spring 会在找不到处理时抛出异常

  • 新建全局异常拦截类 参见 全局处理异常
  • 重写 handleNoHandlerFoundException 父类方法中的 handleExceptionInternal 方法的第二个参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package pw.yumc.demo.config;

    import ...;
    @ControllerAdvice
    public class GlobeExceptionHandler extends ResponseEntityExceptionHandler {
    省略部分代码...;
    @Override
    protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
    HttpServletRequest req = ((ServletWebRequest) request).getNativeRequest(HttpServletRequest.class);
    Map<String, Object> map = new HashMap<>();
    map.add("status", 404);
    map.add("msg", "路径未找到!");
    map.add("path", req.getRequestURI());
    map.add("method", req.getMethod());
    map.add("time", System.currentTimeMillis());
    return handleExceptionInternal(ex, map, headers, status, request);
    }
    }

常见问题解决

POST接口提示 415 的问题

  • 在开发微信支付的时候 遇到一个问题 微信回调回来的数据 接口提示 415(Unsupported Media Type) 错误
  • 当时的接口是这样写的

    1
    2
    3
    4
    5
    6
    7
    8
    @RequestMapping
    public String index(@RequestBody(required = false) byte[] body) {
    if (body == null) {
    return fail("数据为空!");
    }
    String notify = new String(body, "UTF-8");
    log.info("收到微信回调通知: {}", notify);
    }
  • 几经百度 发现是 方法内的参数会被 Spring 解析 但是微信传过来的又是 XML 而且没有提供 Content-Type 所以不支持

  • 改成直接用流解析就行了
    1
    2
    3
    4
    5
    @RequestMapping
    public String index(HttpServletRequest request) {
    String notify = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
    log.info("收到微信回调通知: {}", notify);
    }