Spring——Spring MVC(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011024652/article/details/80314629

本文主要依据《Spring实战》第五章内容进行总结

Spring MVC框架是基于模型-视图-控制器(Model-View-Controller,MVC)模式实现,它能够构建像Spring框架那样灵活和松耦合的Web应用。

1、Spring MVC起步

1.1、Spring MVC如何处理客户端请求

Spring MVC处理客户端请求的过程可以参考如下所示的图示:

这里写图片描述

具体步骤如下:

  1. 客户端请求离开浏览器时①,会带有用户请求内容的信息,至少会包含请求的URL,但是还可能带有其他的信息,例如用户提交的表单信息;
  2. 请求会传递给Spring的DispatcherServlet,DispatcherServlet是一个前端控制器,主要负责将请求委托给应用程序的其他组件来执行实际的处理;
  3. DispatcherServlet要将请求发送给Spring MVC控制器(controller),控制器是一个用于处理请求的Spring组件,DispatcherServlet要确定将请求发送给哪个控制器,所以DispatcherServlet会查询一个或多个处理器映射(handler mapping)②,处理器映射根据请求的URL信息进行决策;
  4. 一旦选择了合适的控制器,DispatcherServlet会将请求发送给选中的控制器③,由控制器进行业务逻辑的处理;
  5. 控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示,这些信息被称为模型(model),控制器要将模型数据打包,并且标示出用于渲染输出的视图名,然后将模型及视图名发送回DispatcherServlet④;
  6. 传递给DispatcherServlet的视图名不一定是真实对应的视图名称,可能是一个逻辑视图名,DispatcherServlet将会使用视图解析器(view resolver)⑤来将逻辑试图名匹配一个特定的试图实现;
  7. 请求最后到达真实视图⑥,在这里它交付模型数据,请求的任务也就完成了;
  8. 视图将使用模型数据渲染输出,这个输出会通过响应对象传递给客户端⑦。

1.2、搭建Spring MVC

1.2.1、配置DispatcherServlet

DispatcherServlet是Spring MVC的核心,它主要负责将请求路由到其它的组件之中,所以配置Spring MVC的第一步就是配置DispathcerServlet。

按照传统的Web框架,Servlet一般都是在web.xml中进行配置的,但在Servlet 3规范之后,我们可以通过Java代码的方式配置,只需要将Java类实现javax.servlet.ServletContainerInitializer接口即可,在Servlet 3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果能发现的话,就会用它来配置Servlet容器。Spring提供了这个接口的实现SpringServletContainerInitializer,这个类又会反过来查找实现WebApplicationInitializer的类。在这里我们创建一个配置类SpringMvcInitializer:

public class SpringMvcInitializer extends
        AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] {RootConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] {WebConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] {"/"};
    }
}

在这里SpringMvcInitializer类扩展了AbstractAnnotationConfigDispatcherServletInitializer,而AbstractAnnotationConfigDispatcherServletInitializer实现了WebApplicationIntializer,所以部署到Servlet 3.0容器中的时候,容器就会自动发现它,并用它来配置Servlet上下文,在后面的章节,我们将会介绍如何使用web.xml配置DispathcerServlet。

我们可以看到,SpringMvcInitializer重写了三个方法,其中getServletMappings()方法会将一个或多个路径映射到DispatcherServlet上,在这里,它映射的是“/”,这表示它会是应用的默认Servlet,它会处理进入应用的所有请求。

1.2.2、两个应用上下文

我们可以看到,上面的SpringMvcInitializer配置类中除了getServletMappings()方法之外还有两个方法,这两个方法都是配置Spring应用上下文的。

当DispathcherServlet启动的时候,它会创建Spring应用上下文,并加载配置文件或配置类中声明的bean,在getServletConfigClasses()方法中,我们要求DispatcherServlet加载应用上下文时,使用定义在WebConfig配置类中的bean。

扫描二维码关注公众号,回复: 3657304 查看本文章

但是在Spring Web应用中,通常还会有另外一个应用上下文,另外的这个应用上下文就是由ContextLoaderListener创建的。

实际上,AbstractAnnotationConfigDispatcherServletInitializer会同时创建DispatcherServlet和ContextLoaderListener,getServletConfigClasses()方法返回的带有@Configuration注解的类将会用来定义DispatcherServlet应用上下文中的bean,getRootConfigClasses()方法返回的带有@Configuration注解的类将会用来配置ContextLoaderListener创建的应用上下文的bean。

通常情况下,DispathcerServlet加载包含Web组件的bean,如控制器、视图解析器以及处理器映射,而ContextLoaderListener要加载应用中的其他bean,这些bean通常是驱动应用后端的中间层和数据层组件。

1.2.3、启用Spring MVC

