第3章控制器层的常用类和注解 视频讲解 本章主要介绍Spring MVC框架中位于控制器层的常用类和注解的用法,第4章会介绍如何用Spring标签在视图层创建HTML表单。 本章提供的范例也位于helloapp应用中。在本章范例中,访问控制器类的请求处理方法的URL入口都以helloapp作为根路径。 本书为了节省篇幅,在展示部分Java类的源代码时,没有列出import语句。如果要了解一个由Spring API提供的类或注解到底来自哪个包,可以参考本书提供的配套源代码,或者查阅Spring的JavaDoc文档,下载地址为https://search.maven.org/search?q=g:org.springframework。 3.1用@Controller注解标识控制器类 把一个类用@Controller注解标识,这个类就变成了Spring MVC框架中的控制器类。不过,要让这个控制器类服从Spring MVC框架的统一调遣,还必须确保Spring MVC框架在启动时,会扫描到控制器类中的@Controller注解,从而把这个控制器类收编到自己的管辖范围内,即把它注册到Spring MVC框架中。 在Spring MVC的配置文件中,以下代码用于告诉Spring MVC框架在哪些Java包中扫描Java类的Spring注解。 这段代码告诉Spring MVC框架需要扫描mypack包以及子包中的Java类的@Controller等Spring注解。如果希望Spring MVC框架忽略扫描某些注解,可以用元素来设定。例如,以下代码告诉Spring MVC框架忽略Java类中的@Service注解。 3.2控制器对象的存在范围 一旦Controller类按照3.1节的方式向Spring MVC框架进行了注册,Spring MVC框架就会管理Controller对象的生命周期。 默认情况下,Controller对象的存在范围为singleton(单例),即在整个应用程序的生命周期内,一个Controller类只有一个实例。 singleton范围的优点是节省内存空间,但是也存在以下两个缺点。 (1) 当大量客户请求同时访问一个Controller对象的共享数据时,容易造成并发问题。 (2) 如果一个Controller对象采用了线程同步机制,那么当大量客户请求同时访问这个Controller对象时,会导致部分处理客户请求的线程阻塞,影响Web应用的并发性能。 为了克服以上缺点,Spring MVC框架允许把一个Controller对象的存在范围设置为request或session,具体细节如下。 (1) request范围: 对于每一个HTTP请求,Spring MVC框架创建一个Controller对象。当完成了对这个HTTP请求的响应,Controller对象就结束生命周期。 (2) session范围: 对于每一个HTTP会话,Spring MVC框架创建一个Controller对象。当这个HTTP会话结束,Controller对象就结束生命周期。 在以下代码中,ControllerA和ControllerB分别使用了@RequestScope和@SessionScope注解,它们的范围分别为request和session。 @Controller @RequestScope //ControllerA的存在范围为request public class ControllerA{} @Controller @SessionScope //ControllerB的存在范围为session public class ControllerB{} @RequestScope注解等价于@Scope("request"),@SessionScope注解等价于@Scope("session")。 除了request和session范围,还可以把Controller对象的存在范围设为application,这意味着在整个Web应用的生命周期内,只有一个Controller对象,例如: @Controller @ApplicationScope //等价于@Scope("application") public class ControllerA{} 3.3设置控制器类的请求处理方法的URL入口 Controller类和普通的Java类一样,可以包含任意方法。在这些方法中,能够被客户端直接请求访问的方法称为请求处理方法。本章提到的控制器类的方法,如果未作特别说明,都是指请求处理方法。 Spring MVC框架允许灵活地为请求处理方法指定URL入口,具体细节如下。 (1) 通过@RequestMapping注解的value属性设定URL入口。 (2) 通过@RequestMapping注解的params、method和headers等属性进一步限制URL入口。 3.3.1设置URL入口的普通方式 @RequestMapping注解既可以标识控制器类,也可以标识方法。以下ControllerA的三个方法都使用了@RequestMapping注解。 @Controller public class ControllerA{ //设定请求方式以及多个URL入口 @RequestMapping(value = {"/input","/"}, method = RequestMethod.GET) public String method1(){…} @RequestMapping(value = {"/hello "}) //设定一个URL入口 public String method2(){…} @RequestMapping("go") //直接设定URL入口 public String method3(){…} } method1()方法的URL入口为helloapp/input以及helloapp/,并且请求方式必须为GET。method2()方法的URL入口为helloapp/hello,无须考虑是GET请求方式还是POST等其他请求方式。method3()方法的URL入口为helloapp/go,无须考虑是GET请求方式还是POST等其他请求方式。 以下ControllerB在类和方法前都使用了@RequestMapping注解。 @Controller @RequestMapping("person") //设定相对根路径 public class ControllerB{ @RequestMapping("/save") public String method1(){…} @RequestMapping("/") public String method2(){…} } method1()方法的URL入口为helloapp/person/save。method2()方法的URL入口为helloapp/person/。 由此可见,位于控制器类之前的@RequestMapping注解设定访问该控制器类的所有请求处理方法的相对根路径。 3.3.2限制URL入口的请求参数、请求方式和请求头 在@RequestMapping注解中不仅可以通过value属性设定URL入口,还可以通过params属性、method属性和headers属性来进一步限制URL入口。 1. params属性 params属性用于限制客户端访问请求处理方法的请求参数。例如: @RequestMapping(value = "test", params = { "username=weiqin", "address","!phone" }) public String testParam() {…} 以上代码中@RequestMapping注解通过params属性设置了三个请求参数: username、address和phone。客户端访问testParam()方法的URL必须为helloapp/test。此外,其请求参数还必须满足以下三个条件。 (1) 包含username请求参数,并且取值为weiqin。 (2) 包含address请求参数,取值无所谓。 (3) 不能包含phone请求参数。 以下URL能正常访问testParam()方法。 http://localhost:8080/helloapp/test? username=weiqin&address=shanghai http://localhost:8080/helloapp/test? username=weiqin&address=beijing&gender=female 以下URL不会访问testParam()方法。 //username取值不是weiqin http://localhost:8080/helloapp/test?username=Mary&address=shanghai //包含phone请求参数不符合要求 http://localhost:8080/helloapp/test? username=weiqin&address=beijing&phone=56567878 //没有包含address请求参数 http://localhost:8080/helloapp/test?username=weiqin //没有包含取值为weiqin的username请求参数 http://localhost:8080/helloapp/test?address=shanghai 2. method属性 method属性用于限制客户端访问请求处理方法的请求方式。例如: @RequestMapping(value = "test", method = { RequestMethod.GET,RequestMethod.DELETE }) public String testMethod(){…} 以上代码表明,当URL为helloapp/test并且客户端请求方式为GET或DELETE,Spring MVC框架会调用testMethod()方法。 3. headers属性 headers属性用于限制客户端访问请求处理方法的请求头。例如: @RequestMapping(value = "test", headers = { "Host=localhost","Accept","!Referer" }) public String testHeaders() {…} headers属性的赋值语法与params属性相似。客户端访问testHeaders()方法的URL必须为helloapp/test。此外,其请求头还必须满足以下三个条件。 (1) 包含Host项,并且取值为localhost。 (2) 包含Accept项,取值无所谓。 (3) 不能包含Referer项。 3.3.3@GetMapping和@PostMapping等简化形式的注解 为了简化@RequestMapping注解中请求方式的设置,Spring MVC框架还提供了以下4种简化形式的映射注解。 (1) @GetMapping: 指定请求方式为GET。 (2) @PostMapping: 指定请求方式为POST。 (3) @PutMapping: 指定请求方式为PUT。 (4) @DeleteMapping: 指定请求方式为DELETE。 例如以下三种映射方式是等价的。 //方式一 @RequestMapping(value = "test", method = { RequestMethod.GET }) public String test(){…} //方式二 @GetMapping(value = "test") public String test(){…} //方式三 @GetMapping("test") public String test(){…} 值得注意的是,@GetMapping等注解只能用来标识请求处理方法,而不能用来标识控制器类,这是和@RequestMapping注解的一个区别。 3.4绑定HTTP请求数据和控制器类的方法参数 Controller类负责处理具体的客户请求,需要读取来自客户端的HTTP请求数据。在控制器类中读取请求数据的原生态方式是从HttpServletRequest对象中读取各种请求数据,例如: @RequestMapping(value = "test") public String test(HttpServletRequest request){ //读取请求参数 String userName=request.getParameter("userName"); //读取请求头中的Host项 String host=request.getHeader("Host"); //读取Cookie Cookie[] cookies=request.getCookies(); return "result"; } 此外,Spring MVC框架对HttpServletRequest对象进行了封装,允许在控制器类的请求处理方法中,把特定的请求数据与方法参数绑定。所谓绑定,这里是指由Spring MVC框架自动把请求数据赋值给方法参数。这样,控制器类的方法直接从方法参数中就能获取特定请求数据,省去了从HttpServletRequest对象中读取特定请求数据的操作。 HTTP请求数据主要包括以下三部分内容。 (1) 请求参数。HTML表单数据也属于请求参数。 (2) 请求头。 (3) Cookie。 Spring MVC框架允许采用以下5种方式把请求数据和方法参数绑定。 (1) 直接定义和请求参数同名的方法参数。 (2) 用@RequestParam注解绑定请求参数。 (3) 用@RequestHeader注解绑定请求头。 (4) 用@CookieValue注解绑定Cookie。 (5) 用@PathVariable注解绑定RESTFul风格的URL变量。 3.4.1直接定义和请求参数同名的方法参数 在控制器类的请求处理方法中,把请求参数与方法参数绑定的最直接方式是定义和请求参数同名的方法参数。例如: @RequestMapping("test") public String testParam(String name, int age, String address) { System.out.println("name="+name); System.out.println("age="+age); System.out.println("adress="+address); return "result"; } 以下URL会访问testParam()方法。 http://localhost:8080/helloapp/test?name=Tom&age=22&address=Shanghai 该URL中包含name、age和address三个请求参数。Spring MVC框架调用testParam()方法时,会把这三个请求参数分别赋值给testParam()的name参数、age参数和address参数。testParam()方法会在服务器端打印以下内容。 name=Tom age=22 address=Shanghai 3.4.2用@RequestParam注解绑定请求参数 @RequestParam注解能把特定请求参数和请求处理方法中的方法参数绑定,例如: @RequestMapping("test") public String testParam( @RequestParam(required=false,defaultValue="Guest") String name, @RequestParam(name="age") int age, @RequestParam("address") String homeAddress) { System.out.println("name="+name); System.out.println("age="+age); System.out.println("homeAdress="+homeAddress); return "result"; } 以上testParam()方法通过@RequestParam注解绑定了三个请求参数。 (1) 第一个@RequestParam注解把name请求参数绑定到name方法参数。默认情况下,请求参数与方法参数同名。 required属性的默认值为true,这里required属性取值为false,表示这个不是必须提供的请求参数。defaultValue属性指定name请求参数的默认值。 (2) 第二个@RequestParam注解把age请求参数绑定到age方法参数。 (3) 第三个@RequestParam注解把address请求参数绑定到homeAddress方法参数。 以下URL会访问testParam()方法。 http://localhost:8080/helloapp/test?name=Tom&age=22&address=Shanghai 该URL包含name、age和address三个请求参数,testParam()方法会在服务器端打印以下内容。 name=Tom age=22 homeAddress=Shanghai 当客户端请求访问的URL为http://localhost:8080/helloapp/test?age=22&address=Shanghai。该URL包含age和address两个请求参数,name请求参数取默认值Guest,testParam()方法会在服务器端打印以下内容。 name=Guest age=22 homeAddress=Shanghai 当客户端请求访问的URL为http://localhost:8080/helloapp/test?name=Tom&age=22。由于该URL没有提供address请求参数,因此客户端的浏览器会得到以下错误信息。 Required String parameter 'address' is not present @RequestParam注解以及后文将提到的@CookieValue注解和@RequestHeader注解具有一些共同的属性,参见表31。 表31@RequestParam、@CookieValue和@RequestHeader的共同属性 属性描述 name属性 指定请求数据的名字 value属性 name属性的别名、作用和name属性相同 required属性 默认值为true,指明是否为必须提供的请求数据 deafultValue属性请求数据的默认值 value属性是name属性的别名,因此两者的作用是相同的。例如以下四个@RequestParam注解的作用相同。 @RequestParam(name="age") int age @RequestParam(value="age") int age @RequestParam("age") int age @RequestParam int age 3.4.3用@RequestHeader注解绑定HTTP请求头 在HTTP请求头中会包含客户端的主机地址、浏览器类型、请求正文的数据类型以及请求正文的长度等信息。@RequestHeader注解能够把请求头中的特定项和请求处理方法的参数绑定,例如: @RequestMapping("test") public String testRequestHeader(@RequestHeader("Host") String hostAddr, @RequestHeader String Host, @RequestHeader String host) { System.out.println(hostAddr + "-----" + Host + "-----" + host); return "result"; } 以上testRequestHeader()方法使用了三个@RequestHeader注解: (1) 第一个注解把请求头中名为Host的请求项与方法参数hostAddr绑定。 (2) 第二个注解把请求头中名为Host或host的请求项与方法参数Host绑定。默认情况下,请求头中请求项和方法参数同名,但是不区分大小写。 (3) 第三个注解把请求头中名为Host或者host的请求项与方法参数host绑定。 通过浏览器访问http://localhost:8080/helloapp/test,testRequestHeader()方法会在服务器端打印以下信息。 localhost:8080-----localhost:8080-----localhost:8080 3.4.4用@CookieValue注解绑定Cookie 先通过一个简单的例子来引入Cookie作用的介绍。用户第一次到一个健身房去健身,健身房会为用户办理一张会员卡,这个会员卡由用户保管。以后用户每次去健身房,都会先出示会员卡。Cookie就类似于会员卡。Cookie是服务器端事先发送给客户端的用来跟踪客户端状态的一些数据,采用“Cookie名字=Cookie值”的数据格式。客户端得到服务器端发送的Cookie后,会把它保存在本地机器上,以后客户端再向服务器端发送请求时,会在请求数据中包含Cookie信息。 假定客户端请求访问服务器端的一个控制器类的testCookie()方法时,在请求数据中包含两个Cookie: “username=Tom”和“address=Shanghai”。以下testCookie()方法会读取这两个Cookie。 @RequestMapping("test") public String testCookie(@CookieValue String username, @CookieValue("address")String homeAddress) { System.out.println("username="+username); System.out.println("homeAddress="+homeAddress); return "result"; } 第一个@CookieValue注解把名字为username的Cookie和username方法参数绑定。默认情况下,Cookie的名字和方法参数的名字相同。第二个@CookieValue注解把名字为address的Cookie和homeAddress方法参数绑定。运行testCookie()方法,在服务器端会打印以下内容。 username=Tom homeAddress=Shanghai 如果客户端的请求数据中不包含名字为username或者address的Cookie,那么当客户端试图访问testCookie()方法时,客户端的浏览器会收到以下错误信 息。 Missing cookie 'username' for method parameter of type String 3.4.5用@PathVariable注解绑定RESTFul风格的URL变量 在访问请求处理方法的URL中可以加入一些变量,@PathVariable注解能够把这些URL变量和方法参数绑定。第15章会进一步介绍RESTFul风格的概念和用法。 例程31的testPath()方法就使用了@PathVariable注解。 例程31TestPathController.java @Controller @RequestMapping("/main/{variable1}") public class TestPathController { @RequestMapping("/test/{variable2}") public String testPath (@PathVariable String variable1, @PathVariable("variable2") int variable2) { System.out.println("variable1="+variable1); System.out.println("variable2="+variable2); return "result"; } } 第一个@RequestMapping注解设置的URL中包含一个variab1e1变量。第二个@RequestMapping注解设置的URL中包含一个variable2变量。 在testPath()方法中,第一个@PathVariable注解把URL中的variable1变量和variable1方法参数绑定,默认情况下,URL变量的名字和方法参数的名字相同。第二个@PathVariable注解把URL中的variable2变量和variable2方法参数绑定。 当浏览器端请求访问的URL为http://localhost:8080/helloapp/main/hello/test/100,URL中variable1变量的取值为hello, variable2变量的取值为100,因此testPath()方法在服务器端打印以下内容。 variable1=hello variable2=100 3.4.6把一组请求参数和一个JavaBean类型的方法参数绑定 Spring MVC框架还会自动把一组请求参数(包括表单数据)转换成一个JavaBean,再把这个JavaBean和方法参数绑定。例如Product类是一个JavaBean,它有以下两个属性以及相应的get和set方法。 private String name; private double price; 以下ProductController类的getDetail()方法把name请求参数和price请求参数转换成一个Product对象,再和product参数绑定。 @Controller public class ProductController { @RequestMapping("/product") public String getDetail(Product product) { System.out.println("name:"+product.getName()); System.out.println("price:"+product.getPrice()); return "result"; } } 通过浏览器访问getDetail()方法,URL为http://localhost:8080/helloapp/product?name=book&price=25,getDetail()方法会在服务器端打印如下内容: name:book price:25 3.5请求参数的类型转换 在Controller类中,如果通过HttpServletRequest的getParameter()方法来读取请求参数,返回的是String类型的请求参数值。如果要获得其他类型的参数,需要在程序中进行类型转换,例如: String param=request.getParameter("age"); int age=Integer.parseInt(param); //把字符串类型转换为int类型 而通过Spring MVC框架把请求参数绑定到控制器类的方法参数时,Spring MVC框架会利用内置的数据类型转换器,对一些常见的数据类型自动进行类型转换。例如在以下testParam()方法中,会把各种类型的请求参数与方法参数绑定。 @RequestMapping("test") public String testParam(String name, int age, boolean isMarried,double weight) { System.out.println("name="+name); System.out.println("age="+age); System.out.println("isMarried="+isMarried); System.out.println("weight="+weight); return "result"; } 当客户端请求访问的URL为http://localhost:8080/helloapp/test?name=Tom&age=22&isMarried=false&weight=53.5,该URL中包含name、age、isMarried和weight四个请求参数,testParam()方法会在服务器端打印以下内容。 name=Tom age=22 isMarried=false weight=53.5 由此可见,Spring MVC框架会根据请求处理方法的参数类型,自动把String类型转换为int类型、boolean类型或者double类型等。 对于复杂的数据类型,还可以自定义类型转换器。例如,假定客户端提供的请求参数为表示用户信息的字符串“Tom,22,false,53.5”,如果希望Spring MVC框架能把它先转换成一个Person对象,再传给控制器类的方法,该如何实现呢?接下来就结合具体的范例介绍创建和使用自定义类型转换器的5个步骤。 (1) 创建hello.jsp,它接收用户输入的Person信息,最后会把Person信息显示到网页上。 (2) 创建包含Person信息的Person类。 (3) 创建类型转换器PersonConverter类,它把String类型的Person信息转换成Person对象。 (4) 在Spring MVC配置文件中注册PersonConverter类型转换器。 (5) 创建控制器类PersonController,它读取经过数据类型转换的person参数,把它保存在Model中,再由hello.jsp显示Person信息。 3.5.1创建包含表单的hello.jsp 在hello.jsp中定义了一个表单,它包含一个名字为personInfo的文本框。此外,它还通过EL表达式显示用户输入的表单数据。例程32是hello.jsp的主要源代码。 例程32hello.jsp的主要源代码 ${person.userName}
${person.age}
${person.isMarried}
${person.weight}


