整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

SpringMVC文件上传下载(单文件、多文件)

SpringMVC文件上传下载(单文件、多文件)

大家好,我是bigsai,今天我们学习SpringMVC的文件上传下载。

文件上传和下载是互联网web应用非常重要的组成部分,它是信息交互传输的重要渠道之一。你可能经常在网页上传下载文件,你可能也曾沉浸于互联网技术的神秘,而本篇就为你解开它神秘的面纱。

您的关注、点赞、转发,是我创作努力源源不断的动力!

本文为笔者原创,已收录在公众号:bigsai 头条:码农bigsai 和博学谷中 同时同步,欢迎关注!

案例分析

你肯定会问:通过本篇可能能够学到什么?

那我很负责任的告诉你,通过本篇文章,你能够掌握SpringMVC文件上传(单文件、多文件)文件下载知识和内容的使用,并能够根据这些实现一些基本的案例。

核心思路拆解

你可能会问:,这么一个完整的项目是如何分工运行?

不急不急,我来告诉你,其实这么一个文件上传下载的项目,它是一个b-s结构的web项目,涉及到前端和服务端,从宏观来看它是这样的一个结构:

但是从文件上传、下载两个功能来看它们之间又是有所区别的,文件上传的主要核心是用户上传的文件服务端接收存储

而文件下载更重要的部分是用户请求之后服务端给用户返回二进制文件

所以文件上传和文件下载的项目大体结构相似,只是各个部分在具体实现上有差别,我们需要更多关注下文件上传和下载服务端的实现和区别。

案例所涉及知识点

在本案例中,用到了以下知识点:

html页面form表单:

在前端无论是html还是jsp等模板引擎编写上传的页面时候。<form> 标签就意为一个(文件)上传的表单。

  • 表单能够包含若干 input 标签,而input标签又有不同类型比如文本字段、复选框、单选框、文件等等。
  • 我们通常使用表单编写若干标签代表我们想要向服务端发送的数据,然后通过标签的按钮将数据请求提交至服务端。
  • 表单的method表示请求的类型(一般为post),action表示需要请求的url地址,enctype表示传输数据类型。

SpringMVC:

案例的文件上传和下载基于SpringMVC,而我们在Springboot项目中整合SpringMVC。

  • 本案例使用SpringMVC作为项目mvc架构的框架,将模型(Model),视图(View),控制器(Controller)分离降低项目的耦合性。
  • 本案例使用SpringMVC的MultipartFile接口和ResponseEntity接口实现文件上传和下载。

创建SpringMVC项目

SpringMVC为一个mvc架构的web框架,创建SpringMVC项目的方式有很多,你可以选择直接通过IDEA创建SpringMVC项目,也可以通过Maven方式创建web项目然后添加SpringMVC的依赖,但这两种方式有太多的配置还需要配置tomcat,在效果一致的情况下咱们尽量简化一些开发配置类的工作,所以不采用以上两种方式创建项目。

而Springboot简化了Spring项目的开发,开箱即用,且内嵌tomcat,所以咱们选择创建基于Springboot且整合SpringMVC的项目方便快捷,更能直奔主题进行操作。

项目创建

首先,打开IDEA,创建项目,选择Spring Initializr类型初始化点击next。


然后你会得到一个选择项目名和一些配置的页面,我们在Group中填写com,而Artifact咱们填写fileupload。点击next。


接着在选择对应模块依赖的时候,选择Spring web 模块,此模块就是包含SpringMVC的web模块


接着选择需要创建项目的地址目录,点击next

这样你就可以得到一个完整的包含web模块(SpringMVC)的Springboot项目,就可以在里面编写咱们项目的代码。

目录介绍

上面创建完的基于Springboot的SpringMVC项目,默认有若干文件和文件夹,不同文件和文件夹有着不同的职责:

  • java:用来编写java服务端相关代码,例如Controller,Dao,Service等。
  • application.properties: 编写一些项目和框架的配置内容以及和第三方框架整合配置等
  • static: 静态资源目录,用来存放html、JavaScript、图片等资源。
  • teamplates:用来编写Thymeleaf等模板引擎,这里不使用
  • pom.xml:编写maven项目jar包资源依赖。如果项目需要引入其他依赖或者修改打包方式可以进行修改。

对于web项目的文件上传,需要进行一定配置以满足我们的使用需求,我们在application.propertis进行以下配置:

# 允许项目中文件上传
spring.servlet.multipart.enabled=true
# 上传文件的临时目录 (一般情况下不用特意修改)
#spring.servlet.multipart.location=# 上传文件最大为 1M (默认值 1M 根据自身业务自行控制即可)
spring.servlet.multipart.max-file-size=104857600
# 上传请求最大为 10M(默认值10M 根据自身业务自行控制即可)
spring.servlet.multipart.max-request-size=104857600
# 文件大小阈值,当大于这个阈值时将写入到磁盘,否则存在内存中,(默认值0 一般情况下不用特意修改)
spring.servlet.multipart.file-size-threshold=0
# 判断是否要延迟解析文件(相当于懒加载,一般情况下不用特意修改)
spring.servlet.multipart.resolve-lazily=false

当然,你对文件有大小等其他要求可以对配置进行自行更改。到这里带有SpringMVC环境的项目已经创建完成啦,剩下的只需要编写前端、服务端代码运行测试即可。

单文件上传

下面请跟我实战 SpringMVC单文件上传。一个完整的文件上传项目有两部分组成:前端界面和服务端程序。

前端设计

对于前端页面,我们使用你一定熟悉的html而不选用其他模板引擎。而form表单是html文件上传的核心组件,你在使用前需要了解它的一些属性。

表单的enctype属性上面说了一个表单文件传输的大体流程,你也知道表单有个至关重要的属性:enctype。而entype值通常有以下三种:

  • application/x-www-form-urlencoded:默认编码方式,在发送前编码所有字符(默认)使用url编码方式,和get请求有些相似。但这种方式如果发送大量二进制数据效率会比较低。
  • multipart/form-data:不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。通常用来向服务端发送二进制数据,而我们的文件也主要以二进制的方式进行传输。
  • text/plain:空格转换为 "+" 加号,但不对特殊字符编码。

所以本单文件上传案例中,需要注意以下事项:

  • 表单的enctype要为multipart/form-data类型,表示二进制传输。
  • 在一个form表单内定义一个input为file属性的标签,代表文件上传。
  • form表单的method需要为post。
  • enctype要为multipart/form-data类型,表示二进制传输。

前端页面的规则了解之后你在static下创建一个index1.html文件,里面具体的代码内容为:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>单文件上传</title>
</head>
<body>
<h2>单文件上传</h2>
<form  action="onfile" method="post" enctype='multipart/form-data'>
    <input type="file" name="file" ><br>
    <input type="submit" value="提交">
</form>
</body>
</html>

其中action="onfile"代表的为请求地址为onfile,这里都在项目内所以用相对地址即可,如果上传为其他接口也可填写对应的绝对地址。这样前端页面就编写完成,我们还需要编写文件上传对应服务端模块。

服务端设计

服务端主要负责文件接受,在前端看起来实现文件上传的页面很简单,但实际上在服务端的文件接收并没有那么容易,因为传过来的不光光是这一个(或多个)二进制文件,还附带一些头信息、文件名等等数据。打包过来的数据如果是文本数据解析可能还好,但是二进制文件数据一旦出现一点错误可能得到的整个文件都是损坏的。并且在咱们java web技术栈中文件上传也是有一定发展的历史的:

servlet文件上传(3.0以前)在servlet3.0以前,文件上传在服务端接收需要使用request.getInputStream()获取表单的二进制数据,但是在解析时候非常麻烦和复杂,对于文件上传这么一个很基本的模块在接收的时候可能要耗费很大的成本和精力去解决它,并且很多初级攻城狮很可能由于对io模块陌生无法实现上传文件在服务端的接收。

所以这个时候一些具有责任感的公司、组织就把它们的解析方法贡献出来供大家使用,大家不需了解传输文件底层内容,这些开源的处理方式中,最流行的当属apache旗下开源的commons-fileupload和 commons-io,把两个jar包加入到项目中你直接了解下这个api如何使用即可。有了这两个jar包,简单学习它的api,你就可以在普通的web项目中很容易的实现上传文件的功能!


servlet3.0以后
随着servlet版本更新,设计者可能看到javaweb开发中原生api对文件上传支持不太友好的问题,所以在api对文件上传的支持得到优化,简化了Java Web的开发。在servlet3.0中主要增加Part这个类用来读取文件数据和信息,在Part中直接将传输文件的名称、头信息、二进制文件分割开,通过简单的api就可以实现文件上传的功能。不需要再添加外部jar包


SpringMVC文件上传文件上传和下载是web开发常用模块,而SpringMVC作为一款优秀的web框架,对很多模块和内容进行更高度的封装和集成,而这么常用的文件上传肯定是少不了的,所以SpringMVC的文件上传基于apache旗下开源的commons-fileupload和 commons-io包。将其进行二次集成和封装至SpringMVC,将方法和内容封装至MultipartFile接口让我们使用起来更加方便,能够容易实现单文件、多文件上传。

对于上述各种文件上传服务端实现方式,大致可以通过下图展示:


通过上图你就可明白SpringMVC文件上传实现的原理,那么下面你就可以进行大显身手啦!SpringMVC处理上传文件很简单,我们需要在java目录下创建一个uploadController.java创建这么一个控制器,在上面加上@Controller注解。在Controller中编写以下代码:


 @PostMapping("onfile")
 @ResponseBody
 public String onfile(MultipartFile file) throws IOException {
     File file1 =new File("F:/fileupload/"+file.getOriginalFilename());//创建file对象
     if(!file1.exists())
            file1.createNewFile();//在磁盘创建该文件
     file.transferTo(file1);//将接受的文件存储
     return "sucucess";
 }

其中:

  • @PostMapping("onfile") 的意思为该请求方式为post,且请求的url在项目中的相对地址为onfile
  • @ResponseBody指不返回web页面,而是返回字符串或json字符串,在这里我们直接用一个成功单词代表跳转后的界面。
  • public String onfile(MultipartFile file) 函数名不重复就行,而MultipartFile file就是SpringMVC封装的一个处理文件的接口,其中参数名(这里是file)要和前端界面文件名相同(input type="file",name="file"中的name),通过这个接口你可以更容易的对文件进行各种操作,而本案例就是将上传的文件保存到本地F盘。

对于函数中的几行核心代码各司其职,除了注释的解释外,大致的流程可以参考如下图:

运行测试

这样启动项目,在浏览器输入http://localhost:8080/index1.html,选择文件上传,点击上传之后就可以在本地看到上传的文件啦。


至此,单文件上传就完成啦,单文件上传前端需要注意的就是form表单的method类型以及 enctype参数,而服务端也只需要用MultipartFile 接口就可以很容易的对文件进行接受。


第四关 多文件上传

上面讲的是单文件上传,很多时候你可能遇到的需求不光光是单文件上传。就比如你一定熟悉这个页面:

如上你可以看到,这么一次文件上传不止一个图片,并且数量也不确定,但都属于同一名称和集合的内容。这就是多文件上传。对于这种情况无论在前端还是服务端也是很容易处理的。

前端设计

我们这里实现一个多张图片的上传,首先在static目录下创建一个index2.html的页面。里面的具体内容为:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>多文件上传</title>
</head>
<body>
<h2>同一类别多个文件上传</h2>
<form name="onfile"  action="onfiles2" method="post" enctype="multipart/form-data">
    图片:
    <input type="file" name="img"><br>
    <input type="file" name="img"><br>
    <input type="file" name="img"><br>
    <input type="file" name="img"><br>
    <input type="submit" value="提交">
</form>
</body>
</html>

这样前端页面就编写完成,其中action要改为onfiles2,也就是待会要在服务端编写的接口。还有注意的这些input 所有type为file代指类型为文件,而name均为img意思是上传一组名称为img图片的集合。

服务端设计

而在我们服务端,其实用MultipartFile[]数组就可以对这样的多文件进行接收,我们在controller中编写以下代码:

@PostMapping("onfiles2")
@ResponseBody
public String onfiles2(MultipartFile img[]) throws IOException {
   for(int i=0;i<img.length;i++)
   {
       if(!img[i].isEmpty())//文件不空
       {
           File imgfile =new File("F:/fileupload/"+img[i].getOriginalFilename());
           imgfile.createNewFile();
           img[i].transferTo(imgfile);
           logger.info(img[i].getOriginalFilename());
       }
   }
    return "sucucess";
}

这个处理方式和前面的很相似,只不过是需要遍历MultipartFile[]对每个文件进行接收处理,当然文件为空的时候不进行处理。

