阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

使用Spring MVC

33次阅读
没有评论

共计 7565 个字符,预计需要花费 19 分钟才能阅读完成。

我们已经介绍了 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,我们写出来的代码类似:

@Controller
public class UserController {@GetMapping("/register")
    public ModelAndView register() {...}

    @PostMapping("/signin")
    public ModelAndView signin(@RequestParam("email") String email, @RequestParam("password") String password) {...}
    ...
}

但是,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 工程目录结构如下:

spring-web-mvc
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── itranswarp
        │           └── learnjava
        │               ├── AppConfig.java
        │               ├── DatabaseInitializer.java
        │               ├── entity
        │               │   └── User.java
        │               ├── service
        │               │   └── UserService.java
        │               └── web
        │                   └── UserController.java
        ├── resources
        │   ├── jdbc.properties
        │   └── logback.xml
        └── webapp
            ├── WEB-INF
            │   ├── templates
            │   │   ├── _base.html
            │   │   ├── index.html
            │   │   ├── profile.html
            │   │   ├── register.html
            │   │   └── signin.html
            │   └── web.xml
            └── static
                ├── css
                │   └── bootstrap.css
                └── js
                    └── 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 的默认查找的配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<appender name="STDOUT"
		class="ch.qos.logback.core.ConsoleAppender">
		<layout class="ch.qos.logback.classic.PatternLayout">
			<Pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</Pattern>
		</layout>
	</appender>

	<logger name="com.itranswarp.learnjava" level="info" additivity="false">
		<appender-ref ref="STDOUT" />
	</logger>

	<root level="info">
		<appender-ref ref="STDOUT" />
	</root>
</configuration>

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

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

配置 Spring MVC

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

@Configuration
@ComponentScan
@EnableWebMvc // 启用 Spring MVC
@EnableTransactionManagement
@PropertySource("classpath:/jdbc.properties")
public class AppConfig {...}

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

@Bean
WebMvcConfigurer createWebMvcConfigurer() {return new WebMvcConfigurer() {@Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/static/**").addResourceLocations("/static/");
        }
    };
}

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

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

@Bean
ViewResolver createViewResolver(@Autowired ServletContext servletContext) {var engine = new PebbleEngine.Builder().autoEscaping(true)
            // cache:
            .cacheActive(false)
            // loader:
            .loader(new Servlet5Loader(servletContext))
            .build();
    var viewResolver = new PebbleViewResolver(engine);
    viewResolver.setPrefix("/WEB-INF/templates/");
    viewResolver.setSuffix("");
    return viewResolver;
}

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

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

// Controller 使用 @Controller 标记而不是 @Component:
@Controller
public class UserController {// 正常使用 @Autowired 注入:
    @Autowired
    UserService userService;

    // 处理一个 URL 映射:
    @GetMapping("/")
    public ModelAndView index() {...}
    ...
}

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

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

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

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

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

<?xml version="1.0"?>
<web-app>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
        </init-param>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.itranswarp.learnjava.AppConfig</param-value>
        </init-param>
        <load-on-startup>0</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</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:

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

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

使用 Spring MVC

编写 Controller

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

总是标记 @Controller 而不是@Component

@Controller
public class UserController {...}

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

@PostMapping("/signin")
public ModelAndView doSignin(@RequestParam("email") String email,
        @RequestParam("password") String password,
        HttpSession session) {...}

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

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

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

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

public String index() {if (...) {return "redirect:/signin";
    } else {return "redirect:/profile";
    }
}

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

public ModelAndView download(HttpServletResponse response) {byte[] data = ...
    response.setContentType("application/octet-stream");
    OutputStream output = response.getOutputStream();
    output.write(data);
    output.flush();
    return null;
}

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

@Controller
@RequestMapping("/user")
public class UserController {// 注意实际 URL 映射是 /user/profile
    @GetMapping("/profile")
    public ModelAndView profile() {...}

    // 注意实际 URL 映射是 /user/changePassword
    @GetMapping("/changePassword")
    public ModelAndView changePassword() {...}
}

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

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

练习

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

下载练习

小结

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

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

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

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-08-05发表,共计7565字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中