使用Spring MVC


我们在前面介绍Web开发时已经讲过了Java Web的基础:Servlet容器,以及标准的Servlet组件:

  • Servlet:能处理HTTP请求并将HTTP响应返回;
  • JSP:一种嵌套Java代码的HTML,将被编译为Servlet;
  • Filter:能过滤指定的URL以实现拦截功能;
  • Listener:监听指定的事件,如ServletContext、HttpSession的创建和销毁。

此外,Servlet容器为每个Web应用程序自动创建一个唯一的ServletContext实例,这个实例就代表了Web应用程序本身。

MVC高级开发中,我们手撸了一个MVC框架,接口和Spring MVC类似。如果直接使用Spring MVC,我们写出来的代码类似:

  1. @Controller
  2. public class UserController {
  3. @GetMapping("/register")
  4. public ModelAndView register() {
  5. ...
  6. }
  7. @PostMapping("/signin")
  8. public ModelAndView signin(@RequestParam("email") String email, @RequestParam("password") String password) {
  9. ...
  10. }
  11. ...
  12. }

但是,Spring提供的是一个IoC容器,所有的Bean,包括Controller,都在Spring IoC容器中被初始化,而Servlet容器由JavaEE服务器提供(如Tomcat),Servlet容器对Spring一无所知,他们之间到底依靠什么进行联系,又是以何种顺序初始化的?

在理解上述问题之前,我们先把基于Spring MVC开发的项目结构搭建起来。首先创建基于Web的Maven工程,引入如下依赖:

  • org.springframework:spring-context:6.0.0
  • org.springframework:spring-webmvc:6.0.0
  • org.springframework:spring-jdbc:6.0.0
  • jakarta.annotation:jakarta.annotation-api:2.1.1
  • io.pebbletemplates:pebble-spring6:3.2.0
  • ch.qos.logback:logback-core:1.4.4
  • ch.qos.logback:logback-classic:1.4.4
  • com.zaxxer:HikariCP:5.0.1
  • org.hsqldb:hsqldb:2.7.0

以及provided依赖:

  • org.apache.tomcat.embed:tomcat-embed-core:10.1.1
  • org.apache.tomcat.embed:tomcat-embed-jasper:10.1.1

这个标准的Maven Web工程目录结构如下:

  1. spring-web-mvc
  2. ├── pom.xml
  3. └── src
  4. └── main
  5. ├── java
  6. └── com
  7. └── itranswarp
  8. └── learnjava
  9. ├── AppConfig.java
  10. ├── DatabaseInitializer.java
  11. ├── entity
  12. └── User.java
  13. ├── service
  14. └── UserService.java
  15. └── web
  16. └── UserController.java
  17. ├── resources
  18. ├── jdbc.properties
  19. └── logback.xml
  20. └── webapp
  21. ├── WEB-INF
  22. ├── templates
  23. ├── _base.html
  24. ├── index.html
  25. ├── profile.html
  26. ├── register.html
  27. └── signin.html
  28. └── web.xml
  29. └── static
  30. ├── css
  31. └── bootstrap.css
  32. └── js
  33. └── jquery.js

其中,src/main/webapp是标准web目录,WEB-INF存放web.xml,编译的class,第三方jar,以及不允许浏览器直接访问的View模版,static目录存放所有静态文件。

src/main/resources目录中存放的是Java程序读取的classpath资源文件,除了JDBC的配置文件jdbc.properties外,我们又新增了一个logback.xml,这是Logback的默认查找的配置文件:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration>
  3. <appender name="STDOUT"
  4. class="ch.qos.logback.core.ConsoleAppender">
  5. <layout class="ch.qos.logback.classic.PatternLayout">
  6. <Pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</Pattern>
  7. </layout>
  8. </appender>
  9. <logger name="com.itranswarp.learnjava" level="info" additivity="false">
  10. <appender-ref ref="STDOUT" />
  11. </logger>
  12. <root level="info">
  13. <appender-ref ref="STDOUT" />
  14. </root>
  15. </configuration>

上面给出了一个写入到标准输出的Logback配置,可以基于上述配置添加写入到文件的配置。

src/main/java中就是我们编写的Java代码了。

配置Spring MVC