运行测试

这样打开浏览器输入:http://localhost:8080/index2.html,上传文件测试效果:

这样一组类似相册上传的功能就完成啦,当然实际开发中的文件上传的要求肯定比这个要求严格很多,可能对文件的格式、大小都有一定的要求,这就要求你在前端和服务端都要对文件的后缀名、大小等信息进行校验,以达到自己场景化的需求。

文件下载

文件下载估计你在日常生活中会经常遇到,而你下载的其实就是服务端(服务器)的资源,对于文件类型有多种多样的,浏览器也能够识别很多种资源,事实上你现在访问的这个网页也是服务端的html文件、图片文件等资源,只不过这些资源浏览器能够显示而不会保存到本地。

直接访问资源VS下载资源

如果直接访问的资源是浏览器所不能识别解析的,例如doc、zip等类型文件,那访问的时候会默认下载到本地。而当你在SpringMVC中使用下载功能时,无论是什么资源都以下载的形式返回给客户端。这种区别可以参考下图:

在文件下载方面的实现,servlet本身也是实现文件下载的,不过使用起来有点繁琐。其原理就是往HttpServletResponse response的输出流写字节内容。而我们SpringMVC对文件下载也做了封装,将下载功能封装至ResponseEntity类中,我们在使用的时候也很方便。下面就来实战文件下载的功能。

首先,我们在F盘建立download文件夹,在里面添加对应文件,这个文件夹我们作为服务端的资源。

前端设计

我们在创建一个文件下载的前端页面,在static目录下创建index3.html,页面的具体内容为:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SpringMVC文件下载</title>
</head>
<body>
<h2>SpringMVC文件下载</h2>
个人照片<a href="/download/个人照片.png">个人照片.png</a><br>
个人简历<a href="/download/个人简历.pdf">个人简历.pdf</a>
</body>
</html>

其中href是下载的超链接,download是下载的接口名,而链接最后面部分则是下载资源名称。

服务端设计

文件下载的原理就是服务端向客户端返回二进制流和信息,而SpringMVC通过ResponseEntity完成。我们在controller中编写以下接口实现下载的功能:

@GetMapping("download/{filename}")
public ResponseEntity<byte[]>download(@PathVariable String filename) throws IOException {
    //下载文件的路径(这里绝对路径)
    String filepath= "F:/download/"+filename;
    File file =new File(filepath);
    //创建字节输入流,这里不实用Buffer类
    InputStream in = new FileInputStream(file);
    //available:获取输入流所读取的文件的最大字节数
    byte[] body = new byte[in.available()];
    //把字节读取到数组中
    in.read(body);
    //设置请求头
    MultiValueMap<String, String> headers = new HttpHeaders();
    headers.add("Content-Disposition", "attchement;filename=" + file.getName());
    //设置响应状态
    HttpStatus statusCode = HttpStatus.OK;
    in.close();
    ResponseEntity<byte[]> entity = new ResponseEntity<byte[]>(body, headers, statusCode);
    return entity;//返回
}

这样就是实现了文件下载功能,如果用传统servlet的方式下载文件可能需要在HttpServletResponse response中设置各种信息,而使用SpringMVC的ResponseEntity只需要将文件二进制主体、头信息以及状态码设置好即可进行文件下载,在易用性和简洁上更胜一筹。

运行测试

打开浏览器输入:http://localhost:8080/index3.html;点击需要下载的文件,就实现了文件下载的功能,运行情况图如下:

此时你就遇到了一个文件下载非常常见的问题:中文文件名错误显示。这个解决方案也很容易解决,只需将Content-Disposition内容后面的文件名进行url编码即可,具体代码为(替换上面对于部分):

headers.add("Content-Disposition", "attchement;filename=" + URLEncoder.encode(file.getName(), "UTF-8"));

这样重启程序,刷新页面再次点击下载的链接,你就会发现文件被成功的下载了:

总结与拓展

至此,SpringMVC的单文件上传、多文件上传以及文件下载你已经全部掌握了,是不是满满的成就感想去实现一个自己的小网站并把相关内容放进去?不过SpringMVC文件上传下载虽然简单,但你依然需要掌握其原理,学好java中的io文件传输,这样在各种场景的文件传输任务中方能胜任。

总结

前面所讲文件上传,前端就是form表单用<input type="file">表示客户端要上传文件,而服务端主要使用MultipartFile或者MultipartFile[]分别接收单个文件和多个文件。而在存储到本地也仅仅需要在本地磁盘创建对应文件然后MultipartFile调用transferTo()方法即可将上传的文件储存。

而文件下载的前端需要一个请求的url链接,服务端需要编写这个链接对应的接口。通过一些名称找到文件在本地真实的位置通过ResponseEntity即可将二进制文件返回给客户达到文件下载的功能。而ResponseEntity使用也很简单在创建时候只需要传入二进制主体、头和状态码即可成功返回,而这些SpringMVC已进行了很好封装你可以直接使用。

而无论是文件上传、多文件上传还是文件下载,一个完整的案例大致都需要这样一个过程:

  • 构思需求和页面大体样式
  • 编写前端html页面
  • 编写服务端响应的请求
  • 启动程序运行测试

在其中过程如果有问题可以根据编译器的错误提示、运行时的错误日志找到根源进行修正,这样完整的案例就可以成功完成啦!

案例拓展

你是否觉得自己掌握的可以了?那好,咱们拓展提升一下,我给你来一个需求:单文件和多文件混合上传

假设小明需要实现一个文件上传功能,小明需要上传一份简历和若干份照片(小于3)。这个项目该如何设计呢?它的计划页面可能是这样的:

我觉得聪明的你一定不会被难住,对于前端界面的html有什么想法呢?

对于种类来说有简历和照片两种文件,对于它们各自来说,简历只有一份,而照片可能有多份。

那么咱们的html页面可以这样设计:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>个人信息上传</title>
</head>
<body>
<h2>个人信息上传</h2>
<form name="onfile"  action="infoupload" method="post" enctype="multipart/form-data">
    姓名:<input type="text" name="name" ><br>
    年龄:<input type="text" name="age"> <br>
    图片:<input type="file" name="img">
         <input type="file" name="img">
    简历:<input type="file" name="resume"><br>
    <input type="submit" value="提交">
</form>
</body>
</html>

这里面和前面的单文件上传不同的是有多个input标签,此外action也要改成infoupload意思是你需要写这么一个接口来处理这个文件上传的内容。在controller中编写以下代码:

 private  static Logger logger= LoggerFactory.getLogger(uploadController.class);
    @PostMapping("infoupload")
    @ResponseBody
    public String onfile(String name,String age, MultipartFile img[],MultipartFile resume) throws IOException {
        logger.info(name);//日志中打印传输的name
        logger.info(age);
        //接收img[]
        for(int i=0;i<img.length;i++)
        {
            if(!img[i].isEmpty())//文件不空
            {
                File imgfile =new File("F:/fileupload/"+img[i].getOriginalFilename());
                imgfile.createNewFile();
                img[i].transferTo(imgfile);
            }
        }
         //接收resume
        File resumefile =new File("F:/fileupload/"+resume.getOriginalFilename());
        //在磁盘中创建文件,此时文件存在但没有内容
        resumefile.createNewFile();
        //将接受的文件复制到创建的文件中
        resume.transferTo(resumefile);
        return "sucucess";
    }

这个理解起来其实也很容易,这个和上面主要的区别就是函数中的多参数,其实每一个参数都是要和前端页面的form表单input标签的内容对应(名称一致)。form表单中的file类型在SpringMVC的controller中就是对应MultipartFile类型,form表单中的text类型对应controller中的String类型。如果上传单个文件,在服务端就用MultipartFile类型参数接收,如果多文件就用MultipartFile[]进行接收。上传类型和个数根据你自己的需求设计定义。

我们启动程序打开浏览器输入http://localhost:8080/index4.html选择文件进行上传,然后在本地你可以看到文件成功被保存。

至此,本篇的内容就结束了,本文主要简单讲解了SpringMVC中文件上传、多文件上传、文件下载的实现,现在你已熟练掌握。青山不改,绿水长流,我们下期再见!下课!

公众号:bigsai

头条号:码农bigsai 一个等你关注而朝思夜暮的博主!如果有帮助,记得转发分享!

什么是rest?什么是restful?

我相信很多人区分不开来,Rest的英文全称为Representational State Transfer,即表述性状态转移,就是将资源的状态以最适合客户端或服务端的形式从服务器端转移到客户端(或者反过来)。Rest是一种软件架构风格而不是标准,提供了设计原则和约束,基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。而Restful是Rest的一种实现,是基于Rest风格构建的API。

Restful API优点

1.Rest面向资源,一目了然,具有自解释性。

2.Restful接口直接基于HTTP协议,轻量,不在需要任何别的诸如消息协议。

3.数据格式默认以Json为主体,数据简单易读,前端不用单独解析,可以直接使用。

4.Rest是无状态的,无状态约束使服务器的变化对客户端是不可见的,因为在两次连续的请求中,客户端并不依赖于同一台服务器。就是说Service端是不用保存Client端的状态信息,比如登陆信息等。Client发送的请求必须包含有能够让服务器理解请求的全部信息,包括自己的状态信息。这点在分布式系统上显的比较重要。

5.不用理会客户端的类型,不论你是Web端请求,还是移动端请求,对后台接口而言,我只需要返回相应约定好格式的数据即可。

Restful中的概念重要

1、资源(Resources)

REST的名称"表现层状态转化"中,省略了主语。"表现层"其实指的是"资源"(Resources)的"表现层"。资源是一种看待服务器的方式,即,将服务器看作是由很多离散的资源组成。每个资源是服务器上一个可命名的抽象概念,它可以是一段文本、一张图片、一首歌曲、一种服务。资源是以名词为核心来组织的,首先关注的是名词。一个资源可以由一个或多个URI(统一资源定位符)来标识,URI既是资源的名称,也是资源在Web上的地址。

所谓"上网",就是与互联网上一系列的"资源"互动,调用它的URI。严格地说,有些网址最后的".html"后缀名是不必要的,因为这个后缀名表示格式,属于"表现层"范畴,而URI应该只代表"资源"的位置。

2、表现层(Representation)

我们把"资源"具体呈现出来的形式,叫做它的Representation,是一段对于资源在某个特定时刻的状态的描述。可以有多种格式,例如HTML/XML/JSON/纯文本/二进制格式/图片/视频/音频等等。它的具体表现形式,应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段是对Representation的描述。

3、状态转移(State Transfer)

访问一个网站,就代表了客户端和服务器的一个互动过程——在客户端和服务器端之间转移(transfer)代表资源状态的表述。而互联网通信协议HTTP协议,是一个无状态协议,所以所有的状态都需保存在服务器端。

SpringMVC实现Restful API

了解了Restful之后,对于一个Java程序员,特别是主攻后端的Java程序员,如何编写一个标准的Restful API才是关键。

在Rest中定位资源是通过URL进行识别的;而Rest中的行为是通过HTTP方法来定义的,所以我们还需要了解一下HTTP方法的定义:

POST:创建资源

READ:获取资源

PUT/PATCH:更新资源

DELETE:删除资源

注意:PUT和PATCH的区别在与PUT是对整个对象进行更新,而PATCH是局部更新,是非幂等的,PATCH在Spring3.2以上版本支持;POST具有非幂等的特点,所以一般情况下用作创建资源,当然还可以进行其他的操作。

1.创建一个基本的Restful API

下面这个例子就是最简单的一个控制器,通过在UserController类上添加@Controller注解表明该类是控制器。而@RequestMapping注解则用来处理请求地址映射,可以同时用在类或者方法上。@RequestMapping注解常用的两个属性就是method和value,method代表接受的HTTP请求方法;value代表URL路径的一部分。最后返回的时候需要加上@ResponseBody注解,该注解的作用是将Controller的方法返回的对象通过适当的转换器转换为指定的格式,并写入返回对象的body区,通常用来返回JSON数据或者是XML数据。

@Controller

public class UserController {

@Autowired

private UserSerivce userSerivce;

@RequestMapping(method=RequestMethod.GET, value="/users")

@ResponseBody

public List<User> getUsers() {

return userSerivce.getUsers();

}

}

在Spring4.0中提供了很多新的注解,其中@RestController注解就是其中之一。从下面的源码可以看到,@RestController其实就是集合了@Controller和@ResponseBody注解。这样我们就不用在每一个API上都使用@ResponseBody注解了。