配置好DispatcherServlet之后,我们需要创建Spring MVC的配置WebConfig,下面示例是最简单的Spring MVC配置:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter{ 
}

最简单的Spring MVC配置就是一个带有@EnableWebMVC注解的类,它可以启用Spring MVC,但是它还有不少的问题需要解决:

  • 没有配置视图解析器,这样的话,Spring会默认使用BeanNameViewResolver;
  • 没有启用组件扫描,这样的话,Spring只能找到显式声明在配置类中的控制器;
  • Dispatcher会映射为应用的默认Servlet,它会处理所有的请求,包括静态资源的请求。

所以我们需要稍微调整一下上面的配置:

@Configuration
@EnableWebMvc
@ComponentScan("web")
public class WebConfig extends WebMvcConfigurerAdapter{

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configure) {
        configure.enable();
    }

}

可以看到,在这个配置类中,我们使用@EnableWebMvc启用Spring MVC,接着我们使用@ComponentScan启用组件扫描,它会查找web包下的组件,然后我们添加了一个ViewResolver视图解析器,在这里,我们使用的是InternalResourceViewResolver,它会查找JSP文件,查找的时候,它会在视图名称上加一个特定的前缀和后缀,例如,名为home的视图将会解析为/WEB-INF/views/home.jsp。最后,WebConfig扩展了WebMvcConfigurerAdapter,通过调用DefaultServletHandlerConfigurer的enable()方法,我们要求DispatcherServlet将对静态资源的请求转发到Servlet容器默认的Servlet上,而不是DispatcherServlet本身来处理此类请求。

2、控制器

2.1、一个简单的控制器

在Spring MVC中,控制器只是方法上添加了@RequestMapping注解的类,这个注解声明了它们所要处理的请求。下面这个例子就是一个最简单的控制器:

@Controller
public class HomeController {
    @RequestMapping(value="/",method=RequestMethod.GET)
    public String home() {
        return "home";
    }
}

可以看到,HomeController带有@Controller注解,@Controller注解是用来声明控制器的,通过查看它的源码:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {

    /**
     * The value may indicate a suggestion for a logical component name,
     * to be turned into a Spring bean in case of an autodetected component.
     * @return the suggested component name, if any
     */
    String value() default "";

}

我们发现,它是基于@Component注解的,它的目的就是辅助实现组件扫描,因为HomeController带有@Controller注解,组件扫描会自动发现它并将它声明为一个Spring应用上下文中的bean。

我们在home()方法上使用了@RequestMapping注解,它的value属性指定了这个方法所要处理的请求路径,method属性指定了它所处理的HTTP方法,在这里,当收到对“/”的HTTP GET请求时,就会调用home()方法。

另外home()方法返回一个String类型的”home”,这个String会被Spring MVC解读为要渲染的视图名称,DispatcherServlet会要求视图解析器将这个逻辑名称解析为实际的视图,在这里,根据配置,逻辑视图名会被解析为“/WEB-INF/views/home.jsp”。

我们可以定义一个简单的home.jsp:

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>主页</title>
</head>
<body>
    <h1>这是主页</h1>
</body>
</html>

接下来,我们对主页进行访问:

这里写图片描述

可以看到,我们在浏览器中访问的请求路径是http://localhost:8080/spring-mvc/,而实际上返回的是home.jsp的内容。

上面这个Controller中,我们是将@RequestMapping注解放在处理方法home()上面的,实际上@RequestMapping注解也可以定义在类级别上,我们修改一下HomeController:

@Controller
@RequestMapping(value="/")
public class HomeController {

    @RequestMapping(method=RequestMethod.GET)
    public String home() {
        return "home";
    }

}

其实上面的HomeController定义和之前定义的HomeController执行效果都是一样的。但是,当控制器在类级别上添加@RequestMapping注解的时候,这个注解会应用到控制器所有处理方法上,处理方法上的@RequestMapping注解会对类级别上的@RequestMapping的声明进行补充。

另外@RequestMapping的value属性能够接受一个String类型的数组,也就是说,它可以处理多个路径的请求,我们修改这个Controller:

@Controller
@RequestMapping(value={"/","/homepage"})
public class HomeController {
    @RequestMapping(method=RequestMethod.GET)
    public String home() {
        return "home";
    }
}

这样的话,我们访问http://localhost:8080/spring-mvc/http://localhost:8080/spring-mvc/homepage效果都是一样的。

2.2、传递模型数据到视图中

上面介绍的例子实现的功能很简单,只是访问一个页面,页面返回指定的输出,而实际应用中,经常需要访问页面时,页面能够根据后台业务逻辑计算的结果返回相应的输出,这就需要一个新的方法来处理这个页面,例如,我们需要知道每个班学生信息,就需要在访问页面时去数据库中查找相应的学生信息,然后将查询到的数据返回给页面显示,在这里学生信息就是模型数据,那我们该如何处理呢?我们新定义一个控制器StudentInfoController:

