美文网首页
Spring 学习笔记(八)构建 Spring Web 应用程序

Spring 学习笔记(八)构建 Spring Web 应用程序

作者: Theodore的技术站 | 来源:发表于2018-12-24 16:43 被阅读3次

    接受请求的输入

    有些Web应用是只读的。人们只能通过浏览器在站点上闲逛,阅读服务器发送到浏览器中的内容。

    不过,这并不是一成不变的。众多的Web应用允许用户参与进去,将数据发送回服务器。如果没有这项能力的话,那Web将完全是另一番景象。

    Spring MVC允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括:
    -查询参数(Query Parameter)。
    -表单参数(Form Parameter)。
    -路径变量(Path Variable)。
    你将会看到如何编写控制器处理这些不同机制的输入。作为开始,我们先看一下如何处理带有查询参数的请求,这也是客户端往服务器端发送数据时,最简单和最直接的方式。

    处理查询参数

    在Spittr应用中,我们可能需要处理的一件事就是展现分页的Spittle列表。在现在的SpittleController中,它只能展现最新的Spittle,并没有办法向前翻页查看以前编写的Spittle历史记录。如果你想让用户每次都能查看某一页的Spittle历史,那么就需要提供一种方式让用户传递参数进来,进而确定要展现哪些Spittle集合。

    在确定该如何实现时,假设我们要查看某一页Spittle列表,这个列表会按照最新的Spittle在前的方式进行排序。因此,下一页中第一条的ID肯定会早于当前页最后一条的ID。所以,为了显示下一页的Spittle,我们需要将一个Spittle的ID传入进来,这个ID要恰好小于当前页最后一条Spittle的ID。另外,你还可以传入一个参数来确定要展现的Spittle数量。

    为了实现这个分页的功能,我们所编写的处理器方法要接受如下的参数:
    -before参数(表明结果中所有Spittle的ID均应该在这个值之前)。
    -count参数(表明在结果中要包含的Spittle数量)。

    为了实现这个功能,我们将程序清单5.10中的spittles()方法替换为使用before和count参数的新spittles()方法。我们首先添加一个测试,这个测试反映了新spittles()方法的功能。

    @Test
        public void shouldShowPagedSpittles() throws Exception{
            List<Spittle> expectedSpittles = createSpittleList(50);
            SpittleRepository mockRepository = mock(SpittleRepository.class);
            when(mockRepository.findSpittles(238900,50))
                .thenReturn(expectedSpittles);
            SpittleController controller = new SpittleController(mockRepository);
            
            MockMvc mockMvc = standaloneSetup(controller)
                    .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
                    .build();
            
            mockMvc.perform(get("/spittles?max=238900&count=50"))
                .andExpect(view().name("Spittles"))
                .andExpect(model().attributeExists("spittleList"))
                .andExpect(model().attribute("spittleList",
                        hasItems(expectedSpittles.toArray())));
    
    @RequestMapping(method=RequestMethod.GET)
    public List<Spittle> spittles(@RequestParam("max") long max,@RequestParam("count") int count){
        return spittleRepository.findSpittles(max,count);
    }
    

    这个测试方法与程序清单5.9中的测试方法关键区别在于它针对“/spittles”发送GET请求,同时还传入了max和count参数。它测试了这些参数存在时的处理器方法,而另一个测试方法则测试了没有这些参数时的情景。这两个测试就绪后,我们就能确保不管控制器发生什么样的变化,它都能够处理这两种类型的请求。

    SpittleController中的处理器方法要同时处理有参数和没有参数的场景,那我们需要对其进行修改,让它能接受参数,同时,如果这些参数在请求中不存在的话,就使用默认值Long.MAX_VALUE和20。@RequestParam注解的defaultValue属性可以完成这项任务:

    @RequestMapping(method=RequestMethod.GET)
    public List<Spittle> spittles(@RequestParam(value="max",
                                                defaultValue=MAX_LONG_AS_STRING) long max,
                                  @ReuquestParam(value="count",defaultValue="20") int count){
         return spittleRepository.findSpittles(max,count);
    }
    

    通过路径参数接受输入

    假设我们的应用程序需要根据给定的ID来展现某一个Spittle记录。其中一种方案就是编写处理器方法,通过使用@RequestParam注解,让它接受ID作为查询参数:

    @ReuqestMapping(value="/show", method=RequestMethod.GET)
    public String showSpittle(@RequestParam("spittle_id") long spittleId, Model model) {
        model.addAttribute(spittleRepository.findOne(spittleId));
        return "spittle";
    }
    

    这个处理器方法将会处理形如“/spittles/show?spittle_id=12345”这样的请求。尽管这也可以正常工作,但是从面向资源的角度来看这并不理想。在理想情况下,要识别的资源(Spittle)应该通过URL路径进行标示,而不是通过查询参数。对“/spittles/12345”发起GET请求要优于对“/spittles/show?spittle_id=12345”发起请求。*前者能够识别出要查询的资源,而后者描述的是带有参数的一个操作——本质上是通过HTTP发起的RPC。
    既然已经以面向资源的控制器作为目标,那我们将这个需求转换为一个测试。程序清单5.12展现了一个新的测试方法,它会断言SpittleController中对面向资源 请求的处理。

    @Test
        public void testSpittle() throws Exception{
            Spittle expectedSpittle = new Spittle("Hello", new Date());
            SpittleRepository mockRepository = mock(SpittleRepository.class);
            when(mockRepository.findOne(12345)).thenReturn(expectedSpittle);
            SpittleController controller = new SpittleController(mockRepository);
            MockMvc mockMvc = standaloneSetup(controller).build();
            
            mockMvc.perform(get("/spittle/12345"))
                .andExpect(view().name("Spittles"))
                .andExpect(model().attributeExists("spittle"))
                .andExpect(model().attribute("spittle",expectedSpittle));
        }
    

    如果想让这个测试通过的话,我们编写的@RequestMapping要包含变量部分,这部分代表了Spittle ID。

    为了实现这种路径变量,Spring MVC允许我们在@RequestMapping路径中添加占位符。占位符的名称要用大括号(“{”和“}”)括起来。路径中的其他部分要与所处理的请求完全匹配,但是占位符部分可以是任意的值。

    @RequestMapping(value="/{spittleId}",method=RequestMethod.GET)
    public String spittle(@PathVarible("spittleId") long spittleId,Model model){
        model.addAttribute(spittleRepository.findOne(spittleIdd));
        return "spittle";
    }
    

    我们可以看到,spittle()方法的spittleId参数上添加了@PathVariable("spittleId")注解,这表明在请求路径中,不管占位符部分的值是什么都会传递到处理器方法的spittleId参数中。如果对“/spittles/54321”发送GET请求,那么将会把“54321”传递进来,作为spittleId的值。

    如果@PathVariable中没有value属性的话,它会假设占位符的名称与方法的参数名相同。这能够让代码稍微简洁一些,因为不必重复写占位符的名称了。但需要注意的是,如果你想要重命名参数时,必须要同时修改占位符的名称,使其互相匹配。

    spittle()方法会将参数传递到SpittleRepository的findOne()方法中,用来获取某个Spittle对象,然后将Spittle对象添加到模型中。模型的key将会是spittle,这是根据传递到addAttribute()方法中的类型推断得到的。

    这样Spittle对象中的数据就可以渲染到视图中了,此时需要引用请求中key为spittle的属性(与模型的key一致)。如下为渲染Spittle的JSP视图片段:

    <div class="spittleView">
        <div class="spittleMessage"><c:out value="$(spittle.message)"/></div>
        <div>
            <span class="spittleTime"><c:out value="${spittle.time}"/></span>
        </div>
    </div>
    
    image.png

    处理表单

    Web应用的功能通常并不局限于为用户推送内容。大多数的应用允许用户填充表单并将数据提交回应用中,通过这种方式实现与用户的交互。像提供内容一样,Spring MVC的控制器也为表单处理提供了良好的支持。

    使用表单分为两个方面:展现表单以及处理用户通过表单提交的数据。在Spittr应用中,我们需要有个表单让新用户进行注册。SpitterController是一个新的控制器,目前只有一个请求处理的方法来展现注册表单。

    package spittr.web;
    
    import static org.springframework.web.bind.annotation.RequestMethod.*;
    import org.springframework.stereotype.Controller;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    @RequestMapping("/spitter")
    public class SpitterControllerRegist {
        @RequestMapping(value="register",method=GET)
        public String showRegistrationForm() {
            return "registerForm";
        }
    }
    

    showRegistrationForm()方法的@RequestMapping注解以及类级别上的@RequestMapping注解组合起来,声明了这个方法要处理的是针对“/spitter/register”的GET请求。这是一个简单的方法,没有任何输入并且只是返回名为registerForm的逻辑视图。按照我们配置InternalResourceViewResolver的方式,这意味着将会使用“/WEB-INF/ views/registerForm.jsp”这个JSP来渲染注册表单。
    尽管showRegistrationForm()方法非常简单,但测试依然需要覆盖到它。因为这个方法很简单,所以它的测试也比较简单。

    @Test
    public void shouleShowRegistration() throws Exception{
        SpitterController controller = new SpitterController();
        MockMvc mockMvc = standaloneSetup(controller).build();
        mockMvc.perform(get("/spitter/register")).andExcept(view().name("registerForm"));
    }
    

    渲染注册表单的JSP:

    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
    <%@ page session="false"%>
    <html>
    <head>
        <title>Spitter</title>
        <link rel="stylesheet" type="text/css" href="<c:url value="/resources/style.css"/>">
    </head>
    <body>
        <h1>Register</h1>
        <form method="POST">
            First Name:<input type="text" name="firstName"/><br/>
            Last Name:<input type="text" name="lastName"/><br/>
            Username:<input type="text" name="username"/><br/>
            Password:<input type="password" name="password"/><br/>
            <input type="submit" value="Register"/>
        </form>
    </body>
    </html>
    
    image.png

    编写处理表单的控制器

    当处理注册表单的POST请求时,控制器需要接受表单数据并将表单数据保存为Spitter对象。最后,为了防止重复提交(用户点击浏览器的刷新按钮有可能会发生这种情况),应该将浏览器重定向到新创建用户的基本信息页面。这些行为通过下面的shouldProcessRegistration()进行了测试。

    @Test
        public void shouldProcessRegistration() throws Exception{
            SpitterRepository mockRepository = mock(SpitterRepository.class);
            Spitter unsaved = new Spitter("jbauer","24hours","Jack","Bauer");
            Spitter saved = new Spitter(24L,"jbauer","24hours","Jack","Bauer");
            when(mockRepository.save(unsaved)).thenReturn(saved);
            
            SpitterController controller = new SpitterController(mockRepository);
            MockMvc mockMvc = standaloneSetup(controller).build();
            
            mockMvc.perform(post("/spitter/register")
                    .param("firstName","Jack")
                    .param("lastName","Bauer")
                    .param("username","jbauer")
                    .param("password","24hours"))
            .andExpect(redirectedUrl("/spitter/jbauer"));
            verify(mockRepository,atLeastOnce()).save(unsaved);     
        }
    

    显然,这个测试比展现注册表单的测试复杂得多。在构建完SpitterRepository的mock实现以及所要执行的控制器和MockMvc之后,shouldProcess-Registration()对“/spitter/register”发起了一个POST请求。作为请求的一部分,用户信息以参数的形式放到request中,从而模拟提交的表单。

    在处理POST类型的请求时,在请求处理完成后,最好进行一下重定向,这样浏览器的刷新就不会重复提交表单了。在这个测试中,预期请求会重定向到“/spitter/jbauer”,也就是新建用户的基本信息页面。最后,测试会校验SpitterRepository的mock实现最终会真正用来保存表单上传入的数据。

    现在,我们来实现处理表单提交的控制器方法。通过shouldProcess-Registration()方法,我们可能认为要满足这个需求需要做很多的工作。但是,在如下的程序清单中,我们可以看到新的SpitterController并没有做太多的事情。

    package spittr.web;
    
    import static org.springframework.web.bind.annotation.RequestMethod.*;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import spittr.Spitter;
    import spittr.data.SpitterRepository;
    
    
    @Controller
    @RequestMapping("/spitter")
    public class SpitterController {
        private SpitterRepository spitterRepository;
        
        @Autowired
        public SpitterContriller(SpitterRepository spitterRepository) {
            this.spitterRepository = spitterRepository;
        }
        
        @RequestMapping(value="/register",method=GET)
        public String showRegistrationForm() {
            return "registerForm";
        }
        
        @RequestMapping(value="/register",method=POST)
        public String processRegistration(Spitter spitter) {
            spitterRepository.save(spitter);
            return "redirect:/spitter/" + spitter.getUsername();
        }
    }
    

    我们之前创建的showRegistrationForm()方法依然还在,不过请注意新创建的processRegistration()方法,它接受一个Spitter对象作为参数。这个对象有firstName、lastName、username和password属性,这些属性将会使用请求中同名的参数进行填充。

    当使用Spitter对象调用processRegistration()方法时,它会进而调用SpitterRepository的save()方法,SpitterRepository是在Spitter-Controller的构造器中注入进来的。

    processRegistration()方法做的最后一件事就是返回一个String类型,用来指定视图。但是这个视图格式和以前我们所看到的视图有所不同。这里不仅返回了视图的名称供视图解析器查找目标视图,而且返回的值还带有重定向的格式。
    当InternalResourceViewResolver看到视图格式中的“redirect:”前缀时,它就知道要将其解析为重定向的规则。

    校验表单

    如果用户在提交表单的时候,username或password文本域为空的话,那么将会导致在新建Spitter对象中,username或password是空的String。至少这是一种怪异的行为。如果这种现象不处理的话,这将会出现安全问题,因为不管是谁只要提交一个空的表单就能登录应用。

    同时,我们还应该阻止用户提交空的firstName和/或lastName,使应用仅在一定程度上保持匿名性。有个好的办法就是限制这些输入域值的长度,保持它们的值在一个合理的长度范围,避免这些输入域的误用。

    有种处理校验的方式非常初级,那就是在processRegistration()方法中添加代码来检查值的合法性,如果值不合法的话,就将注册表单重新显示给用户。这是一个很简短的方法,因此,添加一些额外的if语句也不是什么大问题,对吧?
    与其让校验逻辑弄乱我们的处理器方法,还不如使用Spring对Java校
    验API(Java Validation API,又称JSR-303)的支持。


    image.png

    请考虑要添加到Spitter域上的限制条件,似乎需要使用@NotNull和@Size注解。我们所要做的事情就是将这些注解添加到Spitter的属性上。如下的程序清单展现了Spitter类,它的属性已经添加了校验注解。

    package spittr.web;
    
    public class Spitter {
        private Long id;
        
        @NotNull
        @Size(min = 5,max=16)
        private String username;
        
        @NotNull
        @Size(min = 5,max = 25)
        private String password;
        
        @NotNull
        @Size(min = 2, max = 30)
        private String firstName;
        
        @NotNull
        @Size(min = 2, max = 30)
        private String lastName;
        
    }
    

    processRegistration():确保所提交的数据是合法的

    @RequestMapping(value="/register",method=POST)
    public String processRegisteration(@Valid Spitter spitter,Errors errors){
        if(errors.hasErrors()){
            return "registerForm";
        }
        spitterRepository.save(spitter);
        return "redirect:/spitter/" + spitter.getUsername();
    }
    

    Spitter参数添加了@Valid注解,这会告知Spring,需要确保这个对象满足校验限制。

    如果有校验出现错误的话,那么这些错误可以通过Errors对象进行访问,现在这个对象已作为processRegistration()方法的参数。(很重要一点需要注意,Errors参数要紧跟在带有@Valid注解的参数后面,@Valid注解所标注的就是要检验的参数。)processRegistration()方法所做的第一件事就是调用Errors.hasErrors()来检查是否有错误。

    如果有错误的话,Errors.hasErrors()将会返回到registerForm,也就是注册表单的视图。这能够让用户的浏览器重新回到注册表单页面,所以他们能够修正错误,然后重新尝试提交。

    相关文章

      网友评论

          本文标题:Spring 学习笔记(八)构建 Spring Web 应用程序

          本文链接:https://www.haomeiwen.com/subject/motzkqtx.html