@Target(ElementType.TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Controller

@ResponseBody

public @interface RestController {

@AliasFor(annotation=Controller.class)

String value() default "";

}

当然一个Restful API更有可能我们需要提供查询的参数,所以我们就需要引入@PathVariable注解。该注解可以将URL中占位符参数绑定到控制器处理方法的入参中:URL中的{xxx}占位符可以通过@PathVariable("xxx") 绑定到操作方法的入参中。使用方式如下所示:

// 访问方式 GET http://localhost:8080/user/1

@RequestMapping(method=RequestMethod.GET, value="/user/{id}")

public User getUser(@PathVariable("id") Integer id) {

return userSerivce.getUser(id);

}

2.返回错误信息

在Spring3中添加了一个ResponseEntity用于在返回时可以定义返回的HttpStatus和HttpHeaders,使用方式如下所示。

@RequestMapping(method=RequestMethod.GET, value="/user/{id}")

public ResponseEntity<User> getUser(@PathVariable("id") int id) {

User user=userService.getUser(id);

HttpStatus httpStatus=HttpStatus.OK;

HttpHeaders httpHeaders=new HttpHeaders();

try {

httpHeaders.setLocation(new URI("localhost:8080/user/" + id));

} catch (URISyntaxException e) {

httpStatus=HttpStatus.INTERNAL_SERVER_ERROR;

e.printStackTrace();

}

if(user==null) {

httpStatus=HttpStatus.NOT_FOUND;

}

return new ResponseEntity<User>(user, httpStatus);

}

但是我个人觉得这种方式并不能解决我们平常业务中的问题。在大多数情况下,其实我们的请求都是Ajax请求,我们任然需要和前端沟通返回的格式,比如如下的格式。通过code将后端执行后的结果告诉前端,我们任然需要自己写公用的返回工具类。

{

"code": 200,

"msg": "成功",

"_csrf": "6BerChUe",

"data": {

// 数据存放

},

"login": false

}

3.资源操作

下面是对资源进行增删查改操作。

@RestController

public class UserController {

@Autowired

private UserService userService;

// GET localhost:8080/user/{id}

@RequestMapping(method=RequestMethod.GET, value="/user/{id}")

public ResponseEntity<User> getUser(@PathVariable("id") int id) {

User user=userService.getUser(id);

HttpStatus httpStatus=HttpStatus.OK;

if(user==null) {

httpStatus=HttpStatus.NOT_FOUND;

}

return new ResponseEntity<User>(user, httpStatus);

}

// POST localhost:8080/user

@RequestMapping(method=RequestMethod.POST, value="/user")

public ResponseEntity<User> addUser(@RequestBody User user) {

User result=userService.addUser(user);

HttpStatus httpStatus=HttpStatus.OK;

if(result==null) {

httpStatus=HttpStatus.INTERNAL_SERVER_ERROR;

}

return new ResponseEntity<User>(result, httpStatus);

}

// DELETE localhost:8080/user/1

@RequestMapping(method=RequestMethod.DELETE, value="/user")

public ResponseEntity<User> deleteUser(@PathVariable("id") int id) {

User result=userService.deleteUser(id);

HttpStatus httpStatus=HttpStatus.OK;

if(result==null) {

httpStatus=HttpStatus.INTERNAL_SERVER_ERROR;

}

return new ResponseEntity<User>(result, httpStatus);

}

// PUT localhost:8080/user

@RequestMapping(method=RequestMethod.PUT, value="/user")

public ResponseEntity<User> updateUserByPUT(@RequestBody User user) {

User result=userService.updateUser(user);

HttpStatus httpStatus=HttpStatus.OK;

if(result==null) {

httpStatus=HttpStatus.INTERNAL_SERVER_ERROR;

}

return new ResponseEntity<User>(result, httpStatus);

}

// PATCH localhost:8080/user

@RequestMapping(method=RequestMethod.PATCH, value="/user")

public ResponseEntity<User> updateUserByPATCH(@RequestBody User user) {

User result=userService.updateUser(user);

HttpStatus httpStatus=HttpStatus.OK;

if(result==null) {

httpStatus=HttpStatus.INTERNAL_SERVER_ERROR;

}

return new ResponseEntity<User>(result, httpStatus);

}

}

同样对接口的测试我们可以用专门的工具进行测试,我这里使用的是Postman。

参考文档

《Spring In Action》第四版

https://www.cnblogs.com/wzbinStu/p/8566367.html

https://blog.csdn.net/maxiao124/article/details/79897229

https://baike.baidu.com/item/rest/6330506?fr=aladdin

https://baike.baidu.com/item/RESTful/4406165?fr=aladdin

https://blog.csdn.net/lxbfirst/article/details/78979556

SpringMVC概述

SpringMVC是一种基于Java的Web框架,?用于构建Web应用程序。?

SpringMVC的核心在于其设计模式——MVC(?Model-View-Controller)?,?这种设计模式将应用程序的数据处理、?用户接口和控制逻辑分开,?使得代码结构更加清晰,?便于维护和扩展。?在SpringMVC中,?Model代表数据和相关的业务逻辑,?View负责显示数据给用户,?而Controller则是协调Model和View的桥梁,?处理用户请求并返回相应的视图。

1.1 MVC模型

MVC全称Model View Controller,是一种设计创建Web应用程序的模式。这三个单词分别代表Web应用程序的三个部分:

  • Model(模型):指数据模型。用于存储数据以及处理用户请求的业务逻辑。在Web应用中,JavaBean对象,业务模型等都属于Model。
  • View(视图):用于展示模型中的数据的,一般为jsp或html文件。
  • Controller(控制器):是应用程序中处理用户交互的部分。接受视图提出的请求,将数据交给模型处理,并将处理后的结果交给视图显示。 ?



1.2 SpringMVC主要组件

(1)前端控制器 DispatcherServlet(不需要开发,由框架提供【核心】)

DispatcherServlet 是 Spring MVC 的入口函数。接收请求,响应结果,相当于转发器,中央处理器。有了 DispatcherServlet ,可以大大减少其它组件之间的耦合度。

用户请求到达前端控制器,就相当于 mvc 模式中的 c,DispatcherServlet 是整个流程控制的中心,由它调用其它组件来处理用户的请求。

(2)处理器映射器 HandlerMapping (不需要开发,由框架提供)

HandlerMapping 负责根据用户请求(URL),找到相应的 Handler 即处理器(Controller),SpringMVC 提供了不同映射器实现的不同映射方式,例如:配置文件方式,实现接口方式,注解方式等。

(3)处理器适配器 HandlerAdapter (不需要开发,由框架提供)

按照特定规则(HandlerAdapter 要求的规则)去执行 Handler,通过 HandlerAdapter 对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行处理。

(4)处理器 Handler (需要工程师开发)

Handler 是继 DispatcherServlet 前端控制器的后端控制器,在 DispatcherServlet 的控制下,Handler 对具体的用户请求进行处理。由于 Handler 涉及到具体的用户业务请求,所以一般情况下需要工程师根据业务需求来开发 Handler。

(5)视图解析器 View Resolver (不需要开发,由框架提供)

作用:进行视图解析,根据逻辑视图名解析成真正的视图(View),View Resolver 负责将处理结果生成 View 视图。首先,根据逻辑视图名解析成物理视图名(即具体的页面地址),再生成 View 视图对象,最后对 View 进行渲染,将处理结果通过页面展示给用户。

Spring MVC 框架提供了很多的 View 视图类型,包括:jstlView、freemarkerView、pdfView 等。 一般情况下,需要通过页面标签或页面模版技术,将模型数据通过页面展示给用户,这需要由工程师根据业务需求开发具体的页面。

(6)视图 View (需要工程师开发)

View 是一个接口,实现类才可以支持不同的View类型(jsp、freemarker、pdf...)

1.2 SpringMVC执行流程

1.2.1 SpringMVC三大核心组件

  • HandlerMapping处理器映射器:建立地址与方法的映射。

HandlerMapping负责根据用户请求url找到Handler即处理器,springmvc提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。

  • HandlerAdapter处理器适配器:根据地址调用方法。

Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。

  • ViewResolver 视图解析器:处理ModelAndView数据和视图。

ViewResolver通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。

1.2.2 SpringMVC执行流程图



1.2.3 Spring执行流程具体步骤(参考图)

  1. 用户发送请求至前端控制器DispatcherServlet。
  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器。
  3. 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
  4. DispatcherServlet通过HandlerAdapter处理器适配器调用处理器。
  5. 执行处理器(Controller层,也叫后端控制器)。
  6. Controller执行完成返回数据和视图(ModelAndView)。
  7. HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
  8. DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
  9. ViewReslover解析后返回具体的View视图(JSP / HTML)。
  10. DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)。
  11. DispatcherServlet响应用户,用户看到界面和数据。

1.2.4 总结流程

Spring MVC所有的请求都经过DispatcherServlet来统一分发。DispatcherServlet将请求分发给Controller之前,需要借助于Spring MVC提供的HandlerMapping定位到具体的Controller。 HandlerMapping接口负责完成客户请求到Controller映射。 Controller接口将处理用户请求,这和Java Servlet扮演的角色是一致的。一旦Controller处理完用户请求,则返回ModelAndView(数据和视图)对象给DispatcherServlet前端控制器。从宏观角度考虑,DispatcherServlet是整个Web应用的控制器;从微观考虑,Controller是单个Http请求处理过程中的控制器,而ModelAndView是Http请求过程中返回的模型(Model)和视图(View)。 返回的视图需要通过ViewResolver接口(视图解析器)在Web应用中负责查找View对象,从从而将相应结果渲染给客户。

2 SpringMVC项目快速搭建

2.1 搭建Maven项目



2.2 在pom.xml中加入相关依赖

<!-- springmvc依赖 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.5.RELEASE</version>
</dependency>
<!-- servlet依赖 -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
</dependency>
<!-- jsp依赖 -->
<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>javax.servlet.jsp-api</artifactId>
    <version>2.3.1</version>
</dependency>

2.3 在web.xml中声明DispatcherServlet对象

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
<!--声明springmvc的核心对象
        访问mymvc地址后,报错,文件没有找到。找到文件是/WEB-INF/springmvc-servlet.xml或者myweb-servlet.xml(这个)
        错误原因:在Servlet的init()方法中,创建springmvc使用的容器对象WebApplicationContext
        WebApplicationContext ctx=new ClassPathXmlApplicationContext(配置文件)
        配置文件的默认路径:/WEB-INF/<servlet-name>-servlet.xml
        DispatcherServlet作用:
        1.在init()中创建springmvc的容器对象 WebApplicationContext,创建springmvc配置文件的所有Java对象。
            java对象就是Controller对象
        2.DispatcherServlet 是一个Servlet,能够接受请求。
-->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--        如果是自定义的文件,需要在这写自定义配置文件的位置    和监听器的是一样的-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
 
<!--        在服务器启动时候创建对象,和容器的顺序    在启动时装载对象 随意给个值要求大于等于0  数值越小,创建的越早-->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
<!--        url-pattern 作用:把一些请求交给servlet处理   就例如将/mymvc交给springmvc处理
            使用中央调度器(DispatcherServlet)  1.使用扩展名方式,格式/*.xxx  例如:xxx.xml表示以xml结尾的都算
-->
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
</web-app>

2.4 创建一个发起请求的jsp页面(index.jsp)

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>第一个springmvc</title>
</head>
<body>
    <a href="some.do">发起一个som.do的请求</a> 
</body>
</html>

2.5 创建一个普通的类,作为控制器使用,代替之前的servlet

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
 
/**  @Controller: 创建控制器(处理器)对象
 *  控制器:叫做后端控制器(back controller),自定义的类处理请求的。
 *  位置:在类的上面,表示创建此类的对象,对象放在springmvc的容器中
 *
 */
@Controller
public class MyController {
    /*
        Springmvc框架使用 ,使用控制器类中的方法,处理请求
        方法的特点: 1.方法的形参,表示请求中的参数   2.方法的返回值,表示本次请求的处理请求
     */
 
