Spring Boot中控制器的参数传递以及参数验证

Spring MVC中的处理器映射

控制器中使用注解@RequestMapping处理映射的过程:在Spring MVC项目中,项目启动阶段会将注解@RequestMapping所配置的内容保存到处理映射器(HandlerMapping)中,然年等待请求的发送,通过拦截请求信息与HandlerMapping进行匹配,找到对应的处理器(包含控制器的逻辑),并将处理器以及拦截器保存到HandlerExecutionChain对象中,放回给DispatcherServlet,这样DispatcherServlet就可以运行它们了。可以看出HandlerMapping的主要任务就是请求定位到具体的处理器上。

下面为@RequestMapping注解的源码:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
    String name() default "";

    @AliasFor("path")
    String[] value() default {};

    @AliasFor("value")
    String[] path() default {};

    RequestMethod[] method() default {};

    String[] params() default {};

    String[] headers() default {};

    String[] consumes() default {};

    String[] produces() default {};
}

可以看出,在该注解中可以设置如下的属性值:

  • path和value的属性:设置URL映射的路径
  • method:限定响应的HTTP请求类型,如GET,POST,HEAD,OPTIONS,PUT,TRACE等,默认情况下可以接受所有请求
  • params:当存在对应的HTTP参数时才进行响应
  • headers:限定请求头中存在对应的参数时进行响应
  • consumes:限定HTTP请求体提交类型,如application/json,text/html
  • produces:限定返回的内容类型,仅当HTTP请求头中的类型包含Accept类型才返回

其中value属性是必须的配置项,而为了简化method的配置,在Spring4.3之后新增了如下注解分别对应不同的请求类型:

  • GetMapping
  • PostMapping
  • PatchMapping
  • PutMapping
  • DeleteMapping

其中PatchMapping,PutMapping和DeleteMapping常用在REST风格的项目设计中。

获取控制器参数

Spring MVC中处理器是控制器的包装,其在运行过程中会调用控制器中的方法,并在此之前对HTTP的参数和上下文信息进行解析,将它们转化为控制器所需的参数。下面对控制器如何获取请求的参数进行示例说明。

无注解下的参数传递

由于Spring中已经提供了大量的参数转化规则,所以在很多情况下并不需要对参数的获取进行设置。在默认情况下,参数运行为空,唯一要求为参数名称与Controller中方法的参数名保持一致

@Controller
@RequestMapping("/param")
public class ParamController {

    /**
     * 默认参数传递
     */
    @RequestMapping("/no/annotation")
    @ResponseBody
    public Map<String, Object> noAnnotation(Integer intVal, Long longVal, String str) {
        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("inVal", intVal);
        paramsMap.put("longVal", longVal);
        paramsMap.put("str", str);
        return paramsMap;
    }
}

进行如下请求时:

http://localhost:8080/param/no/annotation?intVal=10&longVal=200

返回结果为 

使用@RequestParam获取参数

当前端URL中的参数名与Controller方法中的参数名不一致时,可以使用@RequestParam注解来指定URL参数与方法参数之间的映射关系:

    /**
     * 参数对应关系设置
     */
    @RequestMapping("/annotation")
    @ResponseBody
    public Map<String, Object> requestParam(@RequestParam(value = "int_val", required = false) Integer intVal,
                                            @RequestParam(value = "long_val", required = false) Long longVal,
                                            @RequestParam(value = "str_val", required = false) String str) {
        Map<String, Object> params = new HashMap<>();
        params.put("intVal", intVal);
        params.put("longVal", longVal);
        params.put("str", str);
        return params;
    }

在使用@RequestParam注解时,默认参数值不能为空,否则会出现异常信息。可以使用required属性指明参数是否运行为空即可。使用如下的URL进行请求:

http://localhost:8080/param/annotation?int_val=10&long_val=20&str_val=yitian

返回结果为:

  

传递数组

Spring MVC中允许以数组的方式进行参数的传递。数组类型的参数以逗号(,)分隔:

    /**
     * 数组参数传递
     */
    @RequestMapping("/requestArray")
    @ResponseBody
    public Map<String, Object> requestArray(int[] intArr, Long[] longArr, String[] strArr) {
        Map<String, Object> params = new HashMap<>();
        params.put("intArr", intArr);
        params.put("longArr", longArr);
        params.put("strArr", strArr);
        return params;
    }

