Spring Framework 学习笔记(3) Sping MVC

1. 背景

Sping MVC 是在 Spring 之上的框架,用于开发 Web 程序。

2. 初步了解 Spring MVC

2.1 知识

Spring MVC 是建立在 Servlet API 之上的 Web 框架,包含在 Spring Framework 中。MVC 是指 模型,视图,控制器的意思,Spring MVC 实现了这种思想。

Spring MVC 分离了控制器、模型对象、过滤器以及处理程序对象的角色,这种分离让它们更容易进行定制。

Spring MVC 不依赖 JSP,可以使用其他模板引擎(JSP,thymeleaf等)。RESTful API 返回的 JSON 格式可以理解为 json View,也是 MVC。

Spring MVC 与许多其他 Web 框架一样,是围绕前端控制器模式( front controller )设计的,其中DispatcherServlet为请求处理提供统一入口,而实际工作由委托组件处理。

一个HTTP请求经过 Spring MVC 需要经历的过程如下:


image.png
  • 1、 HTTP 请求 携带用户请求的内容,比如表单等 到达 DispactcherServlet。
  • 2和3、DispactcherServlet 需要将请求委托给其他组件来执行,它查询 处理器映射(Handler Mapping)以确定具体将请求转发到哪个 控制器 (Controller)
  • 4和5、DispactcherServlet 将请求转发到具体选定的控制器(Controller),Controller 负责访问服务和数据库获得 模型( Model ),并返回一个视图名称。
  • 6和7、DispactcherServlet 将模型和视图 发送到一个 视图解析器 ( View Resolver), 由视图解析器 使用模型渲染输出到视图。
  • 8和9、DispactcherServlet 将视图的呈现内容返回,响应到请求内容给客户端。

2.2 编写一个精简的 MVC 项目

刚刚说了 一个请求所要经历的过程,提到了几个组件,下面我们通过搭建基础版的项目进一步了解。

传统的web项目需要一个web.xml进行配置,包括 Servlet的配置映射,请求映射,视图解析,异常处理,委托组件等。DispatcherServlet 需要知道这些配置。

我们这里不这么做,而由 java 代码配置 DispatcherServlet 。 通过继承 AbstractAnnotationConfigDispatcherServletInitializer 来实现,当它部署在 sevlet 3.0的容器中时,容器会自动发现它并应用配置,示例:

/**
 * web app 的初始化辅助类。等同于 web.xml
 */
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[]{RootConfig.class};
    }

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

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

上面的代码中配置说明如下:

  • getRootConfigClasses() 返回的类将会用来配置 ContextLoaderListener 创建的应用上下文中的Bean。
  • getServletConfigClasses() 返回的类用来配置 web 应用,比如控制器,视图解析器等。

通常在一个 web 应用中有这么两个上下文:

  • Servlet WebApplicationContext : 包含 控制器,视图解析器,Handler映射等。
  • Root WebApplicationContext: 通常包含基础架构 bean,例如数据存储库和业务服务Bean。

关系如下:


image.png

我这里 RootConfig 是空的。

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

ServletConfig 类中配置一个 jsp 视图解析器。

@Configuration
@EnableWebMvc
@ComponentScan
public class ServletConfig extends WebMvcConfigurerAdapter {


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

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

写一个简单的 Controller

@Controller
@RequestMapping("/main")
public class HelloWorldController {
    @RequestMapping(value = "/say", method = RequestMethod.GET)
    public String sayHello(Model model) {
        model.addAttribute("yourname", "zhangsan");
        return "welcome";
    }
}

welcome.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>HelloWorld page</title>
</head>
<body>
<h1>这是首页</h1>
Your Name : ${yourname}
</body>
</html>

使用 Spring 结合 Spring MVC 的一个精简版的项目就搭建好了。
我的完整代码示例见:https://github.com/vir56k/java_demo/tree/master/spring_mvc_demo_1

2.3 读取请求中的参数

支持下述参数:

  • 查询字符串
  • Form 表单中的
  • 请求路径中的

示例:

使用 @RequestParam 读取查询字符串中或表单数据的参数值

    @GetMapping
    public String setupForm(@RequestParam("petId") int petId, Model model) { 
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute("pet", pet);
        return "petForm";
    }

使用 @PathVariable 注解读取 青汽路径的参数。

@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") 
public void findPet(@PathVariable String petId) {
    // ...
}

2.4 参数的合法性校验

Spring Framework 提供对 Java Bean Validation API 的支持。

示例:

public class PersonForm {

    @NotNull
    @Size(max=64)
    private String name;

    @Min(0)
    private int age;
}
public class User {

    private String email;

    @NotNull @Email
    public String getEmail() {
      return email;
    }

    public void setEmail(String email) {
      this.email = email;
    }
}

public class UserService {