3.5.2创建包含Person信息的Person类 在Person类中定义了userName、age、isMarried和weight属性,以及相应的get和set方法。例程33是Person类的源代码。 例程33Person.java public class Person { private String userName = null; private int age; private boolean isMarried; private double weight; public String getUserName() { return this.userName; } public void setUserName(String userName) { this.userName = userName; } … public String getPersonInfo( ) { return(userName+","+age+","+isMarried+","+weight); } } Person类有一个getPersonInfo()方法,返回包含Person信息的字符串。但是,Person类中并没有定义personInfo属性。在hello.jsp中,仍然可以通过${person.personInfo}的形式访问Person信息,例如: 由此可见,EL表达式${person.personInfo}会自动调用Person对象的getPersonInfo()方法。 3.5.3创建类型转换器PersonConverter类 PersonConverter类实现了org.springframework.core.convert.converter.Converter接口,它把String类型的Person信息转换成Person对象。例程34是PersonConverter类的源代码。 例程34PersonConverter.java package mypack; import org.springframework.core.convert.converter.Converter; public class PersonConverter implements Converter { public Person convert(String source) { // 创建一个Person对象 Person person=new Person(); // 以","分隔 String stringValues[] = source.split(","); if (stringValues != null && stringValues.length == 4) { // 为Person实例赋值 person.setUserName(stringValues[0]); person.setAge(Integer.parseInt(stringValues[1])); person.setIsMarried(Boolean.parseBoolean(stringValues[2])); person.setWeight(Double.parseDouble(stringValues[3])); return person; } else { throw new IllegalArgumentException( String.format( "类型转换失败," +"需要格式'userName,age,isMarried,weight ',但格式是[% s ]" , source)); } } } PersonConverter类的convert(String source)方法会解析String类型的source参数,把它转换成一个Person对象。 3.5.4在Spring MVC配置文件中注册类型转换器 只有在Spring MVC的配置文件中注册了PersonConverter类型转换器,Spring MVC框架才会在需要的场合,自动把String类型的Person信息转换为Person对象。以下是在Spring MVC配置文件中的配置代码。 元素定义了一个id为conversionService的Bean组件,它是创建PersonConverter类型转换器的工厂Bean。元素的conversionservice属性的取值为conversionService。因此,Spring MVC框架会利用PersonConverter类型转换器进行数据类型转换。 3.5.5创建处理请求参数的控制器类PersonController 在PersonController类的greet()方法中,把名字为personInfo的请求参数绑定到Person类型的person参数,代码如下: @RequestMapping(value = "/sayHello", method = RequestMethod.POST) public String greet( @RequestParam("personInfo") Person person, Model model) { model.addAttribute("person", person); return "hello"; } 由于3.5.4节已经在Spring MVC框架中注册了PersonConverter类型转换器,Spring MVC框架就会利用该转换器,自动把String类型的personInfo请求参数转换成Person类型的对象。 如图31所示,在hello.jsp页面的文本框中输入字符串“Tom,22,false,52.3”,这个字符串经过PersonConverter的数据类型转换,再由PersonController把它保存到Model中,最后在hello.jsp网页上显示出来。 图31hello.jsp网页显示Person信息 如果在hello.jsp网页上输入的字符串为“Tom,22,not married,null”,PersonConverter试图对它进行类型转换时,会抛出IllegalArgumentException异常。本章没有介绍如何处理程序运行中产生的异常。关于异常的处理,请参见第7章。 3.6请求参数的格式转换 对于日期和数字等类型的请求参数,Spring MVC框架提供了内置的类型转换器,会进行简单的类型转换。例如在例程35中,showDate()方法有一个Date类型的date参数。Spring MVC框架的内置类型转换器会自动把String类型的date请求参数转换为Date类型的date方法参数。 例程35TestFormatController.java @Controller public class TestFormatController { @RequestMapping(value = "/showDate") public String showDate(Date date,Model model) { model.addAttribute("date", date); System.out.println(date); return "showDate"; } @RequestMapping(value = "/useFormat") public String useFormat( @DateTimeFormat(pattern="yyyy-MM-dd")Date date, @NumberFormat(pattern="#,###")int salary, Model model) { model.addAttribute("date", date); model.addAttribute("salary", salary); System.out.println("date="+date); System.out.println("salary="+salary); return "showFormatData"; } } 通过浏览器访问showDate()方法,URL为http://localhost:8080/helloapp/showDate?date=2020/08/20,该URL中date请求参数的值为2020/08/20, Spring MVC框架会把它转换成一个Date对象,赋值给showDate()方法的date参数。 如果通过http://localhost:8080/helloapp/showDate?date=20200820访问showDate()方法,Spring MVC框架的内置类型转换器无法把20200820转换为Date对象,会向客户端返回错误信息。 在这种情况下,可以使用Spring MVC框架的内置格式转换器。下面介绍两个常用的内置格式转换器。 (1) @DateTimeFormat注解: 日期和时间的格式转换器。 (2) @NumberFormat注解: 数字的格式转换器。 例程35的useFormat()方法使用了@DateTimeFormat注解和@NumberFormat注解,并且设定了日期和数字的格式,分别为yyyyMMdd和#,###。 如果通过http://localhost:8080/helloapp/useFormat?date=20200820&salary=5,300访问useFormat()方法,@DateTimeFormat注解会把这个URL中的20200820转换为Date对象,@NumberFormat注解把5,300转换为int类型的5300。 提示: 在Spring 5版本中,如果在Spring MVC的配置文件中为元素设定了conversionservice属性,那么@NumberFormat注解就不起作用,这是需要注意的地方。 除了使用Spring的内置格式转换器,还可以灵活地自定义格式转换器。例程36实现了Formatter接口,能够把基于yyyy*MM*dd格式的字符串转换成Date对象。 例程36DateFormatter.java import org.springframework.format.Formatter; public class DateFormatter implements Formatter { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy*MM*dd"); public String print(Date object, Locale arg) { return dateFormat.format(object); } public Date parse(String source, Locale arg) throws ParseException { return dateFormat.parse(source); } } 接下来,在Spring MVC的配置文件中需要注册自定义的格式转换器,代码如下: 这段代码使得Spring MVC框架会在需要的场合使用自定义的DateFormatter。 通过浏览器再次访问例程35的showDate()方法,URL为http://localhost:8080/helloapp/showDate?date=2020*08*20,Spring MVC框架会利用自定义的DateFormatter把以上URL中的2020*08*20转换成一个Date对象,再把它传给showDate()方法的date参数。 自定义的格式转换器和3.5节的自定义类型转换器可以完成相似的数据类型转换功能。两者的区别如下。 (1) 自定义类型转换器能够对各种数据类型进行转换,而自定义格式转换器只能把String类型转换成其他类型。 (2) 自定义类型转换器会忽略Locale信息,而自定义格式转换器可以根据Locale信息,进行本地化的数据格式转换,这有助于实现Web应用的国际化。第8章将详细介绍了Web应用的国际化。 3.7控制器类的方法的参数类型 Controller类在Spring MVC框架中享有优越地位,Spring MVC框架为控制器类提供了各种资源。从3.4节就可以看出,控制器类如果需要访问客户端的某种请求数据,只需要声明一个与特定请求数据绑定的方法参数。 在Spring MVC框架的全力支持下,控制器类的请求处理方法可以把方法参数定义为以下20种类型。 (1) javax.servlet.ServletRequest或javax.servlet.http.HttpServletRequest。 (2) javax.servlet.ServletResponse或javax.servlet.http.HttpServletResponse。 (3) javax.servlet.http.HttpSession。 (4) org.springframework.web.context.request.WebRequest。 (5) org.springframework.web.context.request.NativeWebRequest。 (6) java.util.Locale。 (7) 用于读取请求数据的java.io.InputStream 或 java.io.Reader。 (8) 用于生成响应结果的java.io.OutputStream 或 java.io.Writer。 (9) java.security.Principal。 (10) org.springframework.http.HttpEntity。 (11) java.util.Map 或 org.springframework.ui.Model。 (12) org.springframework.ui.ModelMap。 (13) org.springframework.web.servlet.ModelAndView。 (14) org.springframework.web.servlet.mvc.support.RedirectAttributes。 (15) org.springframework.validation.Errors。 (16) org.springframework.validation.BindingResult。 (17) org.springframework.web.bind.support.SessionStatus。 (18) org.springframework.web.util.UriComponentsBuilder。 (19) 用@ModelAttribute、@PathVariable、@CookieValue、 @RequestParam、 @RequestHeader、@RequestBody 和@RequestPart 注解标识的参数。 (20) 和请求参数对应的数据类型。 第2章以及本章已经介绍了HttpServletRequest类型、BindingResult类型和Model类型的参数,以及用@ModelAttribute、@RequestParam、@CookieValue和@RequestHeader等注解标识的参数,后文还将陆续介绍其他类型参数的用法。 org.springframework.web.context.request.WebRequest接口的用法和HttpServletRequest接口相似,它也表示客户请求。WebRequest接口的getParameter(String paramName)方法用于读取请求参数,getHeader(String headerName)方法用于读取请求头中的特定项,getContextPath()方法返回当前Web应用的根路径。 3.8控制器类的方法的返回类型 以下是控制器类的请求处理方法的4种常用返回类型。 (1) ModelAndView类型: 包含了Model数据以及视图组件。3.9.4节将介绍把ModelAndView作为返回类型的范例。 (2) String类型: Web组件的逻辑名字。 (3) void: 没有返回值。在这种情况下,在请求处理方法中可以直接通过Writer输出响应结果。 (4) 如果请求处理方法用@ModelAttribute注解来标识,那么方法的返回值无论是什么类型,都会添加到Model中,参见3.9.1节。 3.8.1String返回类型 Controller类的请求处理方法返回String类型字符串,有以下三种用途。 (1) 把请求转发给视图组件。 (2) 把请求转发给其他控制器类组件,返回值以forward:开头。 (3) 把请求重定向到其他控制器类组件,返回值以redirect:开头。 例如,在以下请求处理方法dispatch()中,根据请求参数action的取值返回特定的字符串。 @RequestMapping("test") public String dispatch(@RequestParam("action")String action) { switch(action){ case "forward": //把请求转发给URL入口为input的控制器类的特定方法 return "forward:input"; case "redirect": //重定向到URL入口为output的控制器类的特定方法 return "redirect:output"; case "jsp": default: return "result"; //把请求转发给result.jsp } } dispatch()方法有以下三种返回值。 (1) forward:input: 把请求转发给URL入口为input的控制器类的特定方法。 (2) redirect:output: 重定向到URL入口为output的控制器类的特定方法。 (3) result: 把请求转发给result.jsp文件。 3.8.2void返回类型 当请求处理方法的返回类型为void,可以通过Writer直接在网页上输出数据,例如: @RequestMapping("/testvoid") public void testvoid(Writer writer)throws IOException{ writer.write("hello"); //在网页上输出字符串hello } 3.9控制器与视图的数据共享 视图会把请求数据传给控制器进行处理,控制器也会把响应结果传给视图进行展示。前文已经介绍了视图向控制器传递请求数据的方法。本节将继续介绍由Spring MVC框架提供的用于数据共享的注解、接口和类的用法,主要有以下4种: (1) @ModelAttribute注解: 表示Model的特定数据。 (2) org.springframework.ui.Model接口: 表示Model数据。控制器和视图都能访问Model。 (3) org.springframework.ui.ModelMap类: 表示Model数据的映射。控制器和视图都能访问ModelMap。 (4) org.springframework.web.servlet.ModelAndView类: 表示Model数据和视图。控制器和视图都能访问ModelAndView。 图32显示了这4种注解、接口和类的作用。 图32视图与控制器之间共享数据 Model接口、ModelMap类、ModelAndView类、@ModelAttribute注解中都包含Model。这里的Model和MVC框架中的模型层的概念有区别。MVC框架中的模型层包含业务数据和业务逻辑,而这里的Model是指控制器层的一种用于存放共享数据的容器,它仅包含业务数据,不包含业务逻辑。 Model接口、ModelMap类和ModelAndView类都包含了Model数据,因此它们都具有存取共享数据的功能。它们的区别在于,ModelMap类实现了java.util.Map映射接口,可以直接通过get(String attributeName)方法来读取特定的共享数据; ModelAndView类不仅包含Model数据,还和特定的视图组件绑定,由这个视图组件来展示Model数据。 共享数据在Model中以“属性名=属性值”的形式存放。@ModelAttribute注解表示Model的一个属性,也就是Model中的特定共享数据。 3.9.1@ModelAttribute注解 @ModelAttribute注解有以下三个作用。 (1) 用在控制器类的请求处理方法的参数前面,把方法参数保存到Model中。 (2) 用在控制器类的请求处理方法的参数前面,把Model的特定属性赋值给方法参数。 (3) 用在控制器类的方法前面,表明该方法会向Model中添加特定属性。 1. 把方法参数保存到Model中 在第2章中,PersonController类的greet()方法的person参数用@ModelAttribute注解来标识,例如: @RequestMapping(value = "/sayHello", method = RequestMethod.POST) public String greet( @Valid @ModelAttribute("personbean")Person person, BindingResult bindingResult,Model model) {…} Spring MVC框架先把客户端提供的表单数据和person参数绑定,接着@ModelAttribute注解把person参数保存到Model中,属性名为personbean。 2. 把Model的特定属性赋值给方法参数 假定请求访问以下output()方法的URL中不存在name请求参数,此时,@ModelAttribute注解会把Model中的userName属性赋值给name参数。 @RequestMapping(value="/output") public String output(@ModelAttribute("userName") String name){ System.out.println(name); return "result"; } 例程38将提供完整的范例。 3. 标识用于向Model中添加属性的方法 如果控制器类的一个方法A用@ModelAttribute注解来标识,那么意味着该方法会设置Model数据。当Spring MVC框架调用请求处理方法B之前,会先调用方法A。 提示: 如果控制器类有多个方法(按照定义的先后顺序分别为方法A、方法B和方法C)都用@ModelAttribute注解来标识,那么当Spring MVC框架调用请求处理方法D之前,会依次先调用方法A、方法B和方法C,然后再调用方法D。 例程37的setModel()方法用@ModelAttribute注解来标识。 例程37TestModelController.java @Controller public class TestModelController { @ModelAttribute public void setModel(String userName,Model model){ //把userName方法参数作为userName属性加入到Model中 model.addAttribute("userName", userName); } @RequestMapping(value="/testmodel") public String login(Model model){ // 从Model中读取userName String userName=(String)model.getAttribute("userName"); System.out.println(userName); return "showUser"; } } 在setModel()方法中,userName请求参数和userName方法参数绑定。setModel()方法把userName方法参数作为userName属性添加到Model中。在login()方法中,从Model读取userName属性,并且把请求转发给showUser.jsp。showUser.jsp通过EL表达式${userName}输出Model的userName属性。 通过浏览器访问http://localhost:8080/helloapp/testmodel?userName=Mary,Spring MVC框架先调用setModel()方法,再调用login()方法,最后把请求转发给showUser.jsp。 例程37中用@ModelAttribute注解标识的setModel()方法的返回类型为void。以下代码重新实现了setModel()方法,它具有String类的返回值。它能完成同样的功能,把userName属性添加到Model中。 @ModelAttribute("userName") public String setModel(String userName) { return userName; } @ModelAttribute注解为Model添加了一个userName属性,setModel()方法的返回值就是Model中userName属性的值。 @ModelAttribute注解标识的setModel()方法返回Person对象,这个Person对象会作为person属性添加到Model中。 @ModelAttribute("person") public Person setModel(String userName) { Person p=new Person(); p.setUserName(userName); return p; //返回的Person对象保存到Model中,属性名为person } @RequestMapping(value="/getperson") public String login(Model model){ // 从Model中读取person Person person=(Person)model.getAttribute("person"); System.out.println(person.getUserName()); return "result"; } 例程38在setdata()方法前和output()方法的name参数前都使用了@ModelAttribute注解。 例程38TestAttributeController.java @Controller public class TestAttributeController { @ModelAttribute("userName") public String setdata(String name){ return name.toUpperCase(); } @RequestMapping(value="/output") public String output(@ModelAttribute("userName") String name){ System.out.println(name); return "result"; } } setdata()方法前的@ModelAttribute注解向Model添加了一个userName属性。output()方法的name参数前的@ModelAttribute注解把Model中的userName属性和name参数绑定。 当通过浏览器访问http://localhost:8080/helloapp/output?name=Tom,TestAttributeController类的output()方法会在服务器端打印TOM。 3.9.2Model接口 Model接口表示Model数据,存放在Model中的数据采用“属性名/属性值”的形式。Model中的数据能够被控制器和视图共享,例程37也演示了Model接口的用法。 //向Model中存放共享数据 model.addAttribute("userName", userName); //从Model中读取共享数据 String userName=(String)model.getAttribute("userName"); 3.9.3ModelMap类 ModelMap类和Model接口的功能相似,区别在于两者的语义不同。要从Model对象中读取共享数据,可以调用getAttribute(String attributeName)方法; 而ModelMap类表示Model的映射类型,可以调用ModelMap类的get(String attributeName)方法获得特定的属性值。 此外,Model接口的asMap()方法会返回一个Map对象,通过这个Map对象也能读取特定的属性,例如以下三种方式分别从Model或ModelMap中读取特定属性。 Model model=… ModelMap modelMap=… String userName1=(String)model.getAttribute("userName"); String userName2=(String)(model.asMap().get("userName")); String userName3=(String)modelMap.get("userName"); 例程39的功能与例程37相同,区别在于本节范例用ModelMap类来存放共享数据。 例程39TestModelMapController.java @Controller public class TestModelMapController { @ModelAttribute public void setModel(String userName, ModelMap modelMap){ modelMap.addAttribute("userName", userName); } @RequestMapping(value="/testmap") public String login(ModelMap modelMap){ // 从ModelMap中读取userName属性 String userName=(String)modelMap.get("userName"); System.out.println(userName); return "showUser"; } } 3.9.4ModelAndView类 ModelAndView类和Model接口一样,也能存放共享数据。但它们有以下两点区别。 (1) ModelAndView类添加共享数据的方法是addObject(String attributeName,Object attributeValue),而Model接口添加共享数据的方法是addAddtribute(String attributeName,Object attributeValue)。 (2) ModelAndView类的setViewName(String viewName)方法指定用于展示Model数据的视图组件,参数viewName指定视图组件的逻辑名字,而Model接口不具有这样的方法。 ModelAndView类的以下方法返回包含Model数据的Map对象或ModelMap对象。 Map getModel() ModelMap getModelMap() 例程310的功能与例程37相同,区别在于本节范例用ModelAndView类来存放共享数据。 例程310TestViewController.java @Controller public class TestViewController { @ModelAttribute public void setModel(String userName, ModelAndView modelAndView){ modelAndView.addObject("userName", userName); } @RequestMapping(value="/testview") public ModelAndView login(ModelAndView modelAndView){ // 从ModelAndView中读取userName属性 String userName= (String)(modelAndView.getModel().get("userName")); System.out.println(userName); //showUser是showUser.jsp的逻辑名字 modelAndView.setViewName("showUser"); return modelAndView; //把请求转发给showUser.jsp } } login()方法的返回类型为ModelAndView,login()方法会把请求转发给返回值modelAndView对象指定的视图组件showUser.jsp。 3.9.5把Model中的数据存放在session范围内 2.4.3节已经介绍过,默认情况下,添加到Model中的数据存放在request范围内。如果要把数据存放到session范围内,可以使用@SessionAttributes注解。例程311使用了@SessionAttributes注解。 例程311TestSessionController.java @Controller @SessionAttributes(value={"person","age"}) public class TestSessionController { @ModelAttribute("person") public Person setModel(){ Person p=new Person(); p.setUserName("Tom"); return p; } @RequestMapping(value="/testsession") public String testSession(Model model,int age,String address){ model.addAttribute("age",age); model.addAttribute("address",address); return "sessiontest"; } @RequestMapping(value="/repeat") public String repeat(SessionStatus sessionStatus){ return "sessiontest"; } @RequestMapping(value="/sessionclear") public String testSessionClear(SessionStatus sessionStatus){ sessionStatus.setComplete(); //清除session范围内的Model数据 return "sessiontest"; } } 以上TestSessionController类前的@SessionAttributes(value={"person","age"})注解的作用是声明Model中的person属性和age属性存放在request范围内。 setModel()方法向Model添加了person属性,testSession()方法向Model添加了age属性,person属性和age属性都会保存在session范围内。testSession()方法还向Model添加了address属性,该属性会保存在默认的request范围内。 在testSessionClear()方法中,通过SessionStatus类的setComplete()方法清除session范围内的Model数据。 testSession()、repeat()和testSessionClear()方法都会把请求转发给sessiontest.jsp。sessiontest.jsp输出session范围和request范围内的共享数据,例如: UserName:${sessionScope.person.userName}
Age:${sessionScope.age}
Address:${requestScope.address} 下面按以下三个步骤来运行本范例。 (1) 通过浏览器访问testSession()方法,URL为localhost:8080/helloapp/testsession?age=22&address=shanghai,Spring MVC框架先调用setModel()方法,再调用testSession()方法,这两个方法向session范围存放person属性和age属性,还向request范围存放address属性。sessiontest.jsp会在网页上输出如下信息: UserName:Tom Age:22 Address:shanghai (2) 通过浏览器访问repeat()方法,URL为localhost:8080/helloapp/repeat。此时,在session范围内存在person和age属性,但是在request范围内没有address属性,sessiontest.jsp会在网页上输出如下信息: UserName:Tom Age:22 Address: (3) 通过浏览器访问testSessionClear()方法,URL为localhost:8080/helloapp/sessionclear,testSessionClear()方法清除session范围内的Model数据,sessiontest.jsp会在网页上输出如下信息。 UserName: Age: Address: 3.9.6通过@SessionAttribute注解读取session范围内的Model数据 @SessionAttributes注解把Model数据存放在session范围内,而@SessionAttribute注解读取session范围内的Model数据。@SessionAttribute注解用来标识控制器类的请求处理方法的参数。 在例程312中,testShare()方法的person参数用@SessionAttribute注解标识。 例程312TestShareController.java @Controller @SessionAttributes("person") public class TestShareController { @RequestMapping(value="/setdata") public String testData(Model model){ Person p=new Person(); p.setUserName("Tom"); model.addAttribute("person",p); return "redirect:testshare"; } @RequestMapping(value="/testshare") public String testShare(@SessionAttribute("person") Person person){ System.out.println(person.getUserName()); //打印Tom return "result"; } } testData()方法向Model加入了一个Person对象。TestShareController类的@SessionAttributes("person")注解使得该Person对象实际上存放在session范围内。接下来,testData()方法把请求重定向到testShare()方法。 testShare()方法的@SessionAttribute("person")注解从session范围内获得Person对象,把它赋值给testShare()方法的person方法参数。由此可见,@SessionAttribute注解能把session范围内的特定数据与请求处理方法的参数绑定。 通过浏览器访问http://localhost:8080/helloapp/setdata,testShare()方法会在服务器端打印person.getUserName()方法的返回值Tom。 3.10@ControllerAdvice注解的用法 当一个Web应用中的多个控制器类要完成一些共同的操作,传统的做法是定义一个控制器父类(例如BaseController),它包含了执行共同操作的方法,其他的控制器类(例如ControllerA和ControllerB)继承这个控制器父类。图33显示了控制器父类和控制器子类的关系。 继承是提高控制器类的代码可重用性的有效手段,但是它有一个缺点,那就是由于Java语言不支持多继承,当控制器类继承了一个控制器父类后,就不能再继承其他的类。 Spring MVC框架提供了另一种方式来为多个控制器类提供共同的方法,那就是利用@ControllerAdvice注解来定义一个控制器增强类。 控制器增强类并不是控制器类的父类。在程序运行时,Spring MVC框架会把控制器增强类的方法代码块动态注入其他控制器类中,通过这种方式来增强控制器类的功能。图34显示了控制器增强类(例如MyControllerAdvice)和控制器类的关系。 图33控制器父类和控制器子类的关系 图34控制器增强类和控制器类的关系 例程313的setColors()方法向Model中加入一个colors属性。 例程313MyControllerAdvice.java package mypack; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ModelAttribute; import java.util.*; @ControllerAdvice public class MyControllerAdvice { @ModelAttribute(name = "colors") public Map setColors() { HashMap colors = new HashMap(); colors.put("RED", "红色"); colors.put("BLUE", "蓝色"); colors.put("GREEN", "绿色"); return colors; } } 当程序运行时,Spring MVC框架会把MyControllerAdvice类的setColors()方法动态注入其他控制器类中,因此其他控制器类就自动拥有了该方法。例如,在TestAttributeController类中可以直接访问Model中的colors属性,代码如下: @RequestMapping(value="/testColor") public String testColor( @ModelAttribute("colors") Map colors, @ModelAttribute("userName") String name){ System.out.println(name+"'s favourite color:" +colors.get("RED")); return "result"; } 通过浏览器访问http://localhost:8080/helloapp/testColor?name=Tom,testColor()方法会在服务器端打印TOM's favourite color:红色。 默认情况下,@ControllerAdvice注解用来增强当前Web应用中所有控制器类的功能。此外,它的assignableTypes属性和basePackages属性用来指定需要增强功能的控制器类,例如: //增强PersonController和TestAttributeController的功能 @ControllerAdvice(assignableTypes={PersonController.class, TestAttributeController.class}) public class MyControllerAdvice1{…} //增强mypack包和net.javathinker包中的控制器类的功能 @ControllerAdvice(basePackages ={"mypack","net.javathinker"}) public class MyControllerAdvice2{…} 3.11小结 当视图和控制器分离后,视图与控制器之间需要进行数据的传递和共享。为了方便存取共享数据,Spring MVC框架提供了实用的注解和类,包括: (1) 把请求数据和控制器类的方法参数绑定的注解: @RequestParam注解、@RequestHeader注解、@CookieValue注解和@PathVariable注解。 (2) 对请求参数进行类型转换的接口: org.springframework.core.convert.converter.Converter接口。 (3) 对请求参数进行格式转换的接口: org.springframework.format.Formatter接口。 (4) 存取Model数据的接口和类: Model接口、ModelMap类和ModelAndView类。 (5) 把Model数据和控制器类的方法参数绑定的注解: @ModelAttribute注解。 (6) 把Model数据存放在session范围内的注解: @SessionAttributes注解。 (7) 把session范围内的Model数据和控制器类的方法参数绑定的注解: @SessionAttribute注解。 Controller类是Spring MVC框架中的主力军,Spring MVC框架会掌管Controller对象的生命周期,同时,Spring MVC框架也给控制器类提供了自由发挥的空间: (1) 控制器类的请求处理方法的参数类型可以是来自Servlet API的ServletRequest、ServletResponse和HttpSession等,也可以是来自Spring API的Model、ModelMap和ModelAndView等,还可以是和请求参数对应的任意的数据类型等。 (2) 控制器类的请求处理方法的返回类型可以是void、String以及ModelAndView等。 3.12思考题 1. ()注解用来设定控制器对象的存在范围。(多选) A. @RequestMapping B. @SessionScope C. @Scope D. @RequestScope 2. 对于一个控制器类的以下方法: @RequestMapping(value = "test", params = { "username=Tom", "age","!address" }) public String sayHello() {…} ()是能正常访问sayHello()方法的URL。(多选) A. /test?username=Tom&age=20 B. /sayHello?username=Tom&age=20 C. /test?username=Mike D. /test?username=Tom&age=20&gender=male 3. 在一个控制器类的请求处理方法中,应该用()定义double类型的salary参数,从而把salary请求参数和salary方法参数绑定。(多选) A. @RequestParam(name="salary") double salary B. @RequestParam(value="salary") double salary C. @RequestParam double salary D. double salary 4. 对于控制类的请求处理方法,它的方法参数可以定义为()类型。(多选) A. javax.servlet.ServletRequest B. java.io.Writer C. org.springframework.ui.ModelMap D. 用@SessionAttributes注解标识的参数 5. ()注解既能标识控制器类,又能标识控制器类的方法。(单选) A. @SessionAttribute B. @RequestParam C. @PathVariable D. @RequestMapping 6. 以下具有asMap()方法的是()。(单选) A. ModelMap类 B. ModelAndView类 C. Model接口 D. 用@Controller注解标识的控制器类 7. 一个控制器类的请求处理方法的返回值是forward:hello,以下说法正确的是()。(单选) A. Spring MVC框架会把请求转发给URL入口为/hello的控制器类的请求处理方法 B. Spring MVC框架会把请求转发给hello.jsp C. Spring MVC框架会在服务器端打印字符串forward:hello D. Spring MVC框架会把请求重定向到URL入口为/hello的控制器类的请求处理方法