    /**
     * @RequestMapping :请求映射
     *  属性:value 请求中的uri地址,唯一值,以"/"开头
     *  位置:1.在方法上面(必须) 2.在类定义的上面(可选)
     *  作用:指定的请求,交给指定的方法处理,等同于url-pattern(个人理解 相当于可以做doget相关的操作)
     *  返回值ModelAndView:表示本次请求的处理结果(数据和视图) model:表示数据   view:表示视图
     */
    //可以在一个类中定义多个方法使用多个@RequestMapping注解
    @RequestMapping(value={"/some.do","/first.do"})  //value是一个数组,可以有多个值,相当于将该方法起一个名字
    public ModelAndView doSome(){  //doGet()
    //使用这个方法处理请求,能够处理请求的方法叫做控制器方法
        //调用service对象,处理请求,返回数据
        ModelAndView mv=new ModelAndView();
        //添加数据
        mv.addObject("msg","在ModelAddView中处理了some.do的请求");
        mv.addObject("fun","执行了dosome的方法");
        //指定视图,setviewName("视图路径")  相当于请求转发request.getRequestDis...("/show.jsp").forward(..)
//        mv.setViewName("/WEB-INF/view/show.jsp");
        //当配置了视图解析器,使用文件名称作为视图名使用,叫做视图逻辑名称
        //使用了逻辑名称,框架使用配置文件中视图解析器的前缀和后缀,拼接为完整地视图路径 ,例如/WEB-INF/view/ + show + .jsp
        mv.setViewName("show");
        /*
        当框架调用完dosome方法后,得到返回中modelandview  框架会在后续的处理逻辑值,处理mv对象里的数据和视图
        对数据执行requert,setAttribute(“msg”,“处理了some.do请求”);把数据放到request作用域中
        对视图进行转发操作
         */
        return  mv;
    }
}

2.6 创建显示处理结果的jsp页面(webapp\WEB-INF\view\show.jsp)

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
        /show.jsp,显示request作用域中的数据<br>
        <h2>msg数据:<%=request.getAttribute("msg")%></h2>
        <h2>fun数据:${fun}</h2>
</body>
</html>

2.7 创建springmvc的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--spring的配置文件  声明组件扫描器-->
    <context:component-scan base-package="com.aiowang.controller"/>
<!--    声明视图解析器;帮助处理视图  主要帮助我们处理重复的多余的冗余路径等-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--        前缀:指定试图文件的路径-->
        <property name="prefix" value="/WEB-INF/view/"/>
<!--        后缀,试图文件的扩展名-->
        <property name="suffix" value=".jsp"/>  <!--表示所有的jsp文件-->
    </bean>
</beans>

2.8 配置Tomcat并验证

(1)Tomcat9.0下载 https://tomcat.apache.org/download-90.cgi



(2)IDEA配置Tomcat

打开配置选项



找到左侧Tomcat图标,新建,选择下载好并解压的Tomcat路径



部署



正常运行,成功



2.9 日志配置并验证

2.9.1 Log4j+Commons-Logging方式

(1) 导入依赖

junit是测试用的

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>compile</scope>
</dependency>

(2)基本使用

新建一个测试类HelloLog.java

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Test;

public class LogTest {
    //日志对象
    private Log log=LogFactory.getLog(LogTest.class);
    @Test
    public void test1(){
        log.trace("hello trace!");
        log.debug("hello debug");
        log.info("hello info");
        log.warn("hello warn");
        log.error("hello error");
        log.fatal("hello fatal");
    }
}

(3)新建log4j配置文件

写好了测试类运行发现没有打印 因为配置追加器,追加器:<appender>的意思是我们的日志要输出到哪里 写一个log4j的配置文件 新建log4j.xml,配置信息如下

<?xml version="1.0" encoding="UTF8" ?>
<!DOCTYPE log4j:configuration PUBLIC "-//LOGGER"
        "http://org/apache/log4j/xml/log4j.dtd">
<log4j:configuration>
    <!--org.apache.log4j.ConsoleAppender 输出到控制台-->
    <appender name="myConsole" class="org.apache.log4j.ConsoleAppender">
        <!--输出格式-->
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern"
                   value="%-d{yyyy-MM-dd HH:mm:ss,SSS} [%c]-[%p] %m%n"/>
            <!--%-d{yyyy-MM-dd HH:mm:ss,SSS}是当前时间
                      [%c]是日志出现的包和类   %p是日志的级别 %m是message也就是日志的消息,%n是换行符 -->
        </layout>
    </appender>
    <!--    输出到文件H:/log/hello.log中-->
    <appender name="myFile1" class="org.apache.log4j.RollingFileAppender">
        <param name="File" value="D:/log/hello.log"/><!--文件位置-->
        <param name="Append" value="true"/><!--是否选中追加-->
        <param name="MaxFileSize" value="1kb"/><!--文件最大字节数-->
        <param name="MaxBackupIndex" value="2"/>
        <!--第一个文件超出上面设置的文件最大字节数后,
        可以新增的新文件数量,这里只能新增2个,
        当日志文件要输出的内容超出3个1kb(第一个加上新增的两个),则覆盖新增的第一个文件,再覆盖第二个文件-->
        <!--日志的输出格式-->
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern"
                   value="%-d{yyyy-MM-dd HH:mm:ss,SSS} [%c]-[%p] %m%n"/>
            <!--%-d{yyyy-MM-dd HH:mm:ss,SSS}是输出当前时间
                      [%c]是输出日志出现的包和类   %p是日志的级别 %m是message也就是日志的消息,%n是换行符 -->

        </layout>
    </appender>
<!--    输出到文件,每天输出一个文件-->

    <appender name="myFile2" class="org.apache.log4j.DailyRollingFileAppender">
        <param name="File" value="h:/log/world.log"/>
        <param name="Append" value="true"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%-d{yyyy-MM-dd HH:mm:ss,SSS} [%c]-[%p] %m%n"/>
        </layout>
    </appender>
    <root>
        <!--优先级设置,all < trace < debug < info < warn < error < fatal < off-->
        <priority value="all"/>
        <appender-ref ref="myConsole"/>
        <appender-ref ref="myFile1"/>
        <appender-ref ref="myFile2"/>
    </root>
</log4j:configuration>

(4)日志输出格式

  • %t 输出产生改日志事件的线程名
  • %p 输出优先级,即上面提到的DEBUG、INFO、WARN、ERROR、FATAL
  • %c 输出所在类的全名
  • %r 输出自应用启动到输出该log信息耗费的毫秒数
  • %m message也就是日志的消息
  • %n 换行符
  • %d 输出日志时间点的日期或手机,默认格式为ISO8601,也可以在其后指定格式,比如%-d{yyyy-MM-dd HH:mm:ss,SSS},输出类似:2022-5-11 14:19:33,921

3 SpringMVC的常用注解

Spring MVC 框架中的常用注解主要包括在控制器层(Controller)、服务层(Service)、数据访问层(Repository)、实体类(Entity)、请求参数(Request Parameters)等方面。以下是这些注解的主要含义和用例 @Autowired、@ComponentScan、@Configuration 和 @Bean 是 Spring 框架中常用的注解,用于实现依赖注入和配置管理。

3.1 在控制器层(Controller)使用的注解:

1、@Controller: 含义: 标识一个类为 Spring MVC 控制器。 用例:

@Controller
public class MyController {
    // Controller methods
}

2、@RequestMapping: 含义: 映射 HTTP 请求的 URL 到一个具体的处理方法。 用例:

@Controller
@RequestMapping("/example")
public class MyController {
    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }
}

3、@RequestParam: 含义: 用于提取请求中的参数值。 客户端发送请求 /example/greet?name=John 用例:

@Controller
@RequestMapping("/example")
public class MyController {
    @RequestMapping("/greet")
    public String greet(@RequestParam("name") String name) {
        return "Hello, " + name + "!";
    }
}

4、@PathVariable: 含义: 用于将 URI 模板变量映射到处理方法的参数。 客户端发送请求 /example/user/123 用例:

@Controller
@RequestMapping("/example")
public class MyController {
    @RequestMapping("/user/{id}")
    public String getUserById(@PathVariable("id") Long userId) {
        // Retrieve user with the specified ID
        return "userDetails";
    }
}

5、@PatchMapping: 含义:用于映射PATCH请求到控制器方法。@PatchMapping是一个用于映射HTTP PATCH请求到控制器方法的注解,在SpringMVC中也可以使用。它可以用于方法级别,用于指定处理PATCH请求的方法。 用例:

@Controller
@RequestMapping("/users")
public class UserController {

    @PatchMapping("/{id}")
    public String updateUser(@PathVariable Long id, @RequestBody User user) {
        // ...
    }
}

3.2 CURD

1、@GetMapping: (查询) 含义:处理 HTTP GET 请求。 用例:

@Controller
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public String getUser(@PathVariable Long id) {
        // ...
    }
}

2、@PostMapping: (新增) 含义:处理 HTTP POST 请求。 用例:

@Controller
@RequestMapping("/users")
public class UserController {

    @PostMapping
    public String createUser(@ModelAttribute User user) {
        // ...
    }
}

3、@PutMapping:(更新) 含义:处理 HTTP PUT 请求。 用例:

@Controller
@RequestMapping("/users")
public class UserController {

    @PutMapping("/{id}")
    public String updateUser(@PathVariable Long id, @ModelAttribute User user) {
        // ...
    }
}

4、@DeleteMapping:(删除) 含义:处理 HTTP DELETE 请求。 用例:

@Controller
@RequestMapping("/users")
public class UserController {

    @DeleteMapping("/{id}")
    public String deleteUser(@PathVariable Long id) {
        // ...
    }
}

5、@PatchMapping: 含义:处理 HTTP PATCH 请求。 用例:

@Controller
@RequestMapping("/users")
public class UserController {

    @PatchMapping("/{id}")
    public String updateUser(@PathVariable Long id, @RequestBody User user) {
        // ...
    }
}

3.3 在服务层(Service)使用的注解

1、@Service: 含义: 标识一个类为服务层的组件。 用例:

@Service
public class MyService {
    // Service methods
}

@Service 是 Spring Framework 中的一个注解,用于标识一个类为服务层(Service Layer)的组件。服务层通常包含应用程序的业务逻辑,负责处理业务规则、调用数据访问层(Repository 或 DAO)执行数据库操作,并协调应用程序的不同部分

组件扫描: @Service 是 Spring 的组件扫描机制的一部分,标识带有该注解的类为一个服务层组件。在应用程序启动时,Spring 会扫描包路径下的所有组件,并注册为 Spring 容器中的 Bean。

依赖注入: 通过将 @Service 注解添加到类上,Spring IoC 容器会自动将该类的实例注入到其他需要依赖的组件中,例如控制器(Controller)或其他服务层组件。

事务管理: 在服务层执行的方法通常涉及数据库操作,@Service 注解通常与 @Transactional 注解一起使用,以启用事务管理。这确保了在业务方法中的一系列操作要么全部成功,要么全部失败(回滚)。

用例:

@Service
public class MyService {
    @Autowired
    private MyRepository myRepository;

    public String performBusinessLogic() {
        // Business logic implementation
        return "Business logic executed successfully";
    }

    public List<MyEntity> getAllEntities() {
        return myRepository.findAll();
    }
}

3.4 在数据访问层(Repository)使用的注解

1、@Repository: 含义: 标识一个类为数据访问层的组件,通常与 Spring 的数据访问异常转换一起使用。 用例:

@Repository
public class MyRepository {
    // Repository methods
}

3.5 在实体类(Entity)使用的注解

1、@Entity: 含义: 标识一个类为 JPA 实体类。 用例:

@Entity
public class User {
    // Entity properties and methods
}

@Entity 注解是 Java Persistence API (JPA) 的一部分,用于标识一个类为 JPA 实体类。JPA 是一种规范,用于描述如何通过 Java 对象与关系型数据库进行映射。@Entity 注解告诉 JPA,被注解的类将映射到数据库中的一个表。

数据库映射: @Entity 注解告诉 JPA 这个类与数据库中的表存在映射关系。类中的字段(成员变量)通常与表中的列相对应。

主键标识: 实体类通常需要一个主键,用于唯一标识每个实体对象。通过 @Entity 注解,JPA 可以识别实体类中的主键。

实体类识别: 当应用程序使用 JPA 进行持久化操作时,JPA 需要知道哪些类是实体类。@Entity 注解是 JPA 识别实体类的标志。

持久性操作: 通过实体类,可以执行 CRUD(Create, Read, Update, Delete)操作。JPA 提供了 EntityManager 接口,可以用于执行这些操作。

关系映射: 实体类之间的关系可以通过 JPA 进行映射,包括一对一、一对多、多对一、多对多等关系。

示例:

@Entity
public class User {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @Column(name="username")
    private String username;

    @Column(name="email")
    private String email;

    // Getters and setters
}

3.5 请求参数相关注解

1、@RequestBody: 含义: 用于将 HTTP 请求的正文映射到方法参数。 用例:

@Controller
@RequestMapping("/example")
public class MyController {
    @RequestMapping("/processJson")
    public String processJson(@RequestBody MyJsonModel jsonModel) {
        // Process JSON data
        return "result";
    }
}

2、@ResponseBody: 含义: 表示方法的返回值直接作为响应体,而不是视图名称。 用例:

