6062 字
17 分钟
SpringMVC常见问题:参数解析、序列化、校验与跨域

@valid和@validated#

@Valid与@Validated注解#

@Valid:常见用在方法,类中字段上进行校验

@Validated:是spring提供的对@Valid的封装,常见用在方法上进行校验

定义的校验类型

@Null       验证对象是否为null @NotNull    验证对象是否不为null, 无法查检长度为0的字符串 @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格. @NotEmpty 检查约束元素是否为NULL或者是EMPTY. @CreditCardNumber信用卡验证 @Email  验证是否是邮件地址,如果为null,不进行验证,算通过验证。 @URL(protocol=,host=, port=,regexp=, flags=) ip地址校验

Booelan检查 @AssertTrue     验证 Boolean 对象是否为 true   @AssertFalse    验证 Boolean 对象是否为 false  

长度检查 @Size(min=, max=)   验证对象(Array,Collection,Map,String)长度是否在给定的范围之内   @Length(min=, max=) Validates that the annotated string is between min and max included.

日期检查 @Past       验证 Date 和 Calendar 对象是否在当前时间之前   @Future     验证 Date 和 Calendar 对象是否在当前时间之后   @Pattern    验证 String 对象是否符合正则表达式的规则

数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为""时无法转换为int,但可以转换为Stirng为"",Integer为null @Min            验证 Number 和 String 对象是否大等于指定的值   @Max            验证 Number 和 String 对象是否小等于指定的值   @DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度 @DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度 @Digits     验证 Number 和 String 的构成是否合法   @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。

注意的几点:

(1)如果一个bean中包含第二个bean,这时要检验第二个bean中某个字段,即嵌套校验,必须要在第一个bean对象中使用@Valid标注到表示第二个bean对象的字段上,然后再第二个bean对象里面的字段上加上校验类型

(2)@Validated支持分组注解

1、先定义一个空接口:GroupA

2、对bean对象中校验类型,添加分组信息,这里我对version字段进行了分组校验信息添加

3、方法入参中进行分组信息添加

如上图所示,则shelveProject方法由于添加了分组信息会校验DeleteProjectRequest对象中的version字段是否为空,而offShelveProject方法没有添加分组信息,不会校验version是否为空。

博客原文:https://blog.csdn.net/zhenwei1994/article/details/81460419

Spring中的@Valid 和 @Validated注解你用对了吗#

1.概述##

本文我们将重点介绍Spring中 @Valid@Validated注解的区别 。

验证用户输入是否正确是我们应用程序中的常见功能。Spring提供了@Valid和@Validated两个注解来实现验证功能,下面我们来详细介绍它们。

2. @Valid和@Validate注解##

在Spring中,我们使用@Valid 注解进行方法级别验证,同时还能用它来标记成员属性以进行验证。

但是,此注释不支持分组验证。@Validated则支持分组验证。

3.例子##

让我们考虑一个使用Spring Boot开发的简单用户注册表单。首先,我们只有名称密码属性:

Copypublic class UserAccount {
@NotNull
@Size(min = 4, max = 15)
private String password;
@NotBlank
private String name;
// standard constructors / setters / getters / toString
}

接下来,让我们看一下控制器。在这里,我们将使用带有@Valid批注的saveBasicInfo方法来验证用户输入:

Copy@RequestMapping(value = "/saveBasicInfo", method = RequestMethod.POST)
public String saveBasicInfo(
@Valid @ModelAttribute("useraccount") UserAccount useraccount,
BindingResult result,
ModelMap model) {
if (result.hasErrors()) {
return "error";
}
return "success";
}

现在让我们测试一下这个方法:

