初识Spring MVC
SpringMVC 起步
SpringMVC基于模型-视图-通知器(Model-View-Controller,MVC)模式实现的,它能够帮助我们构建像Spring框架那样灵活和松耦合的Web应用程序。
SpringMVC 运行流程:
流程说明:
- 用户发送请求到前端控制器(DispatchServlet),该控制器会过滤出哪些请求可以访问servlet、哪些不能访问。就是URL-Pattern的作用,并且会加载SpringMVC.xml配置文件。
- 前端控制器会找到处理器映射器(HandlerMapping),通过HandlerMapping完成URL到Controller映射的组件,简单来说,就是将在SpringMVC.xml中配置的或者注解的URL与对应的处理类找到并进行存储,用
Map
这样的方式来存储。 - HandlerMapping有了映射关系,并且找到了URL对应的处理器,HandlerMapping就会将其处理器(Handler)返回,在放回前,会加上很多拦截器。
- DispatchServlet拿到Handler之后,找到HandlerAdapter(处理器适配器),通过它来访问处理器,并执行处理器。
- 执行处理器。
- 处理器会返回一个ModelAndView对象到HandlerAdapter。
- 通过HandlerAdapter将ModelAndView对象返回到前端控制器DispatchServlet。
- 前端控制器请求视图解析器(ViewResolver)去进行视图解析,根据逻辑视图名解析成真真的视图(JSP),其实就是将ModelAndView对象中存放视图的名称进行查找,找到对应的页面形成视图对象。
- 返回视图对象到前端控制器。
- 视图渲染,就是将ModelAndView对象中的数据放到Request域中,用来让页面加载数据的。
- 通过第8步,通过名称找到了对应的页面,通过第10步,request域中有了所需要的数据,那么就可以进行视图渲染了。最后将其返回即可。
搭建SpringMVC
java配置方式:
配置类:
public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
//配置Servlet的映射
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
//返回的配置类用来配置ContextLoaderListener创建的spring上下文的bean主要加载后端的中间层和数据层 即原来的web.xml
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfig.class};
}
@Override
//返回的配置类用来配置DispatcherServlet创建的应用上下文的bean主要加载web层 即原来的spring-mvc.xml
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
}
AbstractAnnotationConfigDispatcherServletInitializer会自动的配置DispatcherServlet和Spring应用上下文(ContextLoaderListener)
Servlet 3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果能发现的话,就会用它来配置Servlet容器。
Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实WebApplicationInitializer的类并将配置的任务交给它们来完成。
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {}
Spring 3.2引入了一个WebApplicationInitializer基础实现,也就是AbstractAnnotationConfigDispatcherServletInitializer因为我们的SpittrWebAppInitializer扩展了AbstractAnnotationConfigDispatcherServletInitializer(同时也就实现了WebApplicationInitializer)因此当部署到Servlet 3.0容器中的时候,容器会自动发现它,并用它来配置Servlet上下文。
SpittrWebAppInitializer重写了三个方法:
@Override
//配置Servlet的映射
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
//返回的配置类用来配置ContextLoaderListener创建的spring上下文的bean主要加载后端的中间层和数据层 即原来的web.xml
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfig.class};
}
@Override
//返回的配置类用来配置DispatcherServlet创建的应用上下文的bean主要加载web层 即原来的spring-mvc.xml
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
注意:java配置方式只能部署在Servlet 3.0的服务器中才能正常工作,如Tomcat 7或更高版本。
WebApplicationInitializer源码文档:
The code-based approach with {@code WebApplicationInitializer}
* Here is the equivalent {@code DispatcherServlet} registration logic,
* {@code WebApplicationInitializer}-style:
译:
通过使用WebApplicationInitializer的基于代码的方式,其与xml是一个相同的注册逻辑。WebApplicationInitializer代码方式:
* public class MyWebAppInitializer implements WebApplicationInitializer {
* @Override
* public void onStartup(ServletContext container) {
* XmlWebApplicationContext appContext = new XmlWebApplicationContext();
* appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
*
* ServletRegistration.Dynamic dispatcher =
* container.addServlet("dispatcher", new
DispatcherServlet(appContext));
* dispatcher.setLoadOnStartup(1);
* dispatcher.addMapping("/");
* }
* }
*
* As an alternative to the above, you can also extend from {@link
org.springframework.web.servlet.support.AbstractDispatcherServletInitializer}.
译:你也可以通过集成自AbstractDispatcherServletInitializer来实现,以上二者任选其一
* As you can see, thanks to Servlet 3.0's new {@link ServletContext#addServlet} method we're actually registering an instance of the {@code DispatcherServlet}, and this means that the {@code DispatcherServlet} can now be treated like any other object receiving constructor injection of its application context in this case.
译:如你所见,正是由于servlet3.0 的新方法(ServletContext#addServlet)出现,我们可以通过它注册一个DispatchServlet的实例了。这也意味着DispatchServlet可以像其他对象那
样,通过构造注入的方式接受一个Application Context
This style is both simpler and more concise. There is no concern for dealing with init-params, etc, just normal JavaBean-style properties and constructor arguments. You are free to create and work with your Spring application contexts as necessary before injecting them into the {@code DispatcherServlet}.
译:这种方式既简单又简洁。我们不需要如何去处理初始化参数,等,仅仅需要处理像普通JavaBean那样的属性和构造方法参数。在你必须将他们注入到DispatchServlet中之前,你可以很自由的构建和处理你的spring应用程序。
Most major Spring Web components have been updated to support this style of registration. You'll find that {@code DispatcherServlet}, {@code FrameworkServlet},{@code ContextLoaderListener} and {@code DelegatingFilterProxy} all now support constructor arguments. Even if a component (e.g. non-Spring, other third party) has not been specifically updated for use within {@code WebApplicationInitializers}, they still
may be used in any case. The Servlet 3.0 {@code ServletContext} API allows for setting init-params, context-params, etc programmatically.
译:大部分Spring web 组件都进行了更新,以至于能够支持这个注册方式,你可以发现像 DispatcherServlet、FrameworkServlet、ContextLoaderListener和DelegatingFilterProxy ,现在全部都支持构造参数注入。甚至是一些非spring,其他第三方组织的组件在WebApplicationInitializers中使用的没有支持的也会更新,以至于一直可以使用。Servlet3.0 API 也可以通过编程的方式 去设置 初始化参数、容器参数等
A 100% code-based approach to configuration In the example above,
{@code WEB-INF/web.xml} was successfully replaced with code in
* the form of a {@code WebApplicationInitializer}, but the actual
* {@code dispatcher-config.xml} Spring configuration remained XML-based.
* {@code WebApplicationInitializer} is a perfect fit for use with Spring's code-based
* {@code @Configuration} classes.
See @{@link org.springframework.context.annotation.Configuration Configuration} Javadoc for complete details, but the following example demonstrates refactoring to use Spring's {@link org.springframework.web.context.support.AnnotationConfigWebApplicationContext AnnotationConfigWebApplicationContext} in lieu of {@code XmlWebApplicationContext}, and user-defined {@code @Configuration} classes {@code AppConfig} and {@code DispatcherConfig} instead of Spring XML files.
译 :上面是一个完全由代码的配置方式的案例,web.xml文件已经被一个WebApplicationInitializer 成功取代。当然,实际上 Spring的配置任然保持着基于xml方式,WebApplicationInitializer 是一个更加适合 Spring代码的 配置类。可以查看Configuration的文档,了解完整的详情。但是下面这个例子是使用Spring的 AnnotationConfigWebApplicationContext 来代替XmlWebApplicationContext进行重构的。并且用户通过@Configuration,AppConfig,DispatcherConfig来代替xml配置文件。
This example also goes a bit beyond those above to demonstrate typical configuration of the 'root' application context and registration of the {@code ContextLoaderListener}:
译:这个例子比上面的例子更进一步,使用root application context 来重构的典型配置,并且注册ContextLoaderListener
* public class MyWebAppInitializer implements WebApplicationInitializer {
* @Override
* public void onStartup(ServletContext container) {
* // Create the 'root' Spring application context
创建root Spring的应用上下文
* AnnotationConfigWebApplicationContext rootContext =
* new AnnotationConfigWebApplicationContext();
* rootContext.register(AppConfig.class);
* // Manage the lifecycle of the root application context
管理root 应用上下文的生命周期
* container.addListener(new ContextLoaderListener(rootContext));
* // Create the dispatcher servlet's Spring application context
// 创建了Servlet的前端控制器的Spring应用上下文
* AnnotationConfigWebApplicationContext dispatcherContext =
* new AnnotationConfigWebApplicationContext();
* dispatcherContext.register(DispatcherConfig.class);
* // Register and map the dispatcher servlet
//注册并且映射前端控制器
* ServletRegistration.Dynamic dispatcher =
* container.addServlet("dispatcher", new
DispatcherServlet(dispatcherContext));
* dispatcher.setLoadOnStartup(1);
* dispatcher.addMapping("/");
* }
* }
*
* As an alternative to the above, you can also extend from {@link
org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer}.
你也可以通过集成自AbstractAnnotationConfigDispatcherServletInitializer,以上二者任选其一。
Remember that {@code WebApplicationInitializer} implementations are
detected automatically -- so you are free to package them within your application as you see fit.
记住,WebApplicationInitializer 的实现类 都是自动识别的。所以,你可以自由的将他们打包到你认为合适的应用程序中。
Ordering {@code WebApplicationInitializer} execution
{@code WebApplicationInitializer} implementations may optionally be annotated at the
* class level with Spring's @{@link org.springframework.core.annotation.Order Order}
* annotation or may implement Spring's {@link org.springframework.core.Ordered Ordered}
* interface. If so, the initializers will be ordered prior to invocation. This provides
* a mechanism for users to ensure the order in which servlet container initialization
* occurs. Use of this feature is expected to be rare, as typical applications will likely
* centralize all container initialization within a single {@code WebApplicationInitializer}.
Caveats :说明、警告
web.xml versioning
web.xml版本
{@code WEB-INF/web.xml} and {@code WebApplicationInitializer} use are not mutually exclusive; for example, web.xml can register one servlet, and a {@code WebApplicationInitializer} can register another. An initializer can even modify registrations performed in {@code web.xml} through methods such as {@link ServletContext#getServletRegistration(String)}.
译:web.xml与WebApplicationInitializer不会相互排斥,例如,web.xml可注册一个Servlet,而WebApplicationInitializer可以注册另一个,后者甚至可以修改在xml中注册的。
However, if {@code WEB-INF/web.xml} is present in the application, its {@code version} attribute must be set to "3.0" or greater, otherwise {@code ServletContainerInitializer} bootstrapping will be ignored by the servlet container
译:然而,如果web.xml存在于应用中,那么它的版本必须在3.0或以上,否则ServletContainerInitializer 自己化启动将会被Servlet容器忽略。
* <h3>Mapping to '/' under Tomcat</h3>、
在tomcat下映射/
* <p>Apache Tomcat maps its internal {@code DefaultServlet} to "/", and on Tomcat versions
<= 7.0.14, this servlet mapping cannot be overridden programmatically
7.0.15 fixes this issue. Overriding the "/" servlet mapping has also been tested successfully under GlassFish 3.1
译:在tomcat7.0.14及以下,tomcat使用其自身的DefaultServlet去映射/,这个Servlet映射将不能被编程方式重写在7.0.15版本。在GlassFish 3.1下, 成功测试出可以重写/这个Servlet映射器。
*/
public interface WebApplicationInitializer {
/**
* Configure the given {@link ServletContext} with any servlets, filters, listeners context-params and attributes necessary for initializing this web application.
为你这个web容器配置任何必要的 filter,listener,servlet,及其参数和属性
See examples {@linkplain WebApplicationInitializer above}.
* @param servletContext the {@code ServletContext} to initialize
* @throws ServletException if any call against the given {@code ServletContext}
* throws a {@code ServletException}
*/
void onStartup(ServletContext servletContext) throws ServletException;
}
WebConfig:
@Configuration
@EnableWebMvc
//@EnableWebMvc 相当于之前在springmvc.xml文件中配置的<mvc:annotation-driven>
@ComponentScan("chendongdong.spring.test.bean.Test5.web") //启动组件扫描
public class WebConfig implements WebMvcConfigurer{
/*配置JSP视图解析器*/
@Bean
public ViewResolver viewResolver(){
InternalResourceViewResolver resolver =
new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");//前缀
resolver.setSuffix(".jsp");//后缀
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}
/*配置静态资源的处理*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
RootConfig:
@Configuration
//扫描基础包,使用默认的过滤器。即全部都扫描。排除标有Controller注解的。
@ComponentScan(basePackages = "chendongdong.spring.test.bean.Test5", useDefaultFilters = true,excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = EnableWebMvc.class)
})
public class RootConfig {
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>chendongdong.spring.test</groupId>
<artifactId>SpringTest</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>SpringTest</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.8.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>3.0-alpha-1</version>
</dependency>
</dependencies>
</project>
SpittrController
@Controller
public class SpittrController {
@RequestMapping("/PrintSpttr")
@ResponseBody
public String PrintSpttr(){
System.out.println("==== PrintSpttr ====");
return "==== PrintSpttr ====";
}
}
测试:
xml配置方式:
接受请求参数
处理查询参数:
Controller:
@Controller
@RequestMapping({"/SpittrController","/Spittr","/"})
public class SpittrController {
@RequestMapping("/PrintSpttr")
@ResponseBody
public String PrintSpttr(@RequestParam(value = "print",defaultValue = "陈菲菲") String print){
System.out.println(print);
return print;
}
}
测试:
解决方案:
org.springframework.http.converter.StringHttpMessageConverter
. 打开源码可以发现:
默认编码已经被设定为了 Charset.forName(“ISO-8859-1”);
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
// 省略 ....
public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1");
private final List<Charset> availableCharsets;
private boolean writeAcceptCharset = true;
public StringHttpMessageConverter(Charset defaultCharset) {
super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);
this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values());
}
// 省略 ....
}
找到配置SpringMVC的类 WebMvcConfigurerAdapter
public interface WebMvcConfigurer {
/**
* Configure the {@link HttpMessageConverter}s to use for reading or writing
* to the body of the request or response. If no converters are added, a
* default list of converters is registered.
* <p><strong>Note</strong> that adding converters to the list, turns off
* default converter registration. To simply add a converter without impacting
* default registration, consider using the method
* {@link #extendMessageConverters(java.util.List)} instead.
* @param converters initially an empty list of converters
*/
void configureMessageConverters(List<HttpMessageConverter<?>> converters);
/**
* A hook for extending or modifying the list of converters after it has been
* configured. This may be useful for example to allow default converters to
* be registered and then insert a custom converter through this method.
* @param converters the list of configured converters to extend.
* @since 4.1.3
*/
void extendMessageConverters(List<HttpMessageConverter<?>> converters);
}
configureMessageConverters,extendMessageConverters区别:前者覆盖之前默认的编码,后者扩展编码。所以我们只需要覆盖默认的编码就好了。
@Configuration
@EnableWebMvc
//@EnableWebMvc 相当于之前在springmvc.xml文件中配置的<mvc:annotation-driven>
@ComponentScan("chendongdong.spring.test.bean.Test5.web") //启动组件扫描
public class WebConfig implements WebMvcConfigurer{
/*全局修改输出为UTF-8编码*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
stringHttpMessageConverter.setWriteAcceptCharset(false);
converters.add(stringHttpMessageConverter);
}
}
xml配置方式:
<!-- utf-8编码 -->
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg value="UTF-8" />
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
配置后再次测试:
处理通过路径参数:
@RequestMapping("/Print/{print}")
@ResponseBody
public String Print(@PathVariable String print){
System.out.println(print);
return print;
}
测试:
处理表单
register.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%--
Created by IntelliJ IDEA.
User: ouYang
Date: 2019/9/1
Time: 10:29
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Blog</title>
</head>
<body>
<h1>欢迎加入Spring的大家庭</h1>
<form method="post" >
用户名:<input name="username" type="text" ><br>
密码:<input name="password" type="password" ><br>
年龄:<input name="age" type="number" ><br>
<input type="submit" value="提交"><br>
</form>
</body>
</html>
一个普通的表单,使用post请求提交,当我们没有指定action属性的时候,点击提交之后,会使用原地址,即请求表单页面的地址,通过post请求提交,本例中是/register. 表单包括 用户名,密码和年龄三个属性。后台进行相应的Bean接受(User)
user:
public class User implements Serializable {
private String username;
private String password;
private Integer age;
//all/noArgConstructor, getter and setter, toString
}
UserService :
@Service
public class UserService {
private static List<User> users = new ArrayList<>();
public void saveUser(User user){
users.add(user);
}
}
IndexController :
@Controller
public class IndexController {
@Autowired
private UserService userService;
@GetMapping("/")
public String home(){
return "home";
}
@GetMapping("/register")
public String toRegister(){
return "register";
}
/*处理表单数据,并验证*/
@PostMapping("/register")
public String register(User user){
userService.saveUser(user);
return "redirect:/registerSuccess";
}
@GetMapping("/registerSuccess")
public String registerSuccess(){
return "registerSuccess";
}
@GetMapping("/registerFail")
public String registerFail(){
return "registerFail";
}
}
通过上面代码可以看出,通过get
请求类型,请求路径为/register
,通过返回逻辑视图名,DispatchServlet将逻辑视图名去查找视图,并渲染返回,就可以进入注册表单页面。显而易见,前端提交的参数会自动填写到User对象中,这就是SpringMVC的强大之处,可以将数据自动与实体进行映射。