请求URL为:

http://localhost:8080/param/requestArray?intArr=10,11,12&longArr=20,30,40&strArr=yitian1,yitian2

 返回结果为:

JSON数据的传递

在使用JSON格式进行数据传递时,简单建立一个用户注册的页面register.jsp:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Register</title>
    <script type="text/javascript">
        $(document).ready(function() {
            $("#submit").click(function () {
                var userName = $("#userName").val();
                var note = $("#note").val();
                var sex = $("#sex").val();
                if (userName == '' || sex == '' || note == '') {
                    alert("表单参数需填满!");
                    return;
                }
                var params = {
                    userName: userName,
                    sex: sex,
                    note: note
                };
                $.post({
                    url: "./createUser",
                    contentType: "application/json",
                    data: JSON.stringify(params),
                    success: function (result) {
                        if (result == null || result.userName == null) {
                            alert("注册失败!");
                            return;
                        }
                        alert("注册成功!");
                    }
                });
            });
        });
    </script>
</head>
<body>
    <form id="insertForm" method="post">
        <table>
            <tr>
                <td>用户名:</td>
                <td><input id="userName" name="userName"></td>
            </tr>
            <tr>
                <td>性别:</td>
                <td><input id="sex" name="sex"></td>
            </tr>
            <tr>
                <td>备注:</td>
                <td><input id="note" name="note"></td>
            </tr>
            <tr>
                <td></td>
                <td align="right" style="height: 32px;"><input id="submit" type="button" value="SUBMIT"></td>
            </tr>
        </table>
    </form>
    <div><a href="http://localhost:8080/web/index">返回首页</a></div>
</body>
</html>

controller类中代码如下:

    /**
     * 注册用户
     */
    @RequestMapping(value = "/createUser", method = RequestMethod.POST)
    @ResponseBody
    public User createUser(@RequestBody User user) {
        System.out.println(user);
        userService.addUser(user);
        return user;
    }

提交表单后,请求后的返回值如下: 

使用URL传递参数

在REST风格的请求中,常将参数在URL中以/param的方式进行传递,这时就需要使用@PathVariable注解从URL中获取相应的参数值:

    /**
     * URL路径参数传递
     */
    @RequestMapping("/{id}")
    @ResponseBody
    public Long get(@PathVariable("id") Long id) {
        return id;
    }

相应的请求URL为:

http://localhost:8080/param/1

返回值即为:

获取格式化的参数