@Controller
@RequestMapping("/example")
public class MyController {
    @RequestMapping("/getJson")
    @ResponseBody
    public MyJsonModel getJson() {
        // Return JSON data directly
    }
}

3.7 @Autowired

含义: 用于自动装配,将指定类型的 Bean 注入到属性、构造函数或方法参数中。 用例:

@Service
public class MyService {
    private final MyRepository repository;

    @Autowired
    public MyService(MyRepository repository) {
        this.repository=repository;
    }
}

在上例中,MyService 类通过 @Autowired 注解将 MyRepository 类型的 Bean 自动注入到构造函数中。

3.8 @ComponentScan

含义: 扫描指定包路径,寻找标有 @Component、@Service、@Repository、@Controller 注解的类,并将其注册为 Spring Bean。 用例:

@Configuration
@ComponentScan(basePackages="com.example")
public class AppConfig {
    // Configuration content
}

在上例中,@ComponentScan 注解扫描 com.example 包路径下的所有类,将带有相应注解的类注册为 Spring Bean。

3.9 @Configuration

含义: 声明当前类是一个配置类,通常与 @Bean 注解一起使用,用于配置 Spring 应用上下文。 用例:

@Configuration
public class AppConfig {
    // Bean declarations using @Bean
}

在上例中,AppConfig 被声明为配置类,用于定义 Spring Bean。

3.10 @Bean

含义: 在配置类中使用,用于声明一个 Bean。 用例:

@Configuration
public class AppConfig {
    @Bean
    public MyService myService() {
        return new MyService(myRepository());
    }

    @Bean
    public MyRepository myRepository() {
        return new MyRepository();
    }
}

在上例中,@Bean 注解用于声明两个 Bean:MyService 和 MyRepository。

4 SpringMVC的基本配置

4.1 静态资源映射

springmvc静态资源配置

在javaweb项目中配置了DispatcherServlet的情况下,如果不进行额外配置的话,几乎所有的请求都会走这个servlet来处理,默认静态资源按路径是访问不到的会报404错误,下面讲一讲如何配置才能访问到静态资源,本文将介绍三种方法

  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <async-supported>false</async-supported>
  </servlet>

  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

4.1.1 在java配置文件中配置DefaultServletHttpRequestHandler来进行处理

@Configuration
@EnableWebMvc
public class MyMvcConfigurer implements WebMvcConfigurer {

  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    // tomcat默认处理静态资源的servlet名称为default,不指定也可以DefaultServletHttpRequestHandler.setServletContext会自动获取
//    configurer.enable("default");
    configurer.enable();
  }
}

上述配置完成后org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#defaultServletHandlerMapping 方法会生成一个类名为SimpleUrlHandlerMapping的bean,当其他handlerMapping无法处理请求时会接着调用SimpleUrlHandlerMapping对象进行处理。SimpleUrlHandlerMapping中有一个urlMap属性,key为请求路径匹配模式串,/**能匹配所有的路径, value为handler匹配完成后会调用handler处理请求。 接着调用DefaultServletHttpRequestHandler的handleRequest方法处理请求,逻辑比较简单,获取请求转发器进行请求转发交给tomcat默认的servlet来进行处理。

4.1.2 在java配置文件中配置ResourceHttpRequestHandler来进行处理

@Configuration
@EnableWebMvc
public class MyMvcConfigurer implements WebMvcConfigurer {

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/static/**").addResourceLocations("/static/");
  }
}

和第一种配置几乎一样,其实只是换了一个handler类型来处理请求罢了。

上述配置完成后org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#resourceHandlerMapping 方法会生成一个类名为SimpleUrlHandlerMapping的bean,当其他handlerMapping无法处理请求时会接着调用SimpleUrlHandlerMapping对象进行处理

ResourceHttpRequestHandler比DefaultServletHttpRequestHandler的构建稍微复杂一点。之后也是调用SimpleUrlHandlerMapping相同的逻辑先根据请求路径匹配找到对应处理的handler,这里对应的是ResourceHttpRequestHandler之后调用handleRequest方法,原理是先根据请求的路径找到对应的资源文件,再获取资源文件的输入流写入到response响应中。

4.1.3 web.xml配置servlet映射

就是利用容器自身的默认Servlet, 以Tomcat为例,如下图有一个默认的Servlet,名称就是default(也可以在tomcat的配置文件中修改为其他名称,是在tomcat的目录/conf/web.xml中配置的)。

我们只需要在web项目的web.xml中配置静态文件是由此Servlet来映射即可。 default是容器的默认servlet的名称,示例为tomcat容器,其他容器根据实际情况来,如果tomcat配置文件修改了默认servlet名称,则也要修改为实际的。

<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/static/*</url-pattern>
</servlet-mapping>

将带有/static/xxx 路径的请求直接交给tomcat默认的servlet去进行处理

4.1.4 spring-mvc.xml文件中配置

<mvc:resources mapping="/images/**" location="/images/" />
<mvc:resources mapping="/js/**" location="/js/" />
<mvc:resources mapping="/css/**" location="/css/" />
<mvc:resources location="/,classpath:/META-INF/publicResources/" mapping="/resources/**"/>

因为上面的location属性节点是Resource资源, 因此可以使用classpath这类写法。 <mvc:resources />更进一步,由Spring MVC框架自己处理静态资源,并添加一些有用的附加值功能。

首先,<mvc:resources />允许静态资源放在任何地方,如WEB-INF目录下、类路径下等,你甚至可以将JavaScript等静态文件打到JAR包中。通过location属性指定静态资源的位置,由于location属性是Resources类型,因此可以使用诸如"classpath:"等的资源前缀指定资源位置。传统Web容器的静态资源只能放在Web容器的根路径下,<mvc:resources />完全打破了这个限制。

其次,<mvc:resources />依据当前著名的Page Speed、YSlow等浏览器优化原则对静态资源提供优化。你可以通过cacheSeconds属性指定静态资源在浏览器端的缓存时间,一般可将该时间设置为一年,以充分利用浏览器端的缓存。在输出静态资源时,会根据配置设置好响应报文头的Expires 和 Cache-Control值。

在接收到静态资源的获取请求时,会检查请求头的Last-Modified值,如果静态资源没有发生变化,则直接返回303相应状态码,提示客户端使用浏览器缓存的数据,而非将静态资源的内容输出到客户端,以充分节省带宽,提高程序性能。

4.2 拦截器配置

4.2.1 springMVC拦截器的实现一般有两种方式

第一种方式是要定义的Interceptor类要实现Spring的HandlerInterceptor 接口

第二种方式是继承实现了抽象类HandlerInterceptorAdapter

4.2.2 实例

4.2.2.1 编写拦截器

public class UserInterceptor implements HandlerInterceptor{

    /**
     * 该方法在整个请求完成后执行,主要用来清理资源
     * 该方法只能在当前interceptor的preHandler方法的返回值是true时才会执行
     */
    @Override
    public void afterCompletion(HttpServletRequest arg0,
            HttpServletResponse arg1, Object arg2, Exception arg3)
            throws Exception {
    }
    /**
     * 该方法在Controller的方法调用后执行,在视图被渲染以前被调用,所以可以用来对ModelAndView对象进行操作
     * 该方法只能在当前interceptor的preHandler方法的返回值是true时才会执行
     */
    @Override
    public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1,
            Object arg2, ModelAndView arg3) throws Exception {
    }
    /**
     * 该方法在请求之前被调用
     * 该方法返回为true时拦截器才会继续往下执行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
            Object handler) throws Exception {
        //用于判断用户是否登录
        boolean flag=false;
        
        User user=(User) request.getSession().getAttribute("user");
        if(user==null){
            request.setAttribute("message", "请先登录");
            request.getRequestDispatcher("loginPage.jsp").forward(request, response);
        }else{
            flag=true;
        }
        return flag;
    }
    
}

4.2.2.2 配置springmvc-config.xml,在里面配置拦截器,

<mvc:mapping path=""/>配置拦截路径

<mvc:exclude-mapping path=""/>配置不进行拦截的路径。

 <!-- 配置拦截器 -->
 <mvc:interceptors>
	 <mvc:interceptor>
		 <!-- 拦截路径 -->
		 <mvc:mapping path="/*"/>
		 <!-- 不拦截的路径 -->
		 <mvc:exclude-mapping path="/login"/>
		 <mvc:exclude-mapping path="/loginPage"/>
		 <bean class="com.dj.interceptor.UserInterceptor"></bean>
	 </mvc:interceptor>
  <!-- 当设置多个拦截器时,先按**顺序调用preHandle方法**,然后**逆序调用**每个拦截器的postHandle和afterCompletion方法  -->
 </mvc:interceptors>

4.2.2.3 编写controller

@Controller
public class UserController {

    @RequestMapping(value="/{pagename}")
    public String pageName(@PathVariable String pagename){
        return pagename;
        
    }
    @RequestMapping("login")
    public ModelAndView login(String username,String password
            ,ModelAndView mv,HttpSession session){
        
        if(username!=null&&username.equals("aaa")&&password!=null&&password.equals("111")){
            User user=new User();
            user.setUsername(username);
            user.setPassword(password);
            session.setAttribute("user", user);
            mv.setViewName("success");
        }else{
            mv.addObject("message", "账号或密码错误");
            mv.setViewName("loginPage");
        }
        return mv;
    }
    
    @RequestMapping("success")
    public String success(){
        return "success";
        
    }
}

4.2.2.4 编写登录页面和显示登录成功的页面,显示登录成功的页面只有在登录后才能访问,如果没有登录就访问将会被拦截。

<form action="login" method="post">
    <!-- 提示信息 -->
    <font color="red">${requestScope.message }</font><br>
    用户名:<input type="text" name="username" /><br>
    密码:<input type="password" name="password"/>
    <input type="submit" value="登录"/>
</form>

<body> 登陆成功! </body>

4.2.2.5 测试

直接访问success页面被拦截



访问登录页面,因为配置了不进行拦截的路径,所以显示如下



输入账号密码登录成功



4.3 @ControllerAdvice

对于@ControllerAdvice,我们比较熟知的用法是结合@ExceptionHandler用于全局异常的处理,但其作用不仅限于此。ControllerAdvice拆分开来就是Controller Advice,关于Advice,前面我们讲解Spring Aop时讲到,其是用于封装一个切面所有属性的,包括切入点和需要织入的切面逻辑。这里ContrllerAdvice也可以这么理解,其抽象级别应该是用于对Controller进行“切面”环绕的,而具体的业务织入方式则是通过结合其他的注解来实现的。@ControllerAdvice是在类上声明的注解,其用法主要有三点:

  • 结合方法型注解@ExceptionHandler,用于捕获Controller中抛出的指定类型的异常,从而达到不同类型的异常区别处理的目的;
  • 结合方法型注解@InitBinder,用于request中自定义参数解析方式进行注册,从而达到自定义指定格式参数的目的;
  • 结合方法型注解@ModelAttribute,表示其标注的方法将会在目标Controller方法执行之前执行。

在Spring MVC进行调用的过程中,会有很多的特殊的需求。比如全局异常,分页信息和分页搜索条件,请求时带来返回时还得回显页面。

Spring提供@ControllerAdvice对需要处理的范围进行配置。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
    // 控制的扫描包范围
    @AliasFor("basePackages")
    String[] value() default {};
    // 控制的扫描包范围
    @AliasFor("value")
    String[] basePackages() default {};
    // 控制的包类
    Class<?>[] basePackageClasses() default {};
    // @Controller或者@RestController的类 的数据
    Class<?>[] assignableTypes() default {};
    // 控制范围可以用注解进行配置
    Class<? extends Annotation>[] annotations() default {};
}

从上面的讲解可以看出,@ControllerAdvice的用法基本是将其声明在某个bean上,然后在该bean的方法上使用其他的注解来指定不同的织入逻辑。不过这里@ControllerAdvice并不是使用AOP的方式来织入业务逻辑的,而是Spring内置对其各个逻辑的织入方式进行了内置支持。本文将对@ControllerAdvice的这三种使用方式分别进行讲解。

4.3.1 @ExceptionHandler

系统比较庞大时很多的异常是不能控制,或者未知的,不能将所有的sql异常,反射异常,类不存在等抛到页面上展示给用户。

则需要一个全局的拦截器处理,Spring 提供了@ExceptionHandler处理方式。

1)、全局异常处理定义

@ControllerAdvice(basePackages="com.kevin.tool")
public class ExceptionHandlerController {
    /**
     * 错误后返回json
     * 如果想跳转到专门的异常界面,则可以返回{@link org.springframework.web.servlet.ModelAndView}
     *
     * @return 标准异常json
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public Map<String, String> handler() {
        Map<String, String> errorMap=new HashMap<String, String>(16);
        errorMap.put("code", "500");
        errorMap.put("msg", "系统异常,请稍后重试");
        return errorMap;
    }
}

2)、控制器方法调用异常

@RestController
public class ControllerAdviceDemoController {
    @ResponseBody
    @RequestMapping("bindException")
    public String bindException() {
        getMessage();
        return "ok";
    }
    private void getMessage() {
        throw new RuntimeException("未知异常!");
    }
}

3)、访问效果



4.3.2 InitBinder

@InitBinder从字面意思可以看出这个的作用是给Binder做初始化的,@InitBinder主要用在@Controller中标注于方法上(@RestController也算),表示初始化当前控制器的数据绑定器(或者属性绑定器),只对当前的Controller有效。@InitBinder标注的方法必须有一个参数WebDataBinder。所谓的属性编辑器可以理解就是帮助我们完成参数绑定,然后是在请求到达controller要执行方法前执行!

数据绑定有很多的场景,当前比如前端传入的日期为字符串类型,后端按照Format进解析为日期。

1)、全局日期绑定定义

@ControllerAdvice(basePackages="com.kevin.tool")
public class ExceptionHandlerController {
    @InitBinder("date")
    public void globalInitBinder(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
    }
}

2)、控制器方法调用日期转换

@RestController
public class ControllerAdviceDemoController {
    @ResponseBody
    @RequestMapping(value="/initBind", method=RequestMethod.GET)
    public String detail(@RequestParam("id") long id, Date date) {
        System.out.println(date);
        System.out.println(id);
        return "ok";
    }
}

3)、收到的日期类型效果

访问地址为://127.0.0.1:9999/initBind?id=123&date=2019-12-30



4.3.3 ModelAttribute

先看看@ModelAttribute的注解信息,元注解@Target指定可以修饰方法参数和方法(全局)。当前模拟一种常见,就是将所有输出的信息都加上当前的平台信息(比如版本等公共信息,这种需求还是比较多的)。

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModelAttribute {
    
    @AliasFor("name")
    String value() default "";
    
    @AliasFor("value")
    String name() default "";
 
    boolean binding() default true;
}

1)、全局返回属性添加

@ControllerAdvice(basePackages="com.kevin.tool")
public class ExceptionHandlerController {
 
    @ModelAttribute
    public void addAttributes(Model model) {
        model.addAttribute("msg", "hello");
 
        HashMap<String, String> map=new HashMap<>(16);
        map.put("version", "1.0.0");
        map.put("name", "XXX平台");
        model.addAttribute("platform", map);
    }
}

2)、控制器方法访问

@RestController
public class ControllerAdviceDemoController {
 
    @GetMapping("/modelAttributeTest")
    private String modelAttributeTest(@ModelAttribute("msg") String msg,
        @ModelAttribute("platform") Map<String, String> platform) {
 
        String result="msg:" + msg + "<br>" + "info:" + platform;
        return result;
    }
 
}

3)、输出效果



4.4 其他配置

4.4.1 ViewController

在控制器方法中只需要实现页面跳转(只设置页面视图名称)功能而没有其他业务,此时可以在SpringMvc的配置文件中使用view-controller标签表示控制器方法

在SpringMvC的核心配置文件中使用视图控制器标签跳转到首页

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 					http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd 					http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!--自动扫描控制层组件-->
    <context:component-scan base-package="com.atguigu.mvc.controller"></context:component-scan>
    <!--配置Thymeleaf视图解析器-->
    <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
    </bean>
    <!--视图控制器,设置请求对应的视图名称实现页面的跳转-->
    <mvc:view-controller path="/" view-name="index"></mvc:view-controller>
    <!--开启MVC的注解驱动,保证视图控制器设置的请求和控制器方法设置的请求全部都会被前端控制器处理-->
    <mvc:annotation-driven />
</beans>

请求路径path对应的视图名称是view-name,即请求路径/对应的视图名称是index。 此时,可以不需要控制器方法。

4.4.2 路径匹配参数配置

通过@RequestMapping注解: 匹配路径与处理器

@RequestMapping注解用于建立请求URL路径和处理器之间的对应关系.

出现位置: 可以出现在类上,也可以出现在方法上.

当它既出现在类上也出现在方法上时,类上注解值为请求URL的一级目录,方法上注解值为请求URL的二级目录 当它只出现在方法上时,该注解值为请求URL的一级目录 其属性如下:

path: value属性的别名,指定请求的URL,支持Ant风格表达式,通配符如下:

通配符

说明

?

匹配文件(路径)名中的一个字符

*

匹配文件(路径)名中的任意数量(包括0个)的字符

**

匹配任意数量(包括0个)的路径

例如

路径/project/*.a匹配项目根路径下所有在/project路径下的.a文件
路径/project/p?ttern匹配项目根路径下的/project/pattern和/project/pXttern,但不能匹配/project/pttern
路径/**/example匹配项目根路径下的/project/example,/project/foo/example,和/example
路径/project/**/dir/file.*匹配项目根路径下的/project/dir/file.jsp,/project/foo/dir/file.html,/project/foo/bar/dir/file.pdf
路径/**/*.jsp匹配项目根路径下的所有jsp文件
另外,遵循最长匹配原则,若URL请求了/project/dir/file.jsp,现在存在两个匹配模式:/**/*.jsp和/project/dir/*.jsp,那么会根据/project/dir/*.jsp来匹配.
  • method: 指定HTTP请求方法(可选RequestMethod.GET,RequestMethod.HEAD,RequestMethod.POST,RequestMethod.PUT等),多个值之间是或的关系.
  • params: 指定请求参数的限制,支持简单的表达式,如:
@RequestMapping(params={"param1"}),表示请求参数中param1必须出现
@RequestMapping(params={"!param1"}),表示请求参数中param1不能出现
@RequestMapping(params={"param1=value1"}),表示请求参数中param1必须出现且为value1
@RequestMapping(params={"param1!value1"}),表示请求参数中param1必须出现且不为value1
多个值之间是与的关系
  • headers: 限定HTTP请求中必须包含的请求头,同样支持简单的表达式 其中path和method属性较常用

4.4.3 WebMvcConfigurerAdapter

WebMvcConfigurerAdapter配置类是spring提供的一种配置方式,采用JavaBean的方式替代传统的基于xml的配置来对spring框架进行自定义的配置。因此,在spring boot提倡的基于注解的配置,采用“约定大于配置”的风格下,当需要进行自定义的配置时,便可以继承WebMvcConfigurerAdapter这个抽象类,通过JavaBean来实现需要的配置。

WebMvcConfigurerAdapter是一个抽象类,它只提供了一些空的接口让用户去重写,比如如果想添加拦截器的时候,需要去重写一下addInterceptors()这个方法,去配置自定义的拦截器。我们可以看一下WebMvcConfigurerAdapter提供了哪些接口来供我们使用。

public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {
    /*配置路径匹配参数*/
    public void configurePathMatch(PathMatchConfigurer configurer) {}
    /*配置Web Service或REST API设计中内容协商,即根据客户端的支持内容格式情况来封装响应消息体,如xml,json*/
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {}
    /*配置路径匹配参数*/
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {}
    /* 使得springmvc在接口层支持异步*/
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {}
    /* 注册参数转换和格式化器*/
    public void addFormatters(FormatterRegistry registry) {}
    /* 注册配置的拦截器*/
    public void addInterceptors(InterceptorRegistry registry) {}
    /* 自定义静态资源映射*/
    public void addResourceHandlers(ResourceHandlerRegistry registry) {}
    /* cors跨域访问*/
    public void addCorsMappings(CorsRegistry registry) {}
    /* 配置页面直接访问,不走接口*/
    public void addViewControllers(ViewControllerRegistry registry) {}
    /* 注册自定义的视图解析器*/
    public void configureViewResolvers(ViewResolverRegistry registry) {}
    /* 注册自定义控制器(controller)方法参数类型*/
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {}
    /* 注册自定义控制器(controller)方法返回类型*/
    public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {}
    /* 重载会覆盖掉spring mvc默认注册的多个HttpMessageConverter*/
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {}
    /* 仅添加一个自定义的HttpMessageConverter,不覆盖默认注册的HttpMessageConverter*/
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {}
    /* 注册异常处理*/
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {}
    /* 多个异常处理,可以重写次方法指定处理顺序等*/
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {}
}