和普通Spring配置一样,我们编写正常的AppConfig后,只需加上@EnableWebMvc注解,就“激活”了Spring MVC:

  1. @Configuration
  2. @ComponentScan
  3. @EnableWebMvc // 启用Spring MVC
  4. @EnableTransactionManagement
  5. @PropertySource("classpath:/jdbc.properties")
  6. public class AppConfig {
  7. ...
  8. }

除了创建DataSourceJdbcTemplatePlatformTransactionManager外,AppConfig需要额外创建几个用于Spring MVC的Bean:

  1. @Bean
  2. WebMvcConfigurer createWebMvcConfigurer() {
  3. return new WebMvcConfigurer() {
  4. @Override
  5. public void addResourceHandlers(ResourceHandlerRegistry registry) {
  6. registry.addResourceHandler("/static/**").addResourceLocations("/static/");
  7. }
  8. };
  9. }

WebMvcConfigurer并不是必须的,但我们在这里创建一个默认的WebMvcConfigurer,只覆写addResourceHandlers(),目的是让Spring MVC自动处理静态文件,并且映射路径为/static/**

另一个必须要创建的Bean是ViewResolver,因为Spring MVC允许集成任何模板引擎,使用哪个模板引擎,就实例化一个对应的ViewResolver

  1. @Bean
  2. ViewResolver createViewResolver(@Autowired ServletContext servletContext) {
  3. var engine = new PebbleEngine.Builder().autoEscaping(true)
  4. // cache:
  5. .cacheActive(false)
  6. // loader:
  7. .loader(new Servlet5Loader(servletContext))
  8. .build();
  9. var viewResolver = new PebbleViewResolver(engine);
  10. viewResolver.setPrefix("/WEB-INF/templates/");
  11. viewResolver.setSuffix("");
  12. return viewResolver;
  13. }

ViewResolver通过指定prefixsuffix来确定如何查找View。上述配置使用Pebble引擎,指定模板文件存放在/WEB-INF/templates/目录下。

剩下的Bean都是普通的@Component,但Controller必须标记为@Controller,例如:

  1. // Controller使用@Controller标记而不是@Component:
  2. @Controller
  3. public class UserController {
  4. // 正常使用@Autowired注入:
  5. @Autowired
  6. UserService userService;
  7. // 处理一个URL映射:
  8. @GetMapping("/")
  9. public ModelAndView index() {
  10. ...
  11. }
  12. ...
  13. }

如果是普通的Java应用程序,我们通过main()方法可以很简单地创建一个Spring容器的实例:

  1. public static void main(String[] args) {
  2. var context = new AnnotationConfigApplicationContext(AppConfig.class);
  3. }

但是问题来了,现在是Web应用程序,而Web应用程序总是由Servlet容器创建,那么,Spring容器应该由谁创建?在什么时候创建?Spring容器中的Controller又是如何通过Servlet调用的?

在Web应用中启动Spring容器有很多种方法,可以通过Listener启动,也可以通过Servlet启动,可以使用XML配置,也可以使用注解配置。这里,我们只介绍一种最简单的启动Spring容器的方式。

第一步,我们在web.xml中配置Spring MVC提供的DispatcherServlet

  1. <?xml version="1.0"?>
  2. <web-app>
  3. <servlet>
  4. <servlet-name>dispatcher</servlet-name>
  5. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  6. <init-param>
  7. <param-name>contextClass</param-name>
  8. <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
  9. </init-param>
  10. <init-param>
  11. <param-name>contextConfigLocation</param-name>
  12. <param-value>com.itranswarp.learnjava.AppConfig</param-value>
  13. </init-param>
  14. <load-on-startup>0</load-on-startup>
  15. </servlet>
  16. <servlet-mapping>
  17. <servlet-name>dispatcher</servlet-name>
  18. <url-pattern>/*</url-pattern>
  19. </servlet-mapping>
  20. </web-app>

初始化参数contextClass指定使用注解配置的AnnotationConfigWebApplicationContext,配置文件的位置参数contextConfigLocation指向AppConfig的完整类名,最后,把这个Servlet映射到/*,即处理所有URL。

上述配置可以看作一个样板配置,有了这个配置,Servlet容器会首先初始化Spring MVC的DispatcherServlet,在DispatcherServlet启动时,它根据配置AppConfig创建了一个类型是WebApplicationContext的IoC容器,完成所有Bean的初始化,并将容器绑到ServletContext上。

因为DispatcherServlet持有IoC容器,能从IoC容器中获取所有@Controller的Bean,因此,DispatcherServlet接收到所有HTTP请求后,根据Controller方法配置的路径,就可以正确地把请求转发到指定方法,并根据返回的ModelAndView决定如何渲染页面。

最后,我们在AppConfig中通过main()方法启动嵌入式Tomcat:

  1. public static void main(String[] args) throws Exception {
  2. Tomcat tomcat = new Tomcat();
  3. tomcat.setPort(Integer.getInteger("port", 8080));
  4. tomcat.getConnector();
  5. Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
  6. WebResourceRoot resources = new StandardRoot(ctx);
  7. resources.addPreResources(
  8. new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/"));
  9. ctx.setResources(resources);
  10. tomcat.start();
  11. tomcat.getServer().await();
  12. }

上述Web应用程序就是我们使用Spring MVC时的一个最小启动功能集。由于使用了JDBC和数据库,用户的注册、登录信息会被持久化:

spring-mvc

编写Controller

有了Web应用程序的最基本的结构,我们的重点就可以放在如何编写Controller上。Spring MVC对Controller没有固定的要求,也不需要实现特定的接口。以UserController为例,编写Controller只需要遵循以下要点:

总是标记@Controller而不是@Component

  1. @Controller
  2. public class UserController {
  3. ...
  4. }

一个方法对应一个HTTP请求路径,用@GetMapping@PostMapping表示GET或POST请求:

  1. @PostMapping("/signin")
  2. public ModelAndView doSignin(
  3. @RequestParam("email") String email,
  4. @RequestParam("password") String password,
  5. HttpSession session) {
  6. ...
  7. }

需要接收的HTTP参数以@RequestParam()标注,可以设置默认值。如果方法参数需要传入HttpServletRequestHttpServletResponse或者HttpSession,直接添加这个类型的参数即可,Spring MVC会自动按类型传入。

返回的ModelAndView通常包含View的路径和一个Map作为Model,但也可以没有Model,例如:

  1. return new ModelAndView("signin.html"); // 仅View,没有Model

返回重定向时既可以写new ModelAndView("redirect:/signin"),也可以直接返回String:

  1. public String index() {
  2. if (...) {
  3. return "redirect:/signin";
  4. } else {
  5. return "redirect:/profile";
  6. }
  7. }

如果在方法内部直接操作HttpServletResponse发送响应,返回null表示无需进一步处理:

  1. public ModelAndView download(HttpServletResponse response) {
  2. byte[] data = ...
  3. response.setContentType("application/octet-stream");
  4. OutputStream output = response.getOutputStream();
  5. output.write(data);
  6. output.flush();
  7. return null;
  8. }

对URL进行分组,每组对应一个Controller是一种很好的组织形式,并可以在Controller的class定义出添加URL前缀,例如:

  1. @Controller
  2. @RequestMapping("/user")
  3. public class UserController {
  4. // 注意实际URL映射是/user/profile
  5. @GetMapping("/profile")
  6. public ModelAndView profile() {
  7. ...
  8. }
  9. // 注意实际URL映射是/user/changePassword
  10. @GetMapping("/changePassword")
  11. public ModelAndView changePassword() {
  12. ...
  13. }
  14. }

实际方法的URL映射总是前缀+路径,这种形式还可以有效避免不小心导致的重复的URL映射。

可见,Spring MVC允许我们编写既简单又灵活的Controller实现。

练习

在注册、登录等功能的基础上增加一个修改口令的页面。

使用Spring MVC - 图2下载练习:使用Spring MVC (推荐使用IDE练习插件快速下载)

小结

使用Spring MVC时,整个Web应用程序按如下顺序启动:

  1. 启动Tomcat服务器;
  2. Tomcat读取web.xml并初始化DispatcherServlet
  3. DispatcherServlet创建IoC容器并自动注册到ServletContext中。

启动后,浏览器发出的HTTP请求全部由DispatcherServlet接收,并根据配置转发到指定Controller的指定方法处理。

读后有收获可以支付宝请作者喝咖啡:

使用Spring MVC - 图3