  public void createUser(@Email String email,
                         @NotNull String name) {
    ...
  }
}

更多请参考:https://beanvalidation.org/

2.5 视图渲染

而类似 JSP 开发的方式已是古老的方法。较新一点的是 Thymeleaf 框架。当前(本文写作时间2021-07-06)比较流行的开发方式是前后端分离的技术,使用 ReactJS,VUE 单独开发项目。本文不再多介绍。

2.6 使用HTTP消息转换器

消息转换器
消息转换(message conversion)提供了一种更为直接的方式,它能够将控制器产生的数据转换为服务于客户端的表述形式(JSON,XML等)。

当使用消息转换功能时,DispatcherServlet不再将模型数据传送到视图中,它直接通过消息转换器直接转换成指定格式。

比如 如果 Jackson JSON在类路径下,那么处理方法返回的对象将交给 MappingJacksonHttpMessageConverter 来处理。

@RestController注解
正常情况下,当处理方法返回Java对象时,这个对象会放在模型中并在视图中渲染使用。但是,如果使用了消息转换功能的话,我们需要告诉Spring 跳过正常的模型/视图流程,并使用消息转换器。最简单
的方法是为控制器方法添加@ResponseBody注解。

@RequestMapping("/login")
@ResponseBody
public Object login(String name, String password, HttpSession session) {
    ...
    return new JsonResult(user);
}

如果在控制器类上使用@RestController来代替@Controller的话,Spring将会为该控制器的所有处理方法应用消息转换功能。我们不必为每个方法都添加@ResponseBody了。

@RestController
public class XxxController{
   ...
}

返回ResponseEntity对象
控制器方法可以返回一个ResponseEntity对象。ResponseEntity中可以包含响应相关的元数据(如头部信息和状态码)以及要转换的对象实体。

@GetMapping("/custom")
ResponseEntity<String> custom() {
    HttpHeaders headers = new HttpHeaders();
    headers.add("Custom-Header", "foo");

    return new ResponseEntity<>(
      "Custom header set", headers, HttpStatus.OK);
}

2.7 扩展

UriComponentsBuilder
Spring提供了UriComponentsBuilder,可以给我们一
些帮助。它是一个构建类,通过逐步指定URL中的各种组成部分(如host、端口、路径以及
查询),我们能够使用它来构建UriComponents实例。

UriComponents uriComponents=UriComponentsBuilder
        .fromHttpUrl("http://localhost:8080//hello")
        .queryParams(params).build() 
String uri=uriComponents.toUriString();

@RequestMapping
@RequestMapping 标识了这是一个请求映射,还有一些基于此扩展的方法,像下面这些,从名字就能看出具体的含义。

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

自定义 servlet 和 filter
扩展一下,通过继承 WebApplicationInitializer 可以注册自定义 servlet 和 filter:

/**
 * 初始化 web 应用
 */
public class MyWebApplicationInitializer implements WebApplicationInitializer {

    public void onStartup(ServletContext servletContext) throws ServletException {
        // 注册一个 servlet
        ServletRegistration.Dynamic servlet = servletContext.addServlet("myservlet1", MyServlet.class);
        servlet.addMapping("/custom/**");

        // 注册一个过滤器
        FilterRegistration.Dynamic filter1 = servletContext.addFilter("filter1", MyFilter1.class);
        filter1.addMappingForUrlPatterns(null, false, "/custom/*");
    }
}

Multipart 解析器和文件上传
注册一个 Multipart 解析器

public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    // ...

    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {

        // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
        registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
    }

}

接收上传文件

@Controller
public class FileUploadController {

    @PostMapping("/form")
    public String handleFormUpload(@RequestParam("name") String name,
            @RequestParam("file") MultipartFile file) {

        if (!file.isEmpty()) {
            byte[] bytes = file.getBytes();
            // store the bytes somewhere
            return "redirect:uploadSuccess";
        }
        return "redirect:uploadFailure";
    }
}

异常处理
Spring MVC 提供了多种形式将异常转化成 响应:

  • 特定的 Spring 异常将自动映射到 HTTP 的状态码
  • 异常上使用 @ReponseStatus 注解,可以将其对应到某个 HTTP 状态码
  • 方法上使用 @ExceptionHandler 注解,标识处理异常。

@ResponseStatus 注解的自定义异常,将自动映射到 HTTP 的状态码:

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "啊呜,找不见了...")
public class MyException extends RuntimeException {

}

使用 @ExceptionHandler 注解捕获异常:
@ExceptionHandler 作用在 controller 类上时,可以捕获这个 controller 的异常。

    // @ExceptionHandler 注解,在这里会捕获这个类异常
    @ExceptionHandler(MyException2.class)
    public String handleException() {
        return "errorrrr";
    }

@ExceptionHandler 注解 结合“ 控制器通知 ” 可以捕获所有 控制器的异常。

控制器通知( controller advise ) 是指 被标注了@ControllerAdvice 注解的类。这个类可以配合以下的注解使用:

  • @ExceptionHandle 标注的方法
  • @InitBinder 标注的方法
  • @ModelAttribute 标注的方法
    在标注了 @ControllerAdvice 的类中,上述的三个方法会运用到整个应用程序所有控制器中带有 @RequestMapping 方法上。
    示例:
@ControllerAdvice
public class AppExceptionHandler {

    // @ExceptionHandler 注解,在这里会捕获这个类异常
    @ExceptionHandler(MyException3.class)
    public String handleException() {
        return "errorrrr";
    }
}

3. 示例

我的代码示例见:https://github.com/vir56k/java_demo/tree/master/spring_mvc_demo_2

4.参考:

https://docs.spring.io/spring-framework/docs/current/reference/html/web.html

http://websystique.com/springmvc/spring-4-mvc-helloworld-tutorial-annotation-javaconfig-full-example/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容