zl程序教程

您现在的位置是:首页 >  后端

当前栏目

你有没有掉进去过这些Spring MVC中的“陷阱“(上)

SpringMVC 这些 有没有 陷阱 掉进去
2023-06-13 09:11:16 时间

一、自定义返回HTTP状态码

  当浏览器输入一个URL地址时,浏览器会向服务器发出请求,在浏览器接收和显示响应内容之前,服务器会返回一个包含HTTP状态码的响应头,响应浏览器的请求。动态码是一个标识,标识当前响应的状态成功或者失败或者需要进行进行其他操作。

常见的HTTP状态码有200、302、404、500等

HTTP状态码有以下五种类型,HTTP状态码的第一位表示状态码的类型:

  • 1xx:服务器收到客户端的请求,需要客户端继续执行操作
  • 2xx:请求成功
  • 3xx:重定向,需要进一步的操作完成请求
  • 4xx:客户端出错,请求出错
  • 5xx:服务区错误,请求处理发生错误

而我们在编写基于Spring MVC的程序时并没有定义响应的状态码,这是因为Spring MVC已经在框架中定义好了这些响应码,不需要在编写业务代码时再去定义响应码,当然Spring MVC也支持自定义状态码

需要自定义返回状态码的场景有以下几种

  • 针对不容的错误类型发送特定的错误码
  • 客户端的定制化需求

Spring MVC中自定义返回状态码的方式有以下几种:

  • 使用ResponseEntity表示状态码、头部信息、响应体
  • Controller类或者异常类上使用@ResponseStatus注解标识响应码,当方法抛出该异常时返回设置的响应码
  • 使用@ControllerAdvice或者@RestControllerAdvice标识一个异常处理类,@ExceptionHanlder标识一个异常处理方法,方法中定义异常类的返回码及响应体等内容

新建一个项目spring_mvc_traps,添加maven依赖

<modelVersion>4.0.0</modelVersion>
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.3</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.citi</groupId>
<artifactId>spring-mvc-traps</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-traps</name>
<description>Demo project for Spring Boot</description>
<properties>
    <java.version>1.8</java.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

添加启动应用主程序

@SpringBootApplication
public class TrapsApplication {

    public static void main(String[] args) {
        SpringApplication.run(TrapsApplication.class,args);
    }
}

ResponseEntity实现自定义HTTP状态码

@RestController
@RequestMapping("/tesla")
public class TeslaController {

    @GetMapping("/first")
    public ResponseEntity<CommonResponse<String>> cyber(){
        CommonResponse<String> result = new CommonResponse<>(0,"");
        result.setData("Cyber");
        // 自定义HTTP响应码
        return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST);
    }
}

使用IDEA的插件REST Client发起HTTP请求,在resources目录下新建spring_mvc_traps.http 增加http请求

###
GET http://localhost:8080/tesla/first
Accept: application/json

启动该服务,点击spring_mvc_traps.http文件左边的启动按钮,发起HTTP请求

响应头为设置的400,即BAD_REQUEST的枚举值。

@ResponseStatus注解

先看@ResponseStatus注解源码

@ResponseStatus注解可以标注在类上也可以标注在方法上,有三个属性,value和code都表示HTTP状态,默认时INTERAL_SERVER_ERROR,即500。reason属性表示原因,默认为空

新建common包,增加一个CommonException

@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "请求错误")
public class CommonException extends RuntimeException{
}

在TeslaController中新增方法

@GetMapping("/second")
public CommonResponse<String> model3(){
    throw new CommonException();
}

在spring_mvc_traps.http中增加请求方法

GET http://localhost:8080/tesla/second
Accept: application/json

重新启动SpringTrapsApplication程序,并发送HTTP请求

还可以将@ResponseStatus标注在方法上

@GetMapping("/third")
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "请求地址不存在")
public void response404(){

}

在spring_mvc_traps.http增加请求

GET http://localhost:8080/tesla/third
Accept: application/json

@ControllerAdvice或者@RestControllerAdvice及@ExceptionHanlder注解

新增advice包,增加GlobalExceptionAdvice

@RestControllerAdvice
public class GlobalExceptionAdvice {


    @ExceptionHandler(value = CustException.class)
    public ResponseEntity<CommonResponse> handleCustException(HttpServletRequest request, CustException ex){

        CommonResponse<String> result = new CommonResponse<>(0,"");
        result.setData(ex.getMessage());

        return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST);
    }
}