当使用日期或金额作为参数进行传递时,需要对日期和数字类型进行转换以格式化参数值。日期的参数处理注解为@DateTimeFormat,数字的为@NumberFormat。简单的使用下面的页面进行测试:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Formatter Request</title>
</head>
<body>
    <form id="insertForm" method="post" action="./formatter/commit">
        <table>
            <tr>
                <td>日期:(yyyy-MM-dd)</td>
                <td><input id="date" name="date" type="text" value="2020-01-30"></td>
            </tr>
            <tr>
                <td>金额:(#,###,###.##)</td>
                <td><input id="number" name="number" type="text" value="1,234,567.89"></td>
            </tr>
            <tr>
                <td align="right" style="height: 32px;"><input type="submit" value="SUBMIT"></td>
            </tr>
        </table>
    </form>
    <div><a href="http://localhost:8080/web/index">返回首页</a></div>
</body>
</html>

服务端方法为:

    /**
     * 日期和数字格式化传递
     */
    @RequestMapping("/formatter/commit")
    @ResponseBody
    public Map<String, Object> formatCommit(@DateTimeFormat(iso= DateTimeFormat.ISO.DATE) Date date,
                                            @NumberFormat(pattern = "#,###,###.##") Double number) {
        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put("date", date);
        dataMap.put("number", number);
        return dataMap;
    }

其中分别使用注解@DateTimeFormat和@NumberFormat注解约定参数的传入格式 ,提交表单,可以看到格式化的数据。

{date=Sun Feb 16 00:00:00 CST 2020, number=1234567.89}

此外,在Spring Boot中,日期的格式化可以不使用@DateTimeFormat注解,直接在application.properties配置文件中指明时间的格式:

# 日期格式
spring.mvc.date-format=yyyy-MM-dd

自定义参数转换规则

在使用一些特殊格式进行参数传递时,Spring MVC无法自动进行参数转换,此时需要自定义参数转换规则来对参数进行转换。HTTP的请求中包括请求头Header,请求体Body,URL和参数等内容,服务其中还包括上下文环境和交互会话Session,这里的消息转换指的为Body的转换。

在开始自定义参数转换规则之前,首先大致了解下Spring MVC中处理器的参数转换过程。Spring MVC通过WebDataBinder绑定机制来获取请求参数,其主要作用是解析HTTP请求的上下文,然后在控制器的方法调用之前转换参数并提供验证的功能,为调用控制器方法做准备。处理器会从HTTP请求中获取数据,然后通过三种接口来进行各类参数转换,这三种接口分别是:

  • Converter:普通的转换器,将一中类型转换为另一种类型,例如String->Integer
  • Formatter:格式化转换器,用于类似日期,数字等参数格式的转化
  • GenericConverter:将HTTP参数转化为数组

在Spring MVC中已经使用注册的机制实现了很多转换器,从而可以在传入String时,将其转换为Integer,Long等类型。并且对于上述接口,Spring MVC提供了服务机制(ConversionService接口)去管理,Converter,Formatter和GenericConverter可以通过注册机接口进行注册,这样处理器就可以获取对应的转换器来实现参数的转换,而这些过程是由Spring为我们完成的。因此,对于开发者而言,只需要自定义Converter,Formatter或GenericConverter接口,并将其注入到Spring IoC容器中就可以了。

实现字符串User转换为User对象的自定义参数转换器

下面使用Converter接口,实现将自定义的字符串{id}-{userName}-{note}参数转换为User对象的自定义转换器。首先看一下Converter接口中的内容:

@FunctionalInterface
public interface Converter<S, T> {
    @Nullable
    T convert(S var1);
}

只包含一个convert方法,下面实现上述的自定义转换器:

@Component
public class StringToUserConverter implements Converter<String, User> {

    @Override
    public User convert(String userStr) {
        String userName = null;
        Integer sex = null;
        String note = null;

        String[] strArr = userStr.split("-");
        if (strArr.length == 3) {
            userName = strArr[0];
            if (!strArr[1].equals("")) {
                sex = Integer.parseInt(strArr[1]);
            }
            note = strArr[2];
        } else if (strArr.length == 2) {
            userName = strArr[0];
            if (!strArr[1].equals("")) {
                sex = Integer.parseInt(strArr[1]);
            }
        }
        User user = new User(userName, sex, note);
        return user;
    }
}

上述代码中使用@Component注解将实现的转化器装配到IoC容器中,这样Spring Boot就可以在初始化是将这里的转换器自动加入到转化注册列表中,用于参数的处理。使用下面的URL进行测试:

http://localhost:8080/param/converter?user=username1-2-thisisanote

服务端代码:

    /**
     * 自定义参数格式传递:cn.zyt.springbootlearning.component.StringToUserConverter
     */
    @GetMapping("/converter")
    @ResponseBody
    public User getUserByConverter(User user) {
        return user;
    }

返回结果:

 
使用自定义的集合和数组转换器GenericConverter

借助上述定义的StringToUserConverter转换器,数组转换器GenericConverter可以实现多个用户字符串的转化,服务端代码如下:

    /**
     * 借助自定义参数传递,进行批量自定义参数传递
     * 实现GenericConverter数组转化器
     */
    @GetMapping("/list")
    @ResponseBody
    public List<User> getUserListByConverter(List<User> userList) {
        return userList;
    }

请求路径为:

http://localhost:8080/param/requestArray?intArr=10,11,12&longArr=20,30,40&strArr=yitian1,yitian2

返回结果为:

Spring Boot中的参数验证

处理器在将参数进行转换之后,会对需要验证的参数进行合法性的验证,Spring MVC也提供了参数验证的机制。一方面可以使用JSR-303注解验证,在默认情况下Spring Boot会引入关于Hibernate Validator机制来支持JSR-303参数验证规范。另一方面,当验证逻辑比较复杂时,可以自定义验证规范。

通过注解使用Validator验证机制

首先创建一个用于参数传递的POJO:

@Data
public class ValidatorPojo {
    @NotNull(message = "id不能为空")
    private Long id;

    @NotNull
    @Future(message = "需要一个未来的日期")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date date;

    @NotNull
    @DecimalMin(value = "0.1")
    @DecimalMax(value = "10000.00")
    private Double doubleValue;

    @NotNull
    @Min(value = 1, message = "最小值为1")
    @Max(value = 88, message = "最大值为88")
    private Integer integer;

    @Range(min = 1, max = 888, message = "取值范围为1到888")
    private Long range;

    @Email(message = "邮箱格式错误")
    private String email;

    @Size(min = 20, max = 30, message = "字符串长度要求为[20, 30]")
    private String size;
}

对于对象中的每一个属性的验证信息都已经使用注解进行了标注,并设置了相应的提示信息。当某个字段的参数值没有满足该验证规则时,即会返回相应的提示信息。下面创建一个简单的POST请求来测试参数的验证,带有POST请求的页面为:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Register</title>
    <meta name="_csrf" content="${_csrf.token}"/>
    <meta name="_csrf_header" content="${_csrf.headerName}"/>
    <script src="https://code.jquery.com/jquery-3.2.0.js"></script>
    <script type="text/javascript">
        $(document).ready(function () {
            var token = $("meta[name='_csrf']").attr("content");
            var header = $("meta[name='_csrf_header']").attr("content");

            $(document).ajaxSend(function (e, xhr, options) {
                xhr.setRequestHeader(header, token);
            });

            $("#valid").click(function () {
                var pojo = {
                    id: null,
                    date: '2020-01-30',
                    doubleValue: 100.09,
                    integer: 100,
                    range: 1000,
                    email: 'email',
                    size: "adv2323",
                    regexp: 'a,b,c,d'
                }
                $.post({
                    url: './validate',
                    contentType: 'application/json',
                    data: JSON.stringify(pojo),
                    success: function (result) {

                    }
                });
            });
        });
    </script>
</head>
<body>
    <p>This page for validating params.</p><br>
    <input id="valid" name="valid" type="button" value="Click for Vaild"><br>
    <div><a href="http://localhost:8080/web/index">返回首页</a></div>
</body>
</html>

服务端方法为:

    /**
     * POST请求验证参数,返回验证信息
     */
    @RequestMapping("/valid/validate")
    @ResponseBody
    public Map<String, Object> validate(@Valid @RequestBody ValidatorPojo validatorPojo, Errors errors) {
        Map<String, Object> errMap = new HashMap<>();
        // 获取错误信息列表
        List<ObjectError> oes = errors.getAllErrors();
        for (ObjectError oe : oes) {
            String key = null;
            String msg = null;
            // 字段错误
            if (oe instanceof FieldError) {
                FieldError fe = (FieldError) oe;
                key = fe.getField(); // 获取错误验证字段名
            } else {
                // 非字段错误,获取验证对象名称
                key = oe.getObjectName();
            }
            // 错误信息
            msg = oe.getDefaultMessage();
            errMap.put(key, msg);
        }
        System.out.println(errMap);
        return errMap;
    }

代码中使用@RequestBody代表接收一个JSON参数,这样Controller就可以获取通过Ajax提交的JSON请求体。然后使用@Valid注解表示启动参数验证,这样Spring就会使用JSR-303验证机制对参数进行验证,验证的规则即是上述POJO中使用注解对每个字段进行标注的规则。在验证结束后,错误信息会放到Errors对象中,通过判断并遍历该对象中的错误信息,将错误信息返回。

 点击页面中的验证链接,得到的返回结果为:

 

自定义参数验证机制

除了上述的默认提供的验证注解外,Spring提供了自定义的验证器的实现和注册机制。在Spring MVC中,WebDataBinder不仅注册参数转换器外,还允许注册验证器。

为了自定义验证器,可以实现如下的Spring提供的Validator接口,该接口中的内容如下:

public interface Validator {
    boolean supports(Class<?> var1);
    void validate(Object var1, Errors var2);
}

其中supports方法参数为需要验证的POJO类型,如果该方法返回true,则表示Spring会使用当前的验证器的validate方法对其进行验证。validate方法中包含了待验证的var1对象,和保存错误信息的Errors对象。在该方法中就可以自定义对待验证对象的验证逻辑,并将验证后的错误信息保存到Errors对象中。下面实现一个对User对象进行验证的自定义验证器:

public class UserValidator implements Validator {
    /**
     * 这里的自定义验证器仅针对User对象进行验证
     */
    @Override
    public boolean supports(Class<?> aClass) {
        return aClass.equals(User.class);
    }

    /**
     * 验证逻辑
     */
    @Override
    public void validate(Object o, Errors errors) {
        if (o == null) {
            errors.rejectValue("", null, "用户不能为空");
            return;
        }
        User user = (User) o;
        if (StringUtils.isEmpty(user.getUserName())) {
            errors.rejectValue("userName", null, "userName不能为空");
        }
    }
}

上述的验证器中,supports方法指定了仅对User类型的参数对象进行验证,validate方法中对user对象本身已经其中的userName字段进行了验证。

在上面定义完成了Validator之后,还需要将其注册到WebDataBinder中。Spring MVC提供了一个注解@InitBinder,,它可以在执行控制器方法之前执行该注解下的方法,修改WebDataBinder并绑定自定义的验证器。下面将上述定义的UseValidator验证器进行绑定:

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        // 添加自定义的用户验证器
        binder.addValidators(new UserValidator());
        // 定义日期参数格式,参数不在需要注解@DateTimeFormat, boolean参数表示是否允许为空
        binder.registerCustomEditor(Date.class,
                new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
    }

其中,使用addValidators方法新增了自定义的验证器,此外该方法与setValidator方法的区别在于:

用setValidator()方法设置验证器,原来的JSR-303校验失效仅使用自定义验证器
用addValidators()方法,原来的JSR-303校验依旧有效且自定义校验也有效(推荐)

下面使用两个URL和两个服务端方法来进行测试(下面方法依赖于上述StringToUserConverter参数转换器,具体参考上述代码)。

1. 验证自定义验证器的使用

服务端代码:

    /**
     * 使用自定义用户验证器,无需进行DATE格式的验证
     *
     * @param user User对象使用StringToUserConverter(自定义的转换器)进行自动转换
     * @param errors 验证器返回的错误信息
     * @param date 因为WebDataBinder已经绑定的了Date的格式,因此这里不在需要@DateTimeFormat注解
     * @return 验证错误信息
     */
    @RequestMapping(value = "/valid/user-validator", method = RequestMethod.GET)
    @ResponseBody
    public Map<String, Object> validator(@Valid User user, Errors errors, Date date) {
        Map<String, Object> map = new HashMap<>();
        map.put("user", user);
        map.put("date", date);

        if (errors.hasErrors()) {
            List<ObjectError> oes = errors.getAllErrors();
            for (ObjectError error : oes) {
                if (error instanceof FieldError) {
                    FieldError fe = (FieldError) error;
                    map.put(fe.getField(), fe.getDefaultMessage());
                } else {
                    map.put(error.getObjectName(), error.getDefaultMessage());
                }
            }
        }
        return map;
    }

请求URL为:

http://localhost:8080/param/valid/user-validator?user=-1-note1&date=2020-02-01

返回结果为:

2. 验证加入自定义验证器后,原有的JSR-303验证规则

首先看一下User类中设置的验证规则:

@Data
@Alias(value = "user")
public class User implements Serializable {
    private Long id;

    @NotNull(message = "用户名不能为空")
    private String userName;

    @NotNull(message = "备注不能为空")
    private String note;

    @NotNull(message = "性别不能为空")
    private SexEnum sex;
}

该测试的服务端方法为:

    @RequestMapping("/valid/user")
    @ResponseBody
    public Map<String, Object> validator(@Valid User user, Errors errors) {
        Map<String, Object> resultMap = new HashMap<>();

        if (errors.hasErrors()) {
            List<ObjectError> oes = errors.getAllErrors();
            for (ObjectError error : oes) {
                if (error instanceof FieldError) {
                    FieldError fe = (FieldError) error;
                    resultMap.put(fe.getField(), fe.getDefaultMessage());
                } else {
                    resultMap.put(error.getObjectName(), error.getDefaultMessage());
                }
            }
        }
        return resultMap;
    }

请求URL为:

http://localhost:8080/param/valid/user?user=-1-

可以得到返回值如下: 

可以看到在使用addValidators方法时,自定义验证器和原有的JSR-303验证规则都可以生效。

项目及完整源码地址

GitHub: https://github.com/Yitian-Zhang/springboot-learning

发布了322 篇原创文章 · 获赞 64 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/yitian_z/article/details/104380007