@Controller
public class StudentInfoController {
    @RequestMapping(value="/listStudentInfo",method=RequestMethod.GET)
    public String listStudentInfo(Model model) {
        Student s = new Student();
        model.addAttribute(s.getStudentList());

        return "studentInfo";
    }   
}

可以看到,listStudentInfo()方法中给定了一个Model作为参数,这样,listStudentInfo()方法就能将Student中获取到的学生列表信息填充到模型中了。Model实际上就是一个Map(也就是key-value对的集合),它会传递给视图,这样数据就能渲染到客户端了。在本例中,我们调用Model的addAttribute()方法时并没有指定key,那么key会根据值的对象类型推断确定。在本例中,因为值的对象类型为List< Student >,因此,key会推断为studentList。当然我们也可以显式地声明模型的key:

@Controller
public class StudentInfoController {
    @RequestMapping(value="/listStudentInfo",method=RequestMethod.GET)
    public String listStudentInfo(Model model) {
        Student s = new Student();
        model.addAttribute("studentList", s.getStudentList());

        return "studentInfo";
    }
}

如果希望使用非Spring类型的话,我们可以使用java.util.Map来代替Model:

@Controller
public class StudentInfoController {
    @RequestMapping(value="/listStudentInfo",method=RequestMethod.GET)
    public String listStudentInfo(Map model) {
        Student s = new Student();
        model.put("studentList", s.getStudentList());

        return "studentInfo";
    }   
}

我们还可以这样改写这个方法以实现同样的效果:

@RequestMapping(value="/studentInfo",method=RequestMethod.GET)
public List<Student> studentInfo(){
    Student s = new Student();

    return s.getStudentList();
}

在这里,我们既没有返回视图名称,也没有显式地设定模型,这个方法返回的是一个Student列表,当处理方法像这样返回对象或集合时,这个值会放到模型中,模型的key会根据其类型推断得出,在这里也就是studentList。而逻辑视图的名称将会根据请求路径推断得出,因为这个方法处理针对“/studentInfo”的GET请求,因而视图名称将会是studentInfo。

当视图是JSP的时候,模型数据会作为请求属性放到请求之中,这样在JSP中就可以通过JSTL获取到学生信息:

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body>
    <h1>学生信息</h1>
    <c:forEach items="${studentList }" var="student">
        ID:${student.id }<br/>
        姓名:${student.name }<br/>
        性别:${student.sex }<br/>
    </c:forEach>
</body>
</html>

我们再写一个Student类,用于模拟去数据库中查询学生信息:

public class Student {
    private int id;

    private String name;

    private String sex;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public Student(int id, String name, String sex) {
        this.id = id;
        this.name = name;
        this.sex = sex;
    }

    public Student() {
    }

    public List<Student> getStudentList() {
        List<Student> studentList = new ArrayList<Student>();

        Student s1 = new Student(1, "张三", "男");
        Student s2 = new Student(2, "李四", "女");
        Student s3 = new Student(3, "王五", "男");

        studentList.add(s1);
        studentList.add(s2);
        studentList.add(s3);
        return studentList;
    }   
}

这样,我们访问相应的控制器时,可以看到页面:
这里写图片描述

3、接受请求的输入

对于Web应用,客户端除了可以从服务器读取数据之外,还可以允许用户输入,将数据发送到服务器上。Spring MVC允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括:

  • 查询参数
  • 表单参数
  • 路径变量

3.1、处理查询参数

还是上面的获取学生信息的例子,假如我们需要通过ID查询指定学生信息,我们可以将学生ID作为查询参数传递给处理方法:

@RequestMapping(value="/queryStudentInfo",method=RequestMethod.GET)
public String queryStudentInfo(@RequestParam("id") int id, Model model) {
    Student s = new Student().getStudentById(id);

    if(null != s) {
        model.addAttribute("student", s);
    }
    return "student";
}

可以看到,在queryStudentInfo()方法中接收了一个int类型的参数id,这个参数使用了@RequestParam注解进行标注,这个注解表示请求参数中名为id的参数值将会传递给queryStudentInfo()方法的参数id,这样我们就可以获取到查询参数了。我们写一个简单的student.jsp作为页面显示:

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body>
    <h1>学生信息</h1>
    ID:${student.id }<br/>
    姓名:${student.name }<br/>
    性别:${student.sex }<br/>
</body>

这样我们就可以通过查询参数获取到学生信息了:

这里写图片描述

