构建Spring Web应用程序 -- 《Spring In Action》

Spring MVC概述


在Spring MVC中,DispatcherServlet就是前端控制器,所有的请求都会经过前端控制器。
DispatcherServlet: 将请求发送给Spring MVC控制器(Controller)。
Spring MVC控制器: 用于处理请求的Spring组件。

应用程序中有多个控制器,DispatcherServlet根据请求映射的URL信息将请求发送给指定控制器。
控制器的最后一件事: 将模型数据打包,并标示出用于渲染输出的视图名。
控制器不和特定的视图相耦合,传递给DispatcherServlet的视图名并不直接标示某个特定的前段文件(eg: JSP、HTML等)
DispatcherServlet利用视图解析器(View Resolver)来讲逻辑视图名匹配为一个特定的视图实现。(控制器将请求连同模型和视图名发送回DispatcherServlet)
DispatcherServlet以会查询一个或多个处理器映射(handler mapping)来确定请求的下一站在哪里。
Model模型:控制器在完成逻辑处理后产生的信息需要返回给用户并在浏览器上显示。🧶

使用Spring MVC

核心: 使用Spring MVC控制器处理Web请求

配置DispatcherServlet通过Java类进行配置

DispatcherServlet作为Spring MVC核心,主要负责将所有的请求路由到其他的组件之中。

package spitter.config // 应用名:spitter;包名:spitter.config
import org.springframework.web.servlet.support.
AbstractAnnotationConfigDispatcherServletInitializer;
public class SpitterWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer{
@Override
protected String[] getServletMappings(){ // 将一个或多个路径映射到DispatcherServlet上, 映射是”/",表明其会是应用的默认Servlet。会处理进入应用的所有请求。
return new String[]{"/"} // 将DispatcherServlet映射到 "/"
}
@Override
protected Class<?> [] getRootConfigClasses(){ // 用来配置ContextLoaderListener;返回带有@Configuration注解的类来配置ContextLoaderListener
return new Class<?> {RootConfig.class};
}
@Override
protected Class<?> [] getServletConfigClasses(){ // 用来定义DispatcherServelt应用上下文;getServletConfigClasses()方法中,要求DispatcherServlet加载应用上下文时,使用定义在WebConfig配置类中的bean
return new Class<?> {WebCofig.class} // 指定配置类, WebConfig类会添加@Configuration注解
}
}

AbstractAnnotationConfigDispatcherServletInitializer的任意类都会自动地配置Dispatcher-Servlet和Spring应用上下文

DispatcherServlet和Servlet监听器(ContextLoaderListener)之间的关系

  • DispatcherServlet:创建Spring应用上下文,加载配置文件或配置类所声明的Bean
  • ContextLoaderListener: 加载应用中的其他Bean(这些Bean是驱动应用后端的中间层和数据层组件)
    AbstractAnnotationConfigDispatcherServletInitializer会同时创建DispatcherServletContextLoaderListener
  1. getServletConfigClasses()方法返回的带有@Configuration注解的类将用来定义DispatcherServlet应用上下文的Bean。
  2. getRootConfigClasses()方法返回的带有@Configuration注解的类将用来配置ContextLoaderListener创建的应用上下文的Bean。
    web.xml配置和通过AbstractAnnotationConfigDispatcherServletInitializer来配置DispatcherServlet都是可行的

启用Spring MVC

  • 第一种采用web.xml方式添加: <mvc:annotation-driven>
  • 第二种采用配置类方式
@Configuration
@EnableWebMvc
public class WebConfig{
}

采用配置类的方式能够启用Spring MVC但还有后续工作尚未完成

  1. 未配置视图解析器: 默认Spring会使用BeanNameViewResolver,这个视图解析器会查找ID与视图名称向匹配的bean,并查找的bean要实现View接口,以此来解析视图;
  2. 未启动组件扫描:Spring只能找到显式声明在配置类中的控制器;
  3. 默认配置DispatcherServlet会映射为应用的默认Servlet,对于所有Web请求都将处理,包括对静态资源的请求(图片和样式表等)

启用Spring MVC - 配置型实现可行方案