WebMvcConfigurerAdapter提供了很多的接口供用户去实现自定义的配置项。下面挑几个比较重要的介绍一下如何使用这些接口来自定义配置。

(1)注册拦截器

首先,编写拦截器的代码:

public class LoginInterceptor extends HandlerInterceptorAdapter {

    private static final Logger logger=LoggerFactory.getLogger(LoginInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        logger.info("-----------------------------");
        logger.info(request.getRequestedSessionId());
        logger.info("-----------------------------");
        return true;
    }
}

这里只打印相关信息,然后,需要写一个config类去配置这个拦截器:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    /*
    * 拦截器配置*/
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**");
}

配置类继承了WebMvcConfigurerAdapter这个类,并且重写了addInterceptors这个方法,在方法中,注册了上面编写的拦截器,并且为此拦截器配置了拦截路径,这样一来就算是配置好了这个拦截器。

(2)配置CORS跨域

只需要在上面的webConfig里重写WebMvcConfigurerAdapter的addCorsMappings方法就可以获得基于spring的跨域支持。

/**
     * 跨域CORS配置
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        super.addCorsMappings(registry);
        registry.addMapping("/**")
                .allowedHeaders("*")
                .allowedMethods("POST","GET")
                .allowedOrigins("http://...")
                .allowCredentials(true);
    }

(3)配置ViewController

当首页或者登陆页的页面对外暴露,不需要加载任何的配置的时候,这些页面将不通过接口层,而是直接访问,这时,就需要配置ViewController指定请求路径直接到页面。

/**
     * 视图控制器配置
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        super.addViewControllers(registry);
        registry.addViewController("/").setViewName("forward:/index.html");
    }

(4)配置ViewResolver

通常在使用jsp的项目中,会基于spring mvc配置的文件去配置视图解析器,通过重写WebMvcConfigurerAdapter里的configureViewResolvers也可以将自己定义的InternalResourceViewResolver配置整合进spring中。

/**
     * 配置请求视图映射
     *
     * @return
     */
    @Bean
    public InternalResourceViewResolver resourceViewResolver() {
        InternalResourceViewResolver internalResourceViewResolver=new InternalResourceViewResolver();
        //请求视图文件的前缀地址
        internalResourceViewResolver.setPrefix("/WEB-INF/jsp/");
        //请求视图文件的后缀
        internalResourceViewResolver.setSuffix(".jsp");
        return internalResourceViewResolver;
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        super.configureViewResolvers(registry);
        registry.viewResolver(resourceViewResolver());
    }

可以看一下ViewResolverRegistry中的代码:

public UrlBasedViewResolverRegistration jsp() {
        return this.jsp("/WEB-INF/", ".jsp");
    }

    public UrlBasedViewResolverRegistration jsp(String prefix, String suffix) {
        InternalResourceViewResolver resolver=new InternalResourceViewResolver();
        resolver.setPrefix(prefix);
        resolver.setSuffix(suffix);
        this.viewResolvers.add(resolver);
        return new UrlBasedViewResolverRegistration(resolver);
    }

可以看到,即使不去配置,spring也会新建一个默认的视图解析器。十分方便。

(5)配置Formatter

当请求的参数中带有日期的参数的时候,可以在此配置formatter使得接收到日期参数格式统一。

@Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new Formatter<Date>() {
            @Override
            public Date parse(String date, Locale locale) {
                return new Date(Long.parseLong(date));
            }

            @Override
            public String print(Date date, Locale locale) {
                return Long.valueOf(date.getTime()).toString();
            }
        });
    }

4.4.4 WebMvcConfigurer

WebMvcConfigurer配置类其实是Spring内部的一种配置方式,采用JavaBean的形式来代替传统的xml配置文件形式进行针对框架个性化定制,可以自定义一些Handler,Interceptor,ViewResolver,MessageConverter。基于java-based方式的spring mvc配置,需要创建一个配置类并实现WebMvcConfigurer 接口;

在Spring Boot 1.5版本都是靠重写WebMvcConfigurerAdapter的方法来添加自定义拦截器,消息转换器等。SpringBoot 2.0 后,该类被标记为@Deprecated(弃用)。官方推荐直接实现WebMvcConfigurer或者直接继承WebMvcConfigurationSupport,方式一实现WebMvcConfigurer接口(推荐),方式二继承WebMvcConfigurationSupport类,