如果请求参数id不存在,我们可以通过@RequestParam的defaultValue属性设置参数的默认值,这样queryStudentInfo()方法就既可以处理有参数的查询也可以处理无参数的查询了。我们修改一下queryStudentInfo()方法:

@RequestMapping(value="/queryStudentInfo",method=RequestMethod.GET)
public String queryStudentInfo(@RequestParam(value="id",defaultValue="1") int id, Model model) {
    Student s = new Student().getStudentById(id);

    if(null != s) {
        model.addAttribute("student", s);
    }
    return "student";
}

在这里,defaultValue接受的是一个String类型的值,而我们需要的id是一个int类型的,Spring会将defaultValue的值转换为int类型。这样修改以后,即使我们不传递查询参数,也可以获取到一个id默认为1的学生信息:

这里写图片描述

3.2、通过路径参数接受输入

假设我们的应用程序需要根据学生ID获取到学生信息,一种方法就是上节介绍的通过@RequestParam注解获取查询参数,然后去后台进行查询,另一种方式我们可以通过路径参数获取。例如,我们要获取ID为2的学生信息,如果通过查询参数,我们需要访问“/queryStudentInfo?id=2”,而通过路径参数,我们只需要访问“/queryStudentInfo/2”即可,那么我们该如何获取到查询参数呢?我们修改上面的控制器方法:

@RequestMapping(value="/queryStudentInfo/{id}",method=RequestMethod.GET)
public String queryStudentInfo(@PathVariable("id") int id, Model model) {
    Student s = new Student().getStudentById(id);

    if(null != s) {
        model.addAttribute("student", s);
    }
    return "student";
}

在之前介绍的内容中,所有的方法都映射到了静态定义的路径上,但是要获取到路径参数,@RequestMapping注解中就需要包含变量部分,在这里我们可以使用占位符“{}”,路径中其他部分要与所处理的请求完全匹配,但是占位符部分可以是任意值。在这里,queryStudentInfo()方法的id参数上添加了@PathVariable(“id”)注解,这表明在请求路径中,不管占位符部分的值是什么都会传递到处理器方法的id参数中。这样,我们访问“/queryStudentInfo/2”即可获取到ID为2的学生信息:

这里写图片描述

因为上面方法的参数名恰巧与占位符的名称相同,因此我们可以去掉@PathVariable中的value属性:

@RequestMapping(value="/queryStudentInfo/{id}",method=RequestMethod.GET)
public String queryStudentInfo(@PathVariable int id, Model model) {
    Student s = new Student().getStudentById(id);

    if(null != s) {
        model.addAttribute("student", s);
    }
    return "student";
}

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

3.3、处理表单参数

很多时候,我们需要用户在浏览器中输入一些信息,服务器接收到这些信息之后可以进行一系列的逻辑处理,然后将处理结果返回给用户,通常我们通过表单的方式与用户进行交互。现在,假设我们需要用户在页面录入学生信息,录入之后,再将用户录入的信息返回展示给用户,这样我们就模拟了一个服务器与客户端交互的过程。我们新写一个录入学生信息的表单:

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body>
    <h1>请录入学生信息</h1>
    <form action="addStudent" method="post">
        ID:<input type="text" name="id"/><br/>
        姓名:<input type="text" name="name"/><br/>
        性别:<input type="text" name="sex"/><br/>
        <input type="submit" value="提交"/>
    </form>
</body>
</html>

接下来我们写一个处理器方法来接收表单参数:

@RequestMapping(value="/addStudent",method=RequestMethod.POST)
public String addStudent(Student s, Model model) {
    studentList.add(s);

    return "redirect:showStudent";
}

可以看到,这个方法接收一个Student类型的参数,这个参数的id、name、sex属性会使用请求中同名的参数进行填充,也就是说,如果处理器方法的参数中包含与表单参数同名的属性,这些属性的值将与表单参数进行绑定。

我们还可以看到,这个方法返回的是redirect:showStudent,当InternalResourceViewResolver看到视图的格式中的“redirect:”前缀时,他就知道要将其解析为重定向的规则,而不是视图的名称,视图解析器会重定向到“redirect:”指定的控制器的处理方法上,类似的还有“forward:”前缀,请求将会转发给“forward:”指定路径的控制器处理方法上。

我们可以运行上面的代码进行测试:

这里写图片描述

我们录入一个ID为4的学生信息,点击提交,页面显示刚刚新增的学生信息:

这里写图片描述

从上面的实例我们可以看到,当编写控制器的处理器方法时,Spring MVC及其灵活,概括来讲,如果你的处理器方法需要内容的话,只需将对应的对象作为参数,而它不需要的内容,则没有必要出现在参数列表中。

猜你喜欢

转载自blog.csdn.net/u011024652/article/details/80314629