@Configuration // 将该类标记为配置类
@EnableWebMvc // 启用SpringMVC
@ComponentScan("spitter.web") // 启用组件扫描来扫描spitter.web包来查找组件(扫描带有@Controller注解,会使其成为组件扫描时的Bean,因此不需要在配置类中声明任何控制器
public class WebConfig extends WebMvcConfigurerAdapter{
@Bean
public ViewResolver viewResolver(){
InternalResourceViewResolver resolver = new InternalResourceViewResolver(); // 配置JSP视图解析器
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setExposeContextBeansAttributes(true);
return resolver;
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer){
configurer.enable(); // 配置静态资源处理
}
}

WebConfig类扩展WebMvcConfigurerAdapter并重写了configureDefaultServletHandling()方法。通过调用DefaultServletHandlerConfigurer的enable()方法。
要求DispatcherServlet将对静态资源的请求转发到Servlet容器中默认的Servlet上,而不是使用DispatcherServlet来处理此请求。

  • RootConfig
@Configuration
@ComponentScan(basePackages={"spitter"}, excludeFilters={@Filter(type=FilterType.ANNOTATION, value=EnableWebMvc.class)})
public class RootConfig{
}

控制器

编写基本控制器

在Spring MVC中,控制器知识方法上添加了@RequestMapping注解的类,这个注解声明了它们要处理的请求。
假设控制器类要处理对"/"的请求,并渲染应用的首页

基本控制器

package spittr.web
import static org.springframework.web.bind.annotation.RequestMethod.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bing.annotation.RequestMethod;
@Controller // 声明一个控制器,可以使用Component代替但无法确定HomeCtroller是什么组件类型
public class HomeController{
@RequestMapping(value="/", method=GET) // 处理对"/"的GET请求
public String home(){
return "home"; // 视图名为home (通过DispatcherServlet发给视图解析器来添加前缀、后缀等将这个逻辑名称解析为实际的视图。
// Eg: 视图名:home , 可以被解析为 /WEB-INF/views/home.jsp
}
}

HomeController是一个构造性(stereotype)注解,基于@Component注解。目的:辅助实现组件扫描。
通过@Controller,组件扫描器会自动找到HomeController,并将其声明为Spring应用上下文的Bean

编写自动化测试用来测试控制器

  • 没有通过请求的情况下进行的测试
// 在测试用会直接调用home()方法,并断言返回包含”home“的值的String。
public class HomeControllerTest{
@Test
public void testHomePage() throws Exception{
HomeController controller = new HomeController();
assertEquals("home", controller.home());
}
}
  • 升级测试: 站在Spring MVC的立场上测试控制器,Spring中mock Spring MVC对控制器执行HTTP请求的机制。在测试的时候没必要在启动Web服务器和Web浏览器
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import spitter.web.HomeController;
public class HomeControllerTest{
@Test
public void testHomePage() throws Exception{
HomeController controller = new HomeController();
MockMvc mockMvc = standaloneSetup(controller).build(); // ⭐️ 搭建MockMvc
mockMvc.perform(get("/")).addExpect(view().name("home")); // 预期得到home视图
// MockMvcBuilders.standaloneSetup()调用build()来构建MockMvc实例,然后使用MockMvc实例来执行 "/"的GET请求并设置期望得到的视图名称。
}
}

通过 mock更加完成的模拟和测试了所编写的控制器,通过发起"/"的GET请求,并断言结果的视图的名称为home。

定义类级别的请求处理

拆分@RequestMapping, 将其路径映射部分放到类级别上

@Controller
@RequestMapping("/") // 将控制器映射到 "/"
public class HomeController{
@RequestMapping(method=GET) // 处理GET请求
public String home(){
return "home"; // 视图名:home
}
}

上述代码中,将映射路径级别上升到类上,HTTP方法映射在方法级别上。当控制器在类级别上添加@RequestMapping注解时,此注解会应用到控制器的所有处理器方法上。

修改@RequestMapping中的参数将其 value 属性能够接受一个String类型的数组。

@Controller
@RequestMapping({"/", "/homepage"})
public class HomeController{
}

传递模型数据到视图中

  • DAO层,创建一个获取Spittle列表的Repository
package spittr.data;
import java.util.List;
import spittr.spittle;
public interface SpittleRepository{
List<Spittle> findSpittles(long max, int count);
}

findSpittles()方法接收两个参数,max参数:返回的Spittle中Spittle ID属性的最大值,count参数:返回多少个Spittle对象。
模拟测试要求获取最新20个Spittle对象