5 SpringMVC的高级配置

5.1 文件上传配置

Spring MVC 为文件上传提供了直接支持,这种支持是通过即插即用的 MultipartResolver 实现的。Spring 使用 Jakarta Commons FileUpload 技术实现了一个 MultipartResolver 实现类:CommonsMultipartResolver。

在 Spring MVC 上下文中默认没有装配 MultipartResolver,因此默认情况下不能处理文件的上传工作。如果想使用 Spring 的文件上传功能,则需要先在上下文中配置 MultipartResolver。

5.1.1 配置 MultipartResolver

下面使用 CommonsMultipartResolver 配置一个 MultipartResolver 解析器。

<!-- 文件上传 -->
<bean id="multipartResolver"
  class="org.springframework.web.multipart.commons.CommonsMultipartResolver"
  p:defaultEncoding="UTF-8"//①请求的编码格式,默认为ISO-8859-1
  p:maxUploadSize="5000000"//②上传文件的大小上限,单位为字节(5MB)
  p:uploadTempDir="file://d:/temp"/>//③上传文件的临时路径

defaultEncoding 必须和用户 JSP 的 pageEncoding 属性一致,以便正确读取表单的内容。uploadTempDir 是文件上传过程中所使用的临时目录,文件上传完成后,临时目录中的临时文件会被自动清除。

为了让 CommonsMultipartResolver 正常工作,必须先将 Jakarta Commons FileUpload 及 Jakarta Commons io 的类包添加到类路径下。

5.1.2 编写控制器和文件上传表单页面

在 UserController 中添加一个用于处理用户头像上传的方法,如下面代码所示。

@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping(value="/uploadPage")//①
    public String updatePage() {    
        return "uploadPage";
    }
    
    @RequestMapping(value="/upload")
    public String updateThumb(@RequestParam("name") String name,
                              @RequestParam("file") MultipartFile file) throws Exception{
                              //②上传的文件自动绑定到MultipartFile中
        if (!file.isEmpty()) {
            file.transferTo(new File("d:/temp/"+file.getOriginalFilename()));
            return "redirect:success.html";
        }else{
            return "redirect:fail.html";
        }
    }
}

Spring MVC 会将上传文件绑定到 MultipartFile 对象中。MultipartFile 提供了获取上传文件内容、文件名等方法,通过其 transferTo() 方法还可将文件存储到硬件中,具体说明如下。

  • byte[] getBytes():获取文件数据。
  • String getContentType():获取文件 MIME 类型,如 image/pjpeg、text/plain 等。
  • InputStream getInputStream():获取文件流。
  • String getName():获取表单中文件组件的名字。
  • String getOriginalFilename():获取上传文件的原名。
  • long getSize():获取文件的字节大小,单位为 Byte.
  • boolean isEmpty():是否有上传的文件。
  • void transferTo(File dest):可以使用该文件将上传文件保存到一个目标文件中。

负责上传文件的表单和一般表单有一些区别,表单的编码类型必须是 multipart/form-data 类型。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c"  uri="http://java.sun.com/jsp/jstl/core" %>
<html>
    <head>
        <title>请上传用户头像</title>
    </head>
    <body>
        <h1>
            请选择上传的头像文件
        </h1>
        <form method="post" action="<c:url value="/user/upload.html"/>" enctype="multipart/form-data">//指定表单内容类型,以便支持文件上传
            <input type="text" name="name" />
            <input type="file" name="file" />//②上传文件的组件名
            <input type="submit" />
        </form>
    </body>
</html>

5.2 自定义HttpMessageConverter

5.2.1 简介

在Spring MVC中,HttpMessageConverter主要用于将HTTP请求的输入内容转换为指定的Java对象,以及将Java对象转换为HTTP响应的输出内容。这种灵活的消息转换机制就是利用HttpMessageConverter来实现的。

Spring MVC提供了多个默认的HttpMessageConverter实现,包括处理JSON、XML、文本等格式的Converter。另外,我们也可以自定义HttpMessageConverter来处理其他格式的数据。

Spring MVC提供了两个注解:@RequestBody和@ResponseBody,分别用于完成请求报文到对象和对象到响应报文的转换。

然而,有时候默认的HttpMessageConverter无法满足特定的需求,例如,当我们需要处理的数据格式没有默认的Converter时,或者我们需要对现有的Converter进行扩展时,就需要自定义HttpMessageConverter。

自定义HttpMessageConverter可以让我们更加灵活地控制数据转换的过程,例如我们可以自定义转换规则、异常处理等。

5.2.2 实战案例

接下来我们通过一个实例讲解如何自定义HttpMessageConverter。

需求

接口请求数据格式:

xxx|yyy|zzz|...

接口返回JSON数据格式

{
    "xxx": xxx,
    "yyy": yyy,
    "zzz": zzz,
    ...
}

其实就上面的数据格式,我们完全可以不用自定义HttpMessageConverter也是完全可以实现的。我们这里主要就是教大家如何在特殊的需求下实现特定的数据转换处理。

(1)自定义HttpMessageConverter转换器

public class PackHttpMessageConverter implements HttpMessageConverter<Object> {
    
  // 设置自定义的Content-Type类型,这样就限定了只有请求的内容类型是该类型才会使用该转换器进行处理
  private static final MediaType PACK=new MediaType("application", "pack", StandardCharsets.UTF_8) ;


  // 判断当前转换器是否能够读取数据
  @Override
  public boolean canRead(Class<?> clazz, MediaType mediaType) {
    return PACK.equals(mediaType) ;
  }
  // 判断当前转换器是否可以将结果数据进行输出到客户端
  @Override
  public boolean canWrite(Class<?> clazz, MediaType mediaType) {
    return true ;
  }
  // 返回当前转换器只支持application/pack类型的数据格式
  @Override
  public List<MediaType> getSupportedMediaTypes() {
    return Arrays.asList(PACK) ;
  }


  // 从请求中读取数据
  @Override
  public Object read(Class<? extends Object> clazz, HttpInputMessage inputMessage)
      throws IOException, HttpMessageNotReadableException {
    InputStream is=inputMessage.getBody() ;
    String res=IOUtils.toString(is, StandardCharsets.UTF_8) ;
    // 这里简单处理只针对Users类型的对象处理
    if (clazz==Users.class) {
      try {
        // 创建实例
        Users target=(Users) clazz.newInstance() ;
        String[] s=res.split("\\|");
        target.setId(Long.valueOf(s[0])) ;
        target.setName(s[1]) ;
        target.setAge(Integer.valueOf(s[2])) ;
        target.setIdNo(s[3]) ;
        return target ;
      } catch (InstantiationException | IllegalAccessException e) {
        e.printStackTrace() ;
      }
    }
    return null ;
  }


  // 将Controller方法返回值写到客户端
  @Override
  public void write(Object t, MediaType contentType, HttpOutputMessage outputMessage)
      throws IOException, HttpMessageNotWritableException {
    // 设置响应头为json格式
    outputMessage.getHeaders().add("Content-Type", "application/json;charset=UTF-8") ;
    ObjectMapper mapper=new ObjectMapper() ;
    OutputStream os=outputMessage.getBody();
    // 输出结果内容
    os.write(mapper.writeValueAsString(t).getBytes(StandardCharsets.UTF_8)) ;
    os.flush(); 
  }
  
}

(2)将PackHttpMessageConverter注册到容器中

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
  
  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(new PackHttpMessageConverter()) ;
  }
}

到这里自定义HttpMessageConverter及注册到容器中就全部完成了,开发还是比较简单,接下来做测试

(3)接口

// 方法非常简单还是用的那些常用的类,@RequestBody接收请求body中的内容
@PostMapping("/i")
public Object i(@RequestBody Users user) {
  System.out.println(handlerAdapter) ;
  return user ;
}

(4)通过Postman测试接口

设置请求的header





似乎没有任何的问题,其实你只要在写的方法中打印下日志,或者调试下,你会发现你的write方法根本就没有被调用,也就是说写数据并没有使用到我们自定义的实现,这是因为有优先级比我们自定义的转换器高,所以要想让写消息也调用自定义的。我们需要如下修改注册方式:

public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
  converters.add(0, new PackHttpMessageConverter()) ;
}

这样我们自定义的转换器就排到了第一的位置,这样就会调用我们自定义的write方法。

5.2.3 实现原理

请求参数由于添加了@RequestBody,所以方法的参数解析器使用的是RequestResponseBodyMethodProcessor。

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
  protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
      Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
    // ...
    // 读取请求数据;调用父类方法
    Object arg=readWithMessageConverters(inputMessage, parameter, paramType);
    // ...
  }
}

AbstractMessageConverterMethodArgumentResolver

public abstract class AbstractMessageConverterMethodArgumentResolver {
  protected <T> Object readWithMessageConverters(...) {
    // ...
    // 遍历所有的消息转换器
    for (HttpMessageConverter<?> converter : this.messageConverters) {
        Class<HttpMessageConverter<?>> converterType=(Class<HttpMessageConverter<?>>) converter.getClass();
        GenericHttpMessageConverter<?> genericConverter=(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
        // 判断当前转换器是否读,也就上面我们自定义中实现的canRead方法
        if (genericConverter !=null ? genericConverter.canRead(targetType, contextClass, contentType) :
            (targetClass !=null && converter.canRead(targetClass, contentType))) {
          if (message.hasBody()) {
            HttpInputMessage msgToUse=getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
            // 读取具体的数据内容
            body=(genericConverter !=null ? genericConverter.read(targetType, contextClass, msgToUse) : ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
            body=getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
          }
          else {
            body=getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
          }
          break;
        }
      }
  }
}

原理也比较的简单。

自定义HttpMessageConverter是Spring MVC中一个强大的工具,它可以帮助开发者更加灵活地控制数据转换的过程,满足特定的需求。

5.2.4 MappingJackson2HttpMessageConverter

MappingJackson2HttpMessageConverter是springboot中默认的Json消息转换器。这个类的继承图如下:



这个类的主要实现逻辑是在AbstractJackson2HttpMessageConverter抽象类中实现的。这个列实现序列化与反序列化的最核心组件是ObjectMapper这个类。

MappingJackson2HttpMessageConverter是一个Spring消息转换器,用于在Web应用程序中处理请求和响应内容。它的工作原理如下:

  1. 请求处理:当接收到请求时,MappingJackson2HttpMessageConverter将请求内容(通常是JSON格式)转换为Java对象。它使用Jackson库来完成此操作。
  2. 响应处理:当生成响应时,MappingJackson2HttpMessageConverter将Java对象转换为JSON格式的响应内容。它再次使用Jackson库来完成此操作。

MappingJackson2HttpMessageConverter通过实现HttpMessageConverter接口并重写相关方法,完成请求和响应内容的转换。当Spring处理请求或生成响应时,它会自动选择合适的消息转换器,并使用它来处理请求和响应内容。

MappingJackson2HttpMessageConverter如何将请求内容转换为Java对象和响应内容转换为JSON

  • 请求内容到Java对象的转换:
  1. MappingJackson2HttpMessageConverter重写了read()方法,该方法接收请求内容和Java对象类型作为参数。
  2. 该方法使用ObjectMapper类,它是Jackson库的主要组件,将请求内容解析为Java对象。
  3. 解析的Java对象作为方法的返回值,并作为请求的参数传递到控制器方法中。
  • 响应内容到JSON的转换: MappingJackson2HttpMessageConverter
  1. 重写了write()方法,该方法接收Java对象和输出流作为参数。
  2. 该方法使用ObjectMapper类将Java对象序列化为JSON格式的内容。
  3. 序列化的内容写入输出流,并作为响应内容返回给客户端。

(1)简单使用:

@Configuration
public class MvcConfig implements WebMvcConfigurer {
  
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter(objectMapper()));
    }
    
    @Bean
    public ObjectMapper objectMapper() {
        return new Jackson2ObjectMapperBuilder()
                .propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
                .featuresToEnable(SerializationFeature.INDENT_OUTPUT)
                .build();
    }
}

这样,在控制器方法中使用@RequestBody或@ResponseBody注解时,就可以通过MappingJackson2HttpMessageConverter进行序列化/反序列化操作了。

(2)自定义MappingJackson2HttpMessageConverter

将时间戳序列化为LocalDateTime,将LocalDateTime反序列化为时间戳