Copy@Test
public void givenSaveBasicInfo_whenCorrectInput`thenSuccess() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfo")
.accept(MediaType.TEXT_HTML)
.param("name", "test123")
.param("password", "pass"))
.andExpect(view().name("success"))
.andExpect(status().isOk())
.andDo(print());
}

在确认测试成功运行之后,现在让我们扩展功能。下一步的逻辑步骤是将其转换为多步骤注册表格,就像大多数向导一样。第一步,名称密码保持不变。在第二步中,我们将获取其他信息,例如agephone。因此,我们将使用以下其他字段更新域对象:

Copypublic class UserAccount {
@NotNull
@Size(min = 4, max = 15)
private String password;
@NotBlank
private String name;
@Min(value = 18, message = "Age should not be less than 18")
private int age;
@NotBlank
private String phone;
// standard constructors / setters / getters / toString
}

但是,这一次,我们将注意到先前的测试失败。这是因为我们没有传递年龄电话字段。

为了支持此行为,我们引入支持分组验证的@Validated批注。

分组验证`,就是将字段分组,分别验证,比如我们将用户信息分为两组:`BasicInfo`和`AdvanceInfo

可以建立两个空接口:

Copypublic interface BasicInfo {
}
Copypublic interface AdvanceInfo {
}

第一步将具有BasicInfo接口,第二步 将具有AdvanceInfo 。此外,我们将更新UserAccount类以使用这些标记接口,如下所示:

Copypublic class UserAccount {
@NotNull(groups = BasicInfo.class)
@Size(min = 4, max = 15, groups = BasicInfo.class)
private String password;
@NotBlank(groups = BasicInfo.class)
private String name;
@Min(value = 18, message = "Age should not be less than 18", groups = AdvanceInfo.class)
private int age;
@NotBlank(groups = AdvanceInfo.class)
private String phone;
// standard constructors / setters / getters / toString
}

另外,我们现在将更新控制器以使用@Validated注释而不是@Valid

Copy@RequestMapping(value = "/saveBasicInfoStep1", method = RequestMethod.POST)
public String saveBasicInfoStep1(
@Validated(BasicInfo.class)
@ModelAttribute("useraccount") UserAccount useraccount,
BindingResult result, ModelMap model) {
if (result.hasErrors()) {
return "error";
}
return "success";
}

更新后,再次执行测试,现在可以成功运行。现在,我们还要测试这个新方法:

Copy@Test
public void givenSaveBasicInfoStep1`whenCorrectInput`thenSuccess() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfoStep1")
.accept(MediaType.TEXT_HTML)
.param("name", "test123")
.param("password", "pass"))
.andExpect(view().name("success"))
.andExpect(status().isOk())
.andDo(print());
}

也成功运行!

接下来,让我们看看@Valid对于触发嵌套属性验证是必不可少的。

4.使用@Valid批注标记嵌套对象##

@Valid 可以用于嵌套对象。例如,在我们当前的场景中,让我们创建一个 UserAddress 对象:

Copypublic class UserAddress {
@NotBlank
private String countryCode;
// standard constructors / setters / getters / toString
}

为了确保验证此嵌套对象,我们将使用@Valid批注装饰属性:

Copypublic class UserAccount {
//...
@Valid
@NotNull(groups = AdvanceInfo.class)
private UserAddress useraddress;
// standard constructors / setters / getters / toString
}

5. 总结##

@Valid保证了整个对象的验证, 但是它是对整个对象进行验证,当仅需要部分验证的时候就会出现问题。

这时候,可以使用@Validated 进行分组验证。

实战:#

校验分组#

/**
* 新增数据 Group
*
* @author zy
*/
public interface AddGroup {
}
/**
* 更新数据 Group
*
* @author zy
*/
public interface UpdateGroup {
}

校验工具类:#

package com.easypark.common.utils.validator;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;
/**
* hibernate-validator校验工具类
*
* 参考文档:http://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/
*
* @author zy
*/
public class ValidatorUtils {
private static final Validator VALIDATOR;
static {
VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
}
/**
* 校验对象
* @param object 待校验对象
* @param groups 待校验的组
* @throws RuntimeException 校验不通过,则报RRException异常
*/
public static void validateEntity(Object object, Class<?>... groups)
throws RuntimeException {
Set<ConstraintViolation<Object>> constraintViolations = VALIDATOR.validate(object, groups);
if (!constraintViolations.isEmpty()) {
StringBuilder msg = new StringBuilder();
for(ConstraintViolation<Object> constraint: constraintViolations){
msg.append(constraint.getMessage()).append("<br>");
}
throw new RuntimeException(msg.toString());
}
}
}

校验字段:#

public class CommercialAddOrEditRequest {
@ApiModelProperty(value = "商户ID(新增的时候不用传值,修改的时候必传!)")
@NotBlank(message = "id不能为空",groups = UpdateGroup.class)
private String id;
@ApiModelProperty(value = "商户名称")
@NotBlank(message = "商户名称不能为空",groups = {AddGroup.class,UpdateGroup.class})
private String commercialName;
@ApiModelProperty(value = "联系人姓名")
@NotBlank(message = "联系人姓名不能为空",groups = {AddGroup.class,UpdateGroup.class})
private String contactName;
@ApiModelProperty(value = "电话")
@NotBlank(message = "电话不能为空",groups = {AddGroup.class,UpdateGroup.class})
private String phone;
}

使用校验:#

@ApiOperation(value = "商户新增或修改接口")
@PostMapping(value = "/addOrEdit")
public JsonResultVO<Object> addOrEdit(@RequestBody CommercialAddOrEditRequest request) {
if(StringUtils.isNotBlank(request.getId())){
ValidatorUtils.validateEntity(request, UpdateGroup.class);
}else {
ValidatorUtils.validateEntity(request, AddGroup.class);
}
commercialService.saveOrUpdateCommercial(request);
return ResponseHelper.createJsonResult();
}

MVC序列化#

springmvc序列化#

使用SpringBoot为例,SpringBoot默认使用Jackson作为序列化工具。通过修改Jackson配置即可自定义序列化规则。

@Valid#

Spring MVC采用的校验是hibernate-validate

@NotNull 值不能为空
@Null 值必须为空
@Pattern(regex=) 字符串必须匹配正则表达式
@Size(min=,max=) 集合的元素数量必须在min和max之间
@CreditNumber(ignoreNonDigitCharacters=) 字符串必须是信用卡号(按美国的标准验证)
@Email 字符串必须是Email地址
@Length(min=,max=) 检查字符串的长度
@NotBlank 字符串必须有字符
@Range(min=,max=) 数字必须大于min,小于等于max
@SafeHtml 字符串是安全的html
@URL 字符串是合法的URL
@AssertFalse 值必须是false
@AssertTrue 值必须是true
@DecimalMax(value,inclusive=) 值必须小于等于(inclusive=true)/小于(inclusive=false) value属性指定的值,可以注解在字符串类型的属性上
@DecimalMin(value,inclusive=) 值必须大于等于(inclusive=true)/大于(inclusive=false) value属性指定的值,可以注解在字符串类型的
属性上
@Digits(integer=,fraction=) 数字格式检查,integer指定整数部分的最大长度,fraction指定小数部分的最大长度
@Future 值必须是未来的日志
@Past 值必须是过去的日期
@Max(value=) 值必须小于等于value指定的值,不能注视在字符串类型的属性上
@Min(value=) 值必须大于等于value指定的值,不能注视在字符串类型的属性上

Jackson 常用注解#

  • @JsonNaming(SnakeCaseStrategy.class)

    指定Json字段名映射策略为蛇形大小写策略。缺省则直接使用Bean属性名

可用的命名映射策略还有: KebabCaseStrategy: 肉串策略 - 单词小写,使用连字符’-‘连接 SnakeCaseStrategy: 蛇形策略 - 单词小写,使用下划线’_‘连接;即老版本中的LowerCaseWithUnderscoresStrategy LowerCaseStrategy: 小写策略 - 简单的把所有字母全部转为小写,不添加连接符 UpperCamelCaseStrategy: 驼峰策略 - 单词首字母大写其它小写,不添加连接符;即老版本中的PascalCaseStrategy

  • @JsonIgnoreProperties({“id”, “created”, “steps”, “copy”, “stepList”})

​ 类注解,指定序列化时忽略这些属性,可以用于覆盖超类中默认输出的属性

  • @JsonInclude(Include.NON_EMPTY)

    仅在属性不为空时序列化此字段,对于字符串,即null或空字符串

  • @JsonIgnore

    序列化时忽略此字段

  • @JsonProperty(value = “user_name”)

    指定序列化时的字段名,默认使用属性名

  • @JsonFormat(pattern = “yyyy-MM-dd HH:mm”, timezone = “GMT+8”)

    指定Date类字段序列化时的格式

  • @JsonUnwrapped(prefix = “user_”)

    private User user; 把成员对象中的属性提升到其容器类,并添加给定的前缀,比如上例中: User类中有name和age两个属性,不使用此注解则序列化为: … “user”: { “name”: “xxx”, “age”: 22 } … 使用此注解则序列化为: … “user_name”: “xxx”, “user_age”: 22, …

  • @JsonIgnoreType

    类注解,序列化时忽略此类

  • @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,property = “id”)

    作用于类或属性上,被用来在序列化/反序列化时为该对象或字段添加一个对象识别码,通常是用来解决循环嵌套的问题

序列化器:#

  • 实现JsonSerializer
  • 对应注解上加注解
  • 需求是:json 字符串不想直接转给前段,而是json对象返回。
@Data
public class Test01 {
@JsonSerialize(using = JsonStringSerilizer.class)
private String jsond = "[{\"url\":\"https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/eQEj1SYtXmOdTqNWZlETR4CpVJieMJQI.jpg\"},{\"url\":\"https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/d5ClnyPCHTuWhBqglVDtIllF2wViDteC.jpg\"},{\"url\":\"https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/ZGSQX6iAxZ0tZhenH0IIvDlagYloSZLn.jpg\"},{\"url\":\"https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/KFOqZenayBSNHykp546iEa5Fus6PmxMs.jpg\"}]";
private Integer age;
private Date time;
}
/**
* <p>DESC: </p>
* <p>DATE: 2019/6/27</p>
* <p>VERSION:1.0.0</p>
* <p>@AUTHOR: ZhengYong</p>
*/
public class JsonStringSerilizer extends JsonSerializer<String> {
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
Object o = JSONUtil.toList(new JSONArray(s),Object.class );
jsonGenerator.writeObject(o);
}
}

反序列化#

前段数据提交:#

  • from-data表单提交
  • application/json 提交

1、 from-data#

springmvc默认转换类:Convert

可以自定义实现Convert<in,out> 接口,并且注册到spring中,即可生效

package com.leyou.auth.service.config;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* <p>DESC: </p>
* <p>DATE: 2019/6/27</p>
* <p>VERSION:1.0.0</p>
* <p>@AUTHOR: ZhengYong</p>
*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Bean
public Converter<String, Date> stringToDate(){
return new Converter<String, Date>() {
@Override
public Date convert(String s) {
if(StringUtils.isNotBlank(s)){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
System.out.println("进入convert");
Date parse = simpleDateFormat.parse(s);
return parse;
} catch (ParseException e) {
e.printStackTrace();
}
}
return null;
}
};
}
}

2、application/json#

因为传入的时json,springmvc默认参数转换使用的是:facson反序列化器 JsonDeserializer

注意controller中参数 注解:@requestBody

@RequestMapping("hello")
@RestController
public class MvcController {
@PostMapping("/hello")
public ResponseEntity hello(@RequestBody Test01 test){
Test01 test01 = new Test01();
test01.setAge(test.getAge());
test01.setTime(test.getTime());
test01.setJsond("[{\"url\":\"https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/eQEj1SYtXmOdTqNWZlETR4CpVJieMJQI.jpg\"},{\"url\":\"https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/d5ClnyPCHTuWhBqglVDtIllF2wViDteC.jpg\"},{\"url\":\"https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/ZGSQX6iAxZ0tZhenH0IIvDlagYloSZLn.jpg\"},{\"url\":\"https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/KFOqZenayBSNHykp546iEa5Fus6PmxMs.jpg\"}]");
return ResponseEntity.ok(test01);
}
}

第一步,实现并且,重写反序列化方法。

package com.leyou.auth.service.serializer;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* <p>DESC: </p>
* <p>DATE: 2019/6/27</p>
* <p>VERSION:1.0.0</p>
* <p>@AUTHOR: ZhengYong</p>
*/
public class JsonStringDesSerilizer extends JsonDeserializer<Date> {
@Override
public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
String value = jsonParser.getValueAsString();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
return dateFormat.parse(value);
} catch (ParseException e) {
System.out.println("反序列化");
e.printStackTrace();
}
return null;
}
}

第二步,对应属性添加注解:@

@Data
public class Test01 {
private String jsond = "";
private Integer age;
@JsonDeserialize(using = JsonStringDesSerilizer.class)
private Date time;
}

3,springboot 配置文件,配置时间序列化#

spring:
datasource:
url: jdbc:mysql://192.192.192.58/dftc_dev?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: dftc@Mysql
redis:
sentinel:
master: master7000
nodes: 192.192.192.58:26380,192.192.192.58:26381,192.192.192.58:26382
password: 123456
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8

注意: 只能对Date 类型的时间 序列化和反序列化

jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8

4,详细讲解#

全局配置 全局配置 全局配置 Jackson时间格式化

mvc Jackson 返回时间时,默认会返回时间戳或者是 2018-08-30T12:12:12CST格式的时间字符串

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
配置了以上参数 只能对Date 类型的时间格式化 对于 LocalDateTime ,Instant 并没有什么效果
只需要添加以下配置就可以解决
@Configuration
public class Java8TimeConfig {
@Value("${spring.jackson.date-format}")
private String formatValue;
@Bean(name = "format")
DateTimeFormatter format() {
return DateTimeFormatter.ofPattern(formatValue);
}
@Bean
public ObjectMapper serializingObjectMapper(@Qualifier("format") DateTimeFormatter format) {
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(format));
javaTimeModule.addSerializer(Instant.class, new InstantCustomSerializer(format));
javaTimeModule.addSerializer(Date.class, new DateSerializer(false, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")));
ObjectMapper mapper = new ObjectMapper()
.registerModule(new ParameterNamesModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(javaTimeModule);
return mapper;
}
class InstantCustomSerializer extends JsonSerializer<Instant> {
private DateTimeFormatter format;
private InstantCustomSerializer(DateTimeFormatter formatter) {
this.format = formatter;
}
@Override
public void serialize(Instant instant, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (instant == null) {
return;
}
String jsonValue = format.format(instant.atZone(ZoneId.systemDefault()));
jsonGenerator.writeString(jsonValue);
}
}
}
需要引入相关jar包 下面是pom文件
<!--jackson 时间格式化-->
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<!--jackson 时间格式化-->

RequestMapping参数解析#

Http请求中Content-Type讲解以及在Spring MVC注解中produce和consumes配置详解

引言: 在Http请求中,我们每天都在使用Content-type来指定不同格式的请求信息,但是却很少有人去全面了解content-type中允许的值有多少,这里将讲解Content-Type的可用值,以及在spring MVC中如何使用它们来映射请求信息。

1、Content-Type#

MediaType,即是Internet Media Type,互联网媒体类型;也叫做MIME类型,在Http协议消息头中,使用Content-Type来表示具体请求中的媒体类型信息。

类型格式:type/subtype(;parameter)? type
主类型,任意的字符串,如text,如果是*号代表所有;
subtype 子类型,任意的字符串,如html,如果是*号代表所有;
parameter 可选,一些参数,如Accept请求头的q参数, Content-Type的 charset参数。

例如: Content-Type: text/html;charset;  常见的媒体格式类型如下:

    text/html : HTML格式     text/plain :纯文本格式          text/xml :  XML格式     image/gif :gif图片格式        image/jpeg :jpg图片格式     image/png:png图片格式    以application开头的媒体格式类型:

   application/xhtml+xml :XHTML格式    application/xml     : XML数据格式    application/atom+xml  :Atom XML聚合格式       application/json    : JSON数据格式    application/pdf       :pdf格式     application/msword  : Word文档格式    application/octet-stream : 二进制流数据(如常见的文件下载)    application/x-www-form-urlencoded :

中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)    另外一种常见的媒体格式是上传文件之时使用的:

    multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式      以上就是我们在日常的开发中,经常会用到的若干content-type的内容格式。#

2、Spring MVC中关于关于Content-Type类型信息的使用#

    首先我们来看看RequestMapping中的Class定义:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
String[] value() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}

value:  指定请求的实际地址, 比如 /action/info之类。 method:  指定请求的method类型, GET、POST、PUT、DELETE等 consumes: 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html; produces:    指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回 params: 指定request中必须包含某些参数值是,才让该方法处理 headers: 指定request中必须包含某些指定的header值,才能让该方法处理请求

3、使用示例#

  3.1 headers

@RequestMapping(value = "/test", method = RequestMethod.GET, headers="Referer=http://www.ifeng.com/")
public void testHeaders(@PathVariable String ownerId, @PathVariable String petId) {
// implementation omitted
}

  这里的Headers里面可以匹配所有Header里面可以出现的信息,不局限在Referer信息。   示例2

@RequestMapping(value = "/response/ContentType", headers = "Accept=application/json")
public void response2(HttpServletResponse response) throws IOException {
//表示响应的内容区数据的媒体类型为json格式,且编码为utf-8(客户端应该以utf-8解码)
response.setContentType("application/json;charset=utf-8");
//写出响应体内容
String jsonData = "{"username":"zhang", "password":"123"}";
response.getWriter().write(jsonData);
}

服务器根据请求头“Accept=application/json”生产json数据。#

当你有如下Accept头,将遵守如下规则进行应用: ①Accept:text/html,application/xml,application/json       将按照如下顺序进行produces的匹配 ①text/html ②application/xml ③application/json ②Accept:application/xml;q=0.5,application/json;q=0.9,text/html       将按照如下顺序进行produces的匹配 ①text/html ②application/json ③application/xml       参数为媒体类型的质量因子,越大则优先权越高(从0到1) ③Accept:/,text/*,text/html

  将按照如下顺序进行produces的匹配 ①text/html ②text/* ③*/*

即匹配规则为:最明确的优先匹配。

Requests部分 Header 解释 示例 Accept 指定客户端能够接收的内容类型 Accept: text/plain, text/html Accept-Charset 浏览器可以接受的字符编码集。 Accept-Charset: iso-8859-5 Accept-Encoding 指定浏览器可以支持的web服务器返回内容压缩编码类型。 Accept-Encoding: compress, gzip Accept-Language 浏览器可接受的语言 Accept-Language: en,zh Accept-Ranges 可以请求网页实体的一个或者多个子范围字段 Accept-Ranges: bytes Authorization HTTP授权的授权证书 Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== Cache-Control 指定请求和响应遵循的缓存机制 Cache-Control: no-cache Connection 表示是否需要持久连接。(HTTP 1.1默认进行持久连接) Connection: close Cookie HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。 Cookie: $Version=1; Skin=new; Content-Length 请求的内容长度 Content-Length: 348 Content-Type 请求的与实体对应的MIME信息 Content-Type: application/x-www-form-urlencoded Date 请求发送的日期和时间 Date: Tue, 15 Nov 2010 08:12:31 GMT Expect 请求的特定的服务器行为 Expect: 100-continue From 发出请求的用户的Email From: [email protected] Host 指定请求的服务器的域名和端口号 Host: www.zcmhi.com If-Match 只有请求内容与实体相匹配才有效 If-Match: “737060cd8c284d8af7ad3082f209582d” If-Modified-Since 如果请求的部分在指定时间之后被修改则请求成功,未被修改则返回304代码 If-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT If-None-Match 如果内容未改变返回304代码,参数为服务器先前发送的Etag,与服务器回应的Etag比较判断是否改变 If-None-Match: “737060cd8c284d8af7ad3082f209582d” If-Range 如果实体未改变,服务器发送客户端丢失的部分,否则发送整个实体。参数也为Etag If-Range: “737060cd8c284d8af7ad3082f209582d” If-Unmodified-Since 只在实体在指定时间之后未被修改才请求成功 If-Unmodified-Since: Sat, 29 Oct 2010 19:43:31 GMT Max-Forwards 限制信息通过代理和网关传送的时间 Max-Forwards: 10 Pragma 用来包含实现特定的指令 Pragma: no-cache Proxy-Authorization 连接到代理的授权证书 Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== Range 只请求实体的一部分,指定范围 Range: bytes=500-999 Referer 先前网页的地址,当前请求网页紧随其后,即来路 Referer: http://www.zcmhi.com/archives/71.html TE 客户端愿意接受的传输编码,并通知服务器接受接受尾加头信息 TE: trailers,deflate;q=0.5 Upgrade 向服务器指定某种传输协议以便服务器进行转换(如果支持) Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 User-Agent User-Agent的内容包含发出请求的用户信息 User-Agent: Mozilla/5.0 (Linux; X11) Via 通知中间网关或代理服务器地址,通信协议 Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1) Warning 关于消息实体的警告信息 Warn: 199 Miscellaneous warning Responses 部分  Header 解释 示例 Accept-Ranges 表明服务器是否支持指定范围请求及哪种类型的分段请求 Accept-Ranges: bytes Age 从原始服务器到代理缓存形成的估算时间(以秒计,非负) Age: 12 Allow 对某网络资源的有效的请求行为,不允许则返回405 Allow: GET, HEAD Cache-Control 告诉所有的缓存机制是否可以缓存及哪种类型 Cache-Control: no-cache Content-Encoding web服务器支持的返回内容压缩编码类型。 Content-Encoding: gzip Content-Language 响应体的语言 Content-Language: en,zh Content-Length 响应体的长度 Content-Length: 348 Content-Location 请求资源可替代的备用的另一地址 Content-Location: /index.htm Content-MD5 返回资源的MD5校验值 Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ== Content-Range 在整个返回体中本部分的字节位置 Content-Range: bytes 21010-47021/47022 Content-Type 返回内容的MIME类型 Content-Type: text/html; charset=utf-8 Date 原始服务器消息发出的时间 Date: Tue, 15 Nov 2010 08:12:31 GMT ETag 请求变量的实体标签的当前值 ETag: “737060cd8c284d8af7ad3082f209582d” Expires 响应过期的日期和时间 Expires: Thu, 01 Dec 2010 16:00:00 GMT Last-Modified 请求资源的最后修改时间 Last-Modified: Tue, 15 Nov 2010 12:45:26 GMT Location 用来重定向接收方到非请求URL的位置来完成请求或标识新的资源 Location: http://www.zcmhi.com/archives/94.html Pragma 包括实现特定的指令,它可应用到响应链上的任何接收方 Pragma: no-cache Proxy-Authenticate 它指出认证方案和可应用到代理的该URL上的参数 Proxy-Authenticate: Basic refresh 应用于重定向或一个新的资源被创造,在5秒之后重定向(由网景提出,被大部分浏览器支持)

 

Refresh: 5; url= http://www.zcmhi.com/archives/94.html Retry-After 如果实体暂时不可取,通知客户端在指定时间之后再次尝试 Retry-After: 120 Server web服务器软件名称 Server: Apache/1.3.27 (Unix) (Red-Hat/Linux) Set-Cookie 设置Http Cookie Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1 Trailer 指出头域在分块传输编码的尾部存在 Trailer: Max-Forwards Transfer-Encoding 文件传输编码 Transfer-Encoding Vary 告诉下游代理是使用缓存响应还是从原始服务器请求 Vary: * Via 告知代理客户端响应是通过哪里发送的 Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1) Warning 警告实体可能存在的问题 Warning: 199 Miscellaneous warning WWW-Authenticate 表明客户端请求实体应该使用的授权方案 WWW-Authenticate: Basic 3.2 params的示例

@RequestMapping(value = "/test/{userId}", method = RequestMethod.GET, params="myParam=myValue")
public void findUser(@PathVariable String userId) {
// implementation omitted
}

  仅处理请求中包含了名为“myParam”,值为“myValue”的请求,起到了一个过滤的作用。 3.3 consumes/produces

@Controller
@RequestMapping(value = "/users", method = RequestMethod.POST, consumes="application/json", produces="application/json")
@ResponseBody
public List<User> addUser(@RequestBody User userl) {
// implementation omitted
return List<User> users;
}

  方法仅处理request Content-Type为“application/json”类型的请求. produces标识==>处理request请求中Accept头中包含了”application/json”的请求,同时暗示了返回的内容类型为application/json;

4、总结#

  在本文中,首先介绍了Content-Type主要支持的格式内容,然后基于@RequestMapping标注的内容介绍了主要的使用方法,其中,headers, consumes,produces,都是使用Content-Type中使用的各种媒体格式内容,可以基于这个格式内容来进行访问的控制和过滤。#


跨域问题#

一、什么是跨域#

跨域是指跨域名的访问,以下情况都属于跨域:

跨域原因说明 示例
域名不同 www.jd.comwww.taobao.com
域名相同,端口不同 www.jd.com:8080www.jd.com:8081 二级域名不同 item.jd.com 与 miaosha.jd.com

如果域名和端口都相同,但是请求路径不同,不属于跨域,如:

www.jd.com/item

www.jd.com/goods

而我们刚才是从manage.leyou.com去访问api.leyou.com,这属于二级域名不同,跨域了。

二.为什么有跨域问题?#

跨域不一定会有跨域问题。

因为跨域问题是浏览器对于ajax请求的一种安全限制:一个页面发起的ajax请求,只能是于当前页同域名的路径,这能有效的阻止跨站攻击。

因此:跨域问题 是针对ajax的一种限制。

但是这却给我们的开发带来了不变,而且在实际生成环境中,肯定会有很多台服务器之间交互,地址和端口都可能不同,怎么办?

三.解决跨域问题的方案#

目前比较常用的跨域解决方案有3种:

  • Jsonp 最早的解决方案,利用script标签可以跨域的原理实现。 限制:
    • 需要服务的支持
    • 只能发起GET请求
  • nginx反向代理 思路是:利用nginx反向代理把跨域为不跨域,支持各种请求方式 缺点:需要在nginx进行额外配置,语义不清晰
  • CORS 规范化的跨域请求解决方案,安全可靠。 优势:
    • 在服务端进行控制是否允许跨域,可自定义规则
    • 支持各种请求方式 缺点:
    • 会产生额外的请求

我们这里会采用cors的跨域方案。

四.cors解决跨域#

1.什么是cors#

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

  • 浏览器端: 目前,所有浏览器都支持该功能(IE10以下不行)。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。
  • 服务端: CORS通信与AJAX没有任何差别,因此你不需要改变以前的业务逻辑。只不过,浏览器会在请求中携带一些头信息,我们需要以此判断是否运行其跨域,然后在响应头中加入一些信息即可。这一般通过过滤器完成即可。

2.原理有点复杂#

浏览器会将ajax请求分为两类,其处理方案略有差异:简单请求、特殊请求。

简单请求

只要同时满足以下两大条件,就属于简单请求。:

(1) 请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

当浏览器发现发现的ajax请求是简单请求时,会在请求头中携带一个字段:Origin.

Origin中会指出当前请求属于哪个域(协议+域名+端口)。服务会根据这个值决定是否允许其跨域。

如果服务器允许跨域,需要在返回的响应头中携带下面信息:

Access-Control-Allow-Origin: http://manage.leyou.com
Access-Control-Allow-Credentials: true
Content-Type: text/html; charset=utf-8
  • Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*,代表任意
  • Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true

注意:

如果跨域请求要想操作cookie,需要满足3个条件:

  • 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。
  • 浏览器发起ajax需要指定withCredentials 为true
  • 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名

特殊请求

不符合简单请求的条件,会被浏览器判定为特殊请求,,例如请求方式为PUT。

预检请求

特殊请求会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

一个“预检”请求的样板:

OPTIONS /cors HTTP/1.1
Origin: http://manage.leyou.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.leyou.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

与简单请求相比,除了Origin以外,多了两个头:

  • Access-Control-Request-Method:接下来会用到的请求方式,比如PUT
  • Access-Control-Request-Headers:会额外用到的头信息

预检请求的响应

服务的收到预检请求,如果许可跨域,会发出响应:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://manage.leyou.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 1728000
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

除了Access-Control-Allow-Origin和Access-Control-Allow-Credentials以外,这里又额外多出3个头:

  • Access-Control-Allow-Methods:允许访问的方式
  • Access-Control-Allow-Headers:允许携带的头
  • Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了

如果浏览器得到上述响应,则认定为可以跨域,后续就跟简单请求的处理是一样的了。

3.实现非常简单#

虽然原理比较复杂,但是前面说过:

  • 浏览器端都有浏览器自动完成,我们无需操心
  • 服务端可以通过拦截器统一实现,不必每次都去进行跨域判定的编写。

事实上,SpringMVC已经帮我们写好了CORS的跨域过滤器:CorsFilter ,内部已经实现了刚才所讲的判定逻辑,我们直接用就好了。

在ly-api-gateway中编写一个配置类,并且注册CorsFilter:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
//1.添加CORS配置信息
CorsConfiguration config = new CorsConfiguration();
//1) 允许的域,不要写*,否则cookie就无法使用了
config.addAllowedOrigin("http://manage.leyou.com");
//2) 是否发送Cookie信息
config.setAllowCredentials(true);
//3) 允许的请求方式
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
// 4)允许的头信息
config.addAllowedHeader("*");
// 5)设置最大生命周期
config.setMaxAge(3600L);
//2.添加映射路径,我们拦截一切请求
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
//3.返回新的CorsFilter.
return new CorsFilter(configSource);
}
}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

SpringMVC常见问题:参数解析、序列化、校验与跨域
https://onecodemaker.cn/posts/spring生态-03-springmvc/
作者
糖糖IT
发布于
2026-02-10
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录