在TeslaController中增加方法

@RequestMapping("/fourth")
public CommonResponse<String> fourth() throws CustException{
    throw new CustException("Some error");
}

重启启动应用,在spring_mvc_traps.http增加请求

###
GET http://localhost:8080/tesla/fourth
Accept: application/json

点击发送该请求

二、时间序列化和反序列化中的“陷阱”

新增一个entity包,增加UserInfo实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {

    private long id;
    private String name;

    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private Date createTime;
}

定义一个Controller,UserController;增加GET和POST请求

@RestController
public class UserController {

    @GetMapping("/get")
    public Map<String, Long> getDateByGet(@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") Date date){
        Map<String, Long> result = new HashMap<>();

        result.put("timestamp", date.getTime());

        return result;
    }

    @PostMapping("/post")
    public Map<String, String> getDataByPost(@RequestBody UserInfo userInfo){
        Map<String, String> result = new HashMap<>();
        result.put("id", userInfo.getId().toString());
        result.put("name", userInfo.getName());
        result.put("createTime", userInfo.getCreateTime().toString());

        return result;

    }

}

在resource目录下新增一个spring_mvc_traps_date_transfer.http,定义GET和POST请求发起

###
GET http://localhost:8080/get?date=2022-02-01 23:43:00
Accept: application/json

###
POST http://localhost:8080/post
Content-Type: application/json

{
    "id": "1",
    "name": "stark",
    "createTime": "2022-02-01 23:43:00"
}

发送GET请求

发送POST请求

POST请求中的参数是在请求的BODY中,请求的参数的属性并不会触发 @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")定义的格式,所以会出发JSON转义错误,如何解决这类错误?

使用JsonFormat注解

在UserInfo实体类中的createTime属性增加注解

@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

重新启动应用,发送POST请求

使用自定义格式转换器@JsonDeserialize

@Slf4j
public class DateJacksonConverter extends JsonDeserializer<Date> {

    private static final String[] pattern = new String[] {
            "yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd"
    };

    @Override
    public Date deserialize(JsonParser jsonParser, DeserializationContext context)
            throws IOException, JsonProcessingException {

        Date targetDate = null;
        String originDate = jsonParser.getText();

        if (StringUtils.isNotEmpty(originDate)) {

            try {
                long longDate = Long.parseLong(originDate.trim());
                targetDate = new Date(longDate);
            } catch (NumberFormatException pe) {
                try {
                    targetDate = DateUtils.parseDate(
                            originDate, DateJacksonConverter.pattern
                    );
                } catch (ParseException ex) {
                    log.error("parse error: {}", ex.getMessage());
                    throw new IOException("parse error");
                }
            }
        }

        return targetDate;
    }
}

修改UserInfo实体类中createTime属性,将@JsonFormat注解注释,增加@JsonDeserialize(using = DateJacksonConverter.class)注解

修改POST请求传入参数中createTime的格式,再次发起POST请求

POST http://localhost:8080/post
Content-Type: application/json

{
    "id": "1",
    "name": "stark",
    "createTime": "2022/02/01"
}

仍然可以转化成功

时间格式的局部处理即对需要时间转换的属性上增加@JsonDeserialize注解,这种方式代码可维护性比较差

全局处理Date格式转换

增加时间格式处理的全局配置类,增加@Configuration及在方法上标注@Bean注解,将该类交个Spring容器管理。

@Configuration
public class DateConverterConfig {

    @Bean
    public DateJacksonConverter dateJacksonConverter() {
        return new DateJacksonConverter();
    }

    @Bean
    public Jackson2ObjectMapperFactoryBean jackson2ObjectMapperFactoryBean(
            @Autowired DateJacksonConverter dateJacksonConverter) {
        Jackson2ObjectMapperFactoryBean jackson2ObjectMapperFactoryBean =
                new Jackson2ObjectMapperFactoryBean();
        jackson2ObjectMapperFactoryBean.setDeserializers(dateJacksonConverter);

        return jackson2ObjectMapperFactoryBean;
    }
}

在DateJacksonConverter类中重写handleType()方法,指定针对所有Date类型的属性进行反序列化

@Override
public Class<?> handledType() {
    return Date.class;
}

将UserInfo实体类中createTime属性上的@JsonDeserialize注解注释掉,重新启动应用,再次发起POST请求

同样可以实现时间格式的转换。