public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
    MappingJackson2HttpMessageConverter converter=new MappingJackson2HttpMessageConverter();
    ObjectMapper mapper=new ObjectMapper();
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    // 时间对象自定义格式化
    JavaTimeModule javaTimeModule=new JavaTimeModule();
    javaTimeModule.addDeserializer(LocalDateTime.class, new JsonDeserializer<LocalDateTime>() {
        @Override
        public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
            long timestamp=Long.parseLong(jsonParser.getText());
            Instant instant=Instant.ofEpochMilli(timestamp);
            return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
        }
    });
    javaTimeModule.addSerializer(LocalDateTime.class, new JsonSerializer<LocalDateTime>() {
        @Override
        public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            jsonGenerator.writeNumber(localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli());
        }
    });
    javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
    javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
    javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
    javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
    // Long转换为String传输
    javaTimeModule.addSerializer(Long.class, ToStringSerializer.instance);
    mapper.registerModule(javaTimeModule);

    converter.setObjectMapper(mapper);
    return converter;
}

如果不生效:

  • 想要生效,其实只要把自定义的MappingJackson2HttpMessageConverter位置调整一下,
  • @Bean MappingJackson2HttpMessageConverter方式,注意这里是实现WebMvcConfigurer接口而不是继承WebMvcConfigurationSuppor
  • 如果是继承WebMvcConfigurationSupport的,要重写extendMessageConverters方法,这里不再展开了

5.2.5 StringHttpMessageConverter

StringHttpMessageConverter是Spring MVC中用于读写HTTP消息的字符串转换器。它可以将请求的输入流转换为字符串,同样也可以将字符串写入HTTP响应中

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
 
    @Bean
    public StringHttpMessageConverter stringHttpMessageConverter() {
        StringHttpMessageConverter converter=new StringHttpMessageConverter();
        converter.setWriteAcceptCharset(false); // 设置是否在写入响应时发送AcceptedCharset
        return converter;
    }
}

5.3 服务器端推送技术

5.3.1 SSE

SSE技术是基于单工通信模式,只是单纯的客户端向服务端发送请求,服务端不会主动发送给客户端。服务端采取的策略是抓住这个请求不放,等数据更新的时候才返回给客户端,当客户端接收到消息后,再向服务端发送请求,周而复始。

注意:因为EventSource对象是SSE的客户端,可能会有浏览器对其不支持,但谷歌、火狐、360是可以的,IE不可以。

另外WebSocket技术是双工模式。

服务端代码如下:

//本文使用的是Spring4.x,无需其他类库,;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HomeController {
    
    @RequestMapping(value="/", method=RequestMethod.GET)
    public String home(Locale locale, Model model) {
        return "sse";
    }
    
    @RequestMapping(value="push",produces="text/event-stream")
    public @ResponseBody String push(){
        System.out.println("push msg..");
        try {  
            Thread.sleep(2000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        
        //注意:返回数据的格式要严格按照这样写,‘\n\n’不可少
        return "data:current time: "+new SimpleDateFormat("YYYY-MM-dd hh:mm:ss").format(new Date())+"\n\n";  
    }
}

客户端代码如下,sse.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!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>SSE方式消息推送</title>
</head>
<body>

    <div id="msgFromPush"></div>
      <!--这里的jquery仅仅用于数据的展示,不影响消息推送-->
    <script type="text/javascript" src="<c:url value='resources/jquery-1.10.2.js'/>"></script>
    <script type="text/javascript">
        if(!!window.EventSource){
            var source=new EventSource('push');
            s='';
            source.addEventListener('message',function(e){
                console.log("get message"+e.data);
                s+=e.data+"<br/>";
                $("#msgFromPush").html(s);
            });
            
            source.addEventListener('open',function(e){
                console.log("connect is open");
            },false);
            
            source.addEventListener('error',function(e){
                if(e.readyState==EventSource.CLOSE){
                    console.log("connect is close");
                }else{
                    console.log(e.readyState);
                }
            },false);
        }else{
            console.log("web is not support");
            
        }
        
    </script>
</body>
</html>

运行结果:



5.3.2 Servlet3.0+异步方法处理

Servlet3.0+ 异步处理方法通过设置动态Servlet(即Dynamic)支持异步处理,在客户端(浏览器)以ajax形式不断发送请求,从而获得信息。

(1)动态Servlet支持异步处理

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

import com.config.MvcConfig;

/** Use class that implements WebApplicationInitializer interface to  substitute web.xml
 * @author apple
 *
 */
public class WebInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext applicationContext=         new AnnotationConfigWebApplicationContext();
        applicationContext.register(MvcConfig.class); // process annotated class MvcConfig
        applicationContext.setServletContext(servletContext);  // make applicationContext and servletContext related
        applicationContext.refresh();

        Dynamic servlet=servletContext.addServlet("dispatcher", new DispatcherServlet(applicationContext));
        servlet.addMapping("/");
        servlet.setLoadOnStartup(1);
        servlet.setAsyncSupported(true);
    }   
}

(2)Service Bean

该类仅仅是业务逻辑,与异步实现关系无关。

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.async.DeferredResult;

/** 用于异步Servlet 3.0+的服务器端推送测试,
 * 为Controller提供一个异步的定时更新的DeferredResult<String>
 * @author apple
 *
 */
@Service
public class PushService {
    private DeferredResult<String> deferredResult ;

    public DeferredResult<String> getAyncUpdateDeferredResult() {
        this.deferredResult=new DeferredResult<>();
        return deferredResult;
    }

    /**以下说明通过查看@Scheduled注解获得。
     * 由于@Scheduled注解的处理是通过注册一个ScheduledAnnotationBeanPostProcessor完成的,
     * 而后者是对方法被@Scheduled注解了的Bean,按照该注解的要求,
     * 通过调用一个TaskScheduler进行post process。
     * 因此对于实例化的Bean,必须完成@Scheduled注解的方法后才能被调用。
     */
    @Scheduled(fixedDelay=5000)
    public void refresh() {
        if (this.deferredResult !=null) {
            this.deferredResult.setResult(String.valueOf(System.currentTimeMillis()));
        }
    }
}

(3) Controller

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.DeferredResult;

import com.service.PushService;

@Controller
public class PushController {
    @Autowired
    PushService pushService; 

    @RequestMapping(value="/defer")
    @ResponseBody
    public DeferredResult<String> deferredCall() {
      // 通过Service Bean获取异步更新的DeferredReuslt<String>
        return pushService.getAyncUpdateDeferredResult(); 
    }
}

(4) 测试页面(jsp)

测试页面采用ajax不断发送请求,这些请求构成了并发请求。由于前面Servlet支持异步处理,请求到达服务端后,直到Service Bean实例化,并按照@Scheduled要求延时至设定的时间(例中5000ms)进行了设置,才通过Controller中以@RequestMapping注解的方法发送response到浏览器。 测试结果: 浏览器页面显示(数据为:System.currentTimeMillis()) 1528273203260 1528273208265 1528273213271 1528273218278 1528273223282 1528273228285 1528273233290 1528273238296 1528273243298 1528273248302

由结果可知,请求确实是并发的,Servlet 3.0+对请求进行了异步处理。

6 SpringMVC的测试

6.1 Mock 测试简介

1、什么是 mock 测试

在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法,就是 mock 测试在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法,就是mock测试

  • 虚拟的对象就是 mock 对象。
  • mock 对象就是真实对象在调试期间的代替品。

2、为什么使用 mock 测试

  • 避免开发模块之间的耦合
  • 轻量、简单、灵活

3、MockMVC 介绍

基于 RESTful 风格的 SpringMVC 的测试,我们可以测试完整的 Spring MVC 流程,即从 URL请求到控制器处理,再到视图渲染都可以测试。

1)MockMvcBuilder

MockMvcBuilder 是用来构造 MockMvc 的构造器,其主要有两个实现:StandaloneMockMvcBuilder 和 DefaultMockMvcBuilder,对于我们来说直接使用静态工厂 MockMvcBuilders 创建即可。 MockMvcBuilder 是用来构造 MockMvc 的构造器,其主要有两个实现:StandaloneMockMvcBuilder 和 DefaultMockMvcBuilder,对于我们来说直接使用静态工厂 MockMvcBuilders 创建即可。

2)MockMvcBuilders

负责创建 MockMvcBuilder 对象,有两种创建方式:

standaloneSetup(Object... controllers):通过参数指定一组控制器,这样就不需要从上下文获取了。

webAppContextSetup(WebApplicationContext wac):指定 WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的 MockMvc,本章节下面测试用例均使用这种方式创建 MockMvcBuilder 对象。

3)MockMvc

对于服务器端的 SpringMVC 测试支持主入口点。通过 MockMvcBuilder 构造MockMvcBuilder 由 MockMvcBuilders 建造者的静态方法去建造。

核心方法:perform(RequestBuilder rb) -- 执行一个 RequestBuilder 请求,会自动执行SpringMVC 的流程并映射到相应的控制器执行处理,该方法的返回值是一个 ResultActions。

4)ResultActions

(1)andExpect:添加 ResultMatcher 验证规则,验证控制器执行完成后结果是否正确;

(2)andDo:添加 ResultHandler 结果处理器,比如调试时打印结果到控制台;

(3)andReturn:最后返回相应的 MvcResult;然后进行自定义验证/进行下一步的异步处理;

5)MockMvcRequestBuilders

用来构建请求的,其主要有两个子类 MockHttpServletRequestBuilderMockMultipartHttpServletRequestBuilder(如文件上传使用),即用来 Mock 客户端请求需要的所有数据。

6)MockMvcResultMatchers

(1)用来匹配执行完请求后的结果验证

(2)如果匹配失败将抛出相应的异常

(3)包含了很多验证 API 方法

7)MockMvcResultHandlers

(1)结果处理器,表示要对结果做点什么事情

(2)比如此处使用 MockMvcResultHandlers.print() 输出整个响应结果信息

8)MvcResult

(1)单元测试执行结果,可以针对执行结果进行自定义验证逻辑


6.2 测试用例演示

1、添加依赖

<!-- spring 单元测试组件包 -->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-test</artifactId>
	<version>5.0.7.RELEASE</version>
</dependency>
<!-- 单元测试Junit -->
<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<version>4.12</version>
</dependency>
<!-- Mock测试使用的json-path依赖 -->
<dependency>
	<groupId>com.jayway.jsonpath</groupId>
	<artifactId>json-path</artifactId>
	<version>2.2.0</version>
</dependency>

前两个 jar 依赖我们都已经接触过了,对于返回视图方法的测试这两个 jar 依赖已经足够了,第三个 jar 依赖是用于处理返回 Json 数据方法的,这里要明白每个 jar 的具体作用。

2、被测试的方法

@RequestMapping(value="editItem")
public String editItem(Integer id, Model model) {
	Item item=itemService.getItemById(id);
	model.addAttribute("item", item);
	return "itemEdit";
}

@RequestMapping(value="getItem")
@ResponseBody
public Item getItem(Integer id) {
	Item item=itemService.getItemById(id);
	return item;
}

这里我们提供了两个方法,一个是返回视图的方法,另一个是返回 Json 数据的方法,下面我们会给出测试类,分别对这两个方法进行测试。

3、测试类:ItemMockTest

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:spring/*.xml")
@WebAppConfiguration
public class ItemMockTest {

    @Autowired
    private WebApplicationContext context;

    private MockMvc mockMvc;

    @Before
    public void init() {
        mockMvc=MockMvcBuilders.webAppContextSetup(context).build();
    }
}

这里前两个注解就不再解释了,我们在学习 Spring 与 Junit 整合的时候已经讲解过了,这里说一下第三个注解: @WebAppConfiguration:可以在单元测试的时候,不用启动 Servlet 容器,就可以获取一个 Web 应用上下文。

1)返回视图方法测试

@Test
public void test() throws Exception {
    MvcResult result=mockMvc.perform(MockMvcRequestBuilders.get("/editItem").param("id", "1"))
            .andExpect(MockMvcResultMatchers.view().name("itemEdit"))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andDo(MockMvcResultHandlers.print())
            .andReturn();
    Assert.assertNotNull(result.getModelAndView().getModel().get("item"));
}





这三句代码是我们对结果的期望,最后打印出了结果,说明执行成功,所有期望都达到了,否则会直接报错。从结果中我们就可以看到这个请求测试的情况。

2、返回 Json 数据方法

@Test
public void test1() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get("/getItem")
            .param("id", "1")
            .accept(MediaType.APPLICATION_JSON))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
            .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1))
            .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("IPhone X"))
            .andDo(MockMvcResultHandlers.print())
            .andReturn();
}



在这个方法中比较特殊的就是设置 MediaType 类型,因为都是使用 Json 格式,所以设置了 MediaType.APPLICATION_JSON,jsonPath 用于比对期望的数据是否与返回的结果一致,这里需要注意的是 "$.id" 这 key 的种形式。