List<Spittle> recent = spittleRepository.findSpittles(Long.MAX_VALUE, 20);
  • 实体层,Spittle类包含消息内容、时间戳和位置信息
public class Spittle{
private final Long id;
private final String message;
private finl Date time;
private Double latitude;
private Double longitude;
// setter & getter
}
  • 控制器层,SpittleController在模型中放入最新的Spittle列表
@Controller
@RequestMapping("/spittles")
public class SpittleController{
private SpittleRepository spittleRepository;
@Autowired
public SpittleController(SpittleRepository spittleRepository){ // 注入SpittleRepository
this.spittleRepository = spittleRepository;
}
@RequestMapping(method=RequestMethod.GET)
public String spittles(Model model){
model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20)); // 将spittle添加到模型中
return "spittle"; //返回视图名
}
}

spittles(Model model)方法将Model作为参数。spittles()方法就能将Repository中获取到的Spittle列表填充到模型中。
Model实际上是一个Map(key-value)的集合,将其传递给视图进行数据渲染。
addAttribute()方法不会指定Key,Key会根据值的对象类型推断确定。
eg: model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20)); 是一个 List所以键Key将推断为spittleList

显式声明 Key的值的方法:
第一种

@RequestMapping(method=RequestMethod.GET)
public String spittles(Model model){
model.addAttributes("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
return "spittles";
}

第二种

@RequestMapping(method=RequestMethod.GET)
public String spittles(Map model){
model.put("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
return "spittles";
}
  • 能够替代spittles()方法的编写
@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(){
return spittleRepository.findSpittles(Long.MAX_VALUE, 20));
}

此方法没有返回视图名称而是直接返回查找的数据Spittle列表,模型的key会推断出为
逻辑视图名称会根据请求路径推断得出,上述方法spittles()用来处理/spittles的GET请求,因此视图的名称将会是spittles。

无论使用哪种方式来编写spittles()方法其目的是

模型中存储一个Spittle列表,Key为spittleList,这个列表发送到名为spittles的视图中, 按照InternalResourceViewResolver的方式,视图的JSP:"/WEB-INF/views/spittles.jsp"

  • 编写控制器测试
@Test
public void shouldShowRecentSpittles() throws Exception{
List<Spittle> expectedSpittles = createSpittleList(20); // 产生模拟数据
SpittleRepository mockRepository = mock(SpittleRepository.class);
when(mockRepository.findSpittles(Long.MAX_VALUE, 20))
.thenReturn(expectedSpittles);
SpittleController controller = new SpittleController(mockRepository);
MockMvc mockMvc = standaloneSetup(controller)
.setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
.build(); // Mock Spring MVC
mockMvc.perform(get("/spittles")) // 对 "/spittles" 发起GET请求
// 断言期望的值
.andExpect(view().name("spittles"))
.andExpect(model().attributeExists("spittleList"))
.andExpect(model().attribute("spittleList", hasItems(expectedSpittles.toArray()));
}
private List<Spittle> createSpittleList(int count){
List<Spittle> spittles = new ArrayList<Spittle>();
for(int i = 0; i < count; i ++){
spittles.add(new Spittle("Spittle" + i, new Date()));
}
}
  • 在前端渲染数据
<c:forEach items="${spittleList}" var="spittle"/>
<li id="spittle_<c:out value="spittle.id"/>">
<div class="spittleMessage">
<c:out value="${spittle.message}"/>
</div>
<div>
<span class="spittleTime"><c:out value="${spittle.time}"/></span>
<span class="spittleLocation">
(<c:out value="${spittle.latitude}"/>,
<c:out value="${spittle.longitude}"/>)
</span>
</div>
</c:forEach>

从客户端接收请求的输入

Spring MVC允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括:

  1. 查询参数(Query Parameter)
  2. 表单参数(Form Parameter)
  3. 路径变量(Path Variable)

处理查询参数

SpittleController中的处理器方法能够同时处理有参数和没有参数的场景。

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

max参数默认是Long的最大值,但是defaultValue属性需要String类型的值。 Long.MAX_VALUE不匹配所以需要将Long.MAX_VALUE转换为MAX_LONG_AS_STRING的String类型常量
private static final String MAX_LONG_AS_STRING = Long.toString(Long.MAX_VALUE);

通过路径参数接受输入

场景假设: 根据给定ID来展现某一个Spittle记录📝。

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

此处理器方法将处理: /spittle/show?spittle_id=12345,此种方式是非常传统的请求模式,但从面向资源的角度来看并不理想。

要识别的资源(Spittle)应通过URL路径进行标示,而不是通过查询参数,
/spittle/12345(识别出要查询的资源)发起GET请求要优于对/spittles/show?spittle_id=12345(带有参数的操作通过HTTP发起RPC)发起请求。

  • 响应/spittle/12345请求的控制器
@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String spittle(@PathVariable("spittleId") long spittleId, Model model){
model.addAttribute(spittleRepository.findOne(spittleId));
return "spittle";
}

以上用来处理/spittle/12345的请求,spittle()方法中的spittleId参数上添加@PathVariable("spittleId")注解表明在请求路径中不管占位符部分的值是什么都会传递到处理器方法的spittleId的参数中。

处理表单

处理所提交的表单并注册新用户

@Controller
@RequestMapping("/spitter")
public class SpitterController{
private SpitterRepository spitterRepository;
@Autowired
public SpitterController(SpitterRepository spitterRepository){
this.spitterRepository = spitterRepository;
}
// 显示注册表单
@RequestMapping(value="/register", method=GET)
public String showRegistrationForm(){
return "registerForm";
}
// 注册成功后重定向基本也
@RequestMapping(value="/register", method=POST)
public String proceedRegistration(Spitter spitter){
spitterRepository.save(spitter); // 保存Spitter
return "redirect:/spitter/" + spitter.getUsername(); // 重定向到基本信息页
}
// 重定向到用户基本信息界面
@RequestMapping(value="/{username}", method=GET)
public String showSpitterProfile(@PathVariable String username, Model model){
Spitter spitter = spitterRepository.findByUserName(username);
model.addAttribute(spitter);
return "profile";
}
}

校验表单

对用户提交的数据域进行判空,如果出现空值将阻止进行进一步操作。
Java校验API定义多个注解放到属性上限制这些属性的值。所有的注解都来自javax.validation.constraints

注解 描述
@AssertFalse 注解元素必须是Boolean类型且值为false
@AssertTrue 注解元素必须是Boolean类型且值为true
@DecimalMax 注解元素必须是数字且值小于或等于给定的BigDecimalString值
@DecimalMin 注解元素必须是数字且值大于或等于给定的BigDecimalString值
@Digits 注解元素必须是数字,且值必须有指定的位数
@Future 注解元素必须是一个将来的日期
@Max 注解元素必须是数字,且值要小于或等于给定的值
@Min 注解元素必须是数字,且值要大于或等于给定的值
@NotNull 所注解的元素不能为Null
@Null 所注解的元素必须为Null
@Past 所注解的元素必须为已经过去的日期
@Pattern 所注解的元素必须匹配给定的正则表达式
@Size 所注解的元素必须是String、集合或数组,并且他们的长度要符合给定的范围

Spitter:包含提交到Spittle Post请求中的域

public class Spitter{
private Long id;
// @NotNull注解确保其值不是null,@Size属性限制他们长度在最大值和最小值之间
@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 processRegistration(@Valid Spitter spitter, Errors errors){ // 校验Spitter输入
if(errors.hasErrors()){
return "registerForm"; // 如果校验出现错误则重新返回表单
}
spitterRepository.save(spitter);
return "redirect:/spitter/" + spitter.getUsername();
}

Spitter参数添加@Valid注解,告知Spring要确保这个对象满足校验限制,在Spitter属性上添加校验限制但程序仍然会执行,
如果有校验出现的错误,错误可以通过Errors对象访问。Errors参数要紧跟在带有@Valid注解的参数后面,@Valid注解所表示的就要是检验的参数。processRegistration()方法所做的第一件事是调用Errors.hasErrors()来检查是否有错误。如果有误,Errors.hasErrors()将返回到registerForm.即注册表单的视图让用户修正错误重新提交。

关于SpringMVC内容的干货 http://www.springmvc.cn/

posted @   Felix_Openmind  阅读(272)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
*{cursor: url(https://files-cdn.cnblogs.com/files/morango/fish-cursor.ico),auto;}
点击右上角即可分享
微信分享提示