本篇来学习SpringBoot如何整合Web开发,由于其中涉及的内容非常多,因此分为三篇来进行记录。所谓的SpringBoot如何整合Web开发,其实就是对前面的一些具体细节的补充。本篇学习的内容具体包括:返回JSON数据、静态资源访问、文件上传和@ControllerAdvice
等相关知识。
返回JSON数据 默认实现 JSON是目前主流的前后端数据传输方式,SpringMVC中使用消息转换器HttpMessageConverter
对JSON的转换提供了很好的支持,在SpringBoot中这种支持更进一步得到了提升,也对相关的配置做了进一步的简化。在默认情况下,当开发人员新创建一个SpringBoot项目后,添加Web依赖,也就是web启动器:
1 2 3 4 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
这个依赖中默认添加了jackson-databind
作为JSON处理器,此时不需要添加额外的JSON处理器就能返回一段JSON。
使用spring Initializr
构建工具构建一个SpringBoot的Web应用,名称为webspringboot
,然后添加spring-boot-starter-web
依赖,也就是上面的代码。在com.envy.webspringboot
包内新建一个pojo包,接着在里面新建Book实体类,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.envy.webspringboot.pojo; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import java.util.Date; public class Book { private Integer id; private String name; @JsonIgnore private Integer price; @JsonFormat(pattern="yyyy-MM-dd") private Date publishDate; //getter和setter方法,包含三个参数的构造方法 }
在上面使用了@JsonIgnore
和 @JsonFormat
注解。当你不希望某个字段被序列化为JSON字符串时就可以在该字段上使用@JsonIgnore
注解;当你需要对某个字段的输出格式进行自定义的时候就可以使用@JsonFormat
注解,同时里面传入一个pattern对象作为序列化时的标准样式。
接着在com.envy.webspringboot
包内新建一个controller包,然后在里面新建BookController实体类用于序列化字符串,其实就是将前面的对象转换为JSON对象,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.envy.webspringboot.controller; import com.envy.webspringboot.pojo.Book; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.util.Date; @Controller public class BookController { @GetMapping("/book") @ResponseBody public Book book(){ Book book = new Book(1,"西游记",128,new Date()); return book; } }
当然如果你需要频繁的使用@ResponseBody
注解,那么可以使用@RestController
这个组合注解来替换@Controller
和@ResponseBody
注解。此时代码就变成了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.envy.webspringboot.controller; import com.envy.webspringboot.pojo.Book; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Date; @RestController public class BookController { @GetMapping("/book") public Book book(){ Book book = new Book(1,"西游记",128,new Date()); return book; } }
运行项目,在浏览器地址栏中输入http://localhost:8080/book
,即可看到返回了JSON数据:(使用了JSON Viewer插件后的显示结果,价格不会显示)
这是SpringBoot自带的处理方式。如果采用这种方式,那么对于字段忽略、日期格式化等常见需求都是可以通过注解来实现的。其实这都是Spring中默认提供的MappingJackson2HttpMessageConverter
来实现的,因此你可以根据自己的实际需求来自定义JSON转换器。
自定义转换器 常见的JSON处理器除了jackson-dababind
之外,还有GSON和fastjson,这里针对常见的用法分别来进行举例说明。
(1)使用Gson 。Gson是Google开源的一个JSON解析框架,如果你想使用Gson,需要先除去默认的jackson-databind
,然后加入Gson依赖,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <!--使用GSON需要先移除默认的jackson-databind--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> </dependency>
由于SpringBoot中默认提供了Gson的自动转换类GsonHttpMessageConverter
,因此Gson的依赖添加成功后,可以像使用jackson-databind
那样直接使用Gson。但是在Gson进行转换时,如果想对日期数据进行格式化,那么还需要开发者自定义HttpMessageConverter
。
你可以通过如下方式来自定义HttpMessageConverter
。首先需要阅读GsonHttpMessageConverter
中的一段源码:
1 2 3 4 public GsonHttpMessageConverter(Gson gson) { Assert.notNull(gson, "A Gson instance is required"); this.gson = gson; }
注意此处Gson的版本为2.8.6,使用了断言来保证必须传入Gson对象。它允许用户可以不提供一个GsonHttpMessageConverter
对象,如果用户没有提供时,SpringBoot会默认提供一个GsonHttpMessageConverter
对象。
在com.envy.webspringboot
包内新建一个config包,接着在里面新建GsonConfig类用于返回一个GsonHttpMessageConverter
对象,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.envy.webspringboot.config; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.json.GsonHttpMessageConverter; import java.lang.reflect.Modifier; @Configuration public class GsonConfig { @Bean GsonHttpMessageConverter gsonHttpMessageConverter(){ GsonHttpMessageConverter converter = new GsonHttpMessageConverter(); GsonBuilder builder = new GsonBuilder(); builder.setDateFormat("yyyy-MM-dd"); builder.excludeFieldsWithModifiers(Modifier.PROTECTED); Gson gson =builder.create(); converter.setGson(gson); return converter; } }
接下来介绍一下上述代码的含义,开发人员首先需要定义一个方法用于提供一个GsonHttpMessageConverter
的实例。然后设置Gson的解析日期时的解析规则,接着设置Gson解析时,修饰符为protected的字段被过滤掉。(这里的过滤方式和jackson-databind
不一样,需要特别注意。),最后就是创建Gson对象,并将其放入到GsonHttpMessageConverter
的实例中并返回converter对象即可。
前面说了Gson解析时被过滤的字段前面是通过使用protected修饰符来修饰的,因此可以新建一个GsonBook类,这里仅仅是将原来Book类中的price字段前面的修饰符修改为protected,相应的代码为:
1 2 3 4 5 6 7 8 9 public class GsonBook { private Integer id; private String name; /**通过设置protected标识符来进行字段的过滤**/ protected Integer price; private Date publishDate; //getter和setter方法,包含三个参数的构造方法 }
既然这样,那可以在BookController中新建一个gsonBook方法,里面的代码为:
1 2 3 4 5 @GetMapping("/gsonbook") public GsonBook gsonBook(){ GsonBook gsonBook = new GsonBook(2,"红楼梦",168,new Date()); return gsonBook; }
运行项目,在浏览器地址栏中输入http://localhost:8080/gsonbook
,即可看到返回了JSON数据:(价格被过滤了,依旧不会显示)
(2)使用fastjson 。fastjson是阿里巴巴开源的一个JSON解析框架,是目前JSON解析速度最快的开源框架,该框架也可以集成到SpringBoot中。不同于Gson的地方在于,fastjson集成完后不能立即使用,需要提供一个HttpMessageConverter
后才能使用。
同样如果你想使用fastjson,需要先除去默认的jackson-databind
,然后加入fastjson依赖,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!--使用fastjson需要先移除默认的jackson-databind--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency>
接着在config包内新建MyFastJsonConfig
类(不能定义为FastJsonConfig
类,因为这个就是它内置的配置类)用于返回一个FastJsonHttpMessageConverter
对象,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.envy.webspringboot.config; import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.nio.charset.Charset; @Configuration public class MyFastJsonConfig { @Bean FastJsonHttpMessageConverter fastJsonHttpMessageConverter(){ FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); FastJsonConfig config = new FastJsonConfig(); config.setDateFormat("yyyy-MM-dd"); config.setCharset(Charset.forName("UTF-8")); config.setSerializerFeatures( SerializerFeature.WriteClassName, SerializerFeature.WriteMapNullValue, SerializerFeature.PrettyFormat, SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteNullStringAsEmpty ); converter.setFastJsonConfig(config); return converter; } }
首先你需要自定义一个MyFastJsonConfig类,用于返回一个FastJsonHttpMessageConverter
实例。接着实例化一个FastJsonConfig
对象,并在里面设置fastjson解析JOSN的一些细节,如日期格式、数据编码、是否在生成的JSON中输出类名、是否输出value为null的数据、生成的JSON格式化、空集合输出[]而非null、空字符串输出””,而不是null等基本配置。
请注意MyFastJsonConfig类配置完后,还需要配置一下响应的编码,否则返回的JSON中文会乱码,你只需要在application.properties
配置文件中添加如下配置:
1 spring.http.encoding.force-response=true
pojo对象可以继续使用前面的GsonBook,自然controller包内的gsonBook方法也是可以使用的。运行一下项目,在浏览器地址栏中输入http://localhost:8080/gsonbook
,即可看到返回了JSON数据:(价格没有被过滤,因此会被显示)
对于FastJsonHttpMessageConverter
的配置,除了上述这种方式外,还有一种方式。在SpringBoot项目中,当开发者引入spring-boot-starter-web
依赖后,该依赖又依赖了spring-boot-autoconfigure
,在这个自动化配置中,有一个WebMvcAutoConfiguration
类提供了对SpringMVC最基本的配置,如果某一项自动化配置不满足开发需求,开发者可以针对该项来自定义配置,此时只需要实现WebMvcConfigurer
接口即可(在Spring5.0之前是通过继承WebMvcConfigurerAdapter
类来实现的),相关代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.envy.webspringboot.config; import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.nio.charset.Charset; import java.util.List; @Configuration public class MyWebMvcConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); FastJsonConfig config = new FastJsonConfig(); config.setDateFormat("yyyy-MM-dd"); config.setCharset(Charset.forName("UTF-8")); config.setSerializerFeatures( SerializerFeature.WriteClassName, SerializerFeature.WriteMapNullValue, SerializerFeature.PrettyFormat, SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteNullStringAsEmpty ); converter.setFastJsonConfig(config); converters.add(converter); } }
这里自定义的MyWebMvcConfig
类实现了WebMvcConfigurer
接口中的configureMessageConverters
方法,然后将自定义的FastJsonHttpMessageConverter
对象加入到converters中。
当然如果你使用了Gson,也是可以采用上面这种方式,但是并不推荐这么做。因为当项目中没有GsonHttpMessageConverter
时,SpringBoot会默认提供一个GsonHttpMessageConverter
,此时你如果重写configureMessageConverters
方法,而参数converters中已经有GsonHttpMessageConverter
的实例了,需要替换已有的GsonHttpMessageConverter
实例,操作比较麻烦,所以对于Gson,推荐直接提供GsonHttpMessageConverter
这种方式。
(3)使用hutool 。Hutool 是一个小而全的Java工具类库,通过静态方法封装,用于降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅。该框架也可以集成到SpringBoot中。同样,hutool集成完后不能立即使用,需要提供一个HttpMessageConverter
后才能使用。
同样如果你想使用hutool,需要先除去默认的jackson-databind
,然后加入hutool依赖,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!--使用hutool需要先移除默认的jackson-databind--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.3.3</version> </dependency>
请注意使用hutool是不需要配置HttpMessageConverter
,因为它内部已经实现了,因此最后可以直接返回String对象。在pojo包内新建一个HutoolBook
类,里面的代码为:
1 2 3 4 5 6 7 public class HutoolBook { private Integer id; private String name; private Integer price; private Date publishDate; //getter和setter方法,包含所有参数的构造方法 }
接着在BookController中新建一个hutoolBook方法,里面的代码为:
1 2 3 4 5 6 7 @GetMapping("/hutoolbook") public String hutoolBook(){ HutoolBook hutoolBook = new HutoolBook(3,"三国演义",null,new Date()); //第二2个true表示跳过空值, 第三个参数表示保持有序 JSONObject jsonObject = JSONUtil.parseObj(hutoolBook,true,true); return jsonObject.toStringPretty(); }
运行项目,在浏览器地址栏中输入http://localhost:8080/hutoolbook
,即可看到返回了JSON数据:(价格被过滤了,因为你这里设置了跳过空值,所以值为null的属性都不会显示)
Hutool默认将日期输出为时间戳,如果需要自定义日期格式,可以修改上述代码为:
1 2 3 4 5 6 7 8 @GetMapping("/hutoolbook") public String hutoolBook(){ HutoolBook hutoolBook = new HutoolBook(3,"三国演义",null,new Date()); //第二2个true表示跳过空值, 第三个参数表示保持有序 JSONObject jsonObject = JSONUtil.parseObj(hutoolBook,true,true); jsonObject.setDateFormat("yyyy-MM-dd"); return jsonObject.toStringPretty(); }
同样,JSONUtil还可以支持以下对象转为JSONObject对象:String对象、Java Bean对象、Map对象、XML字符串(使用JSONUtil.parseFromXml方法)、ResourceBundle(使用JSONUtil.parseFromResourceBundle)等,所以优先建议大家使用hutool,这个工具配置和使用非常简单。
静态文件上传 在SpringMVC中,对于静态资源都需要开发者手动配置静态资源过滤。SpringBoot中对此提供了自动化配置,可以简化静态资源过滤配置。
默认策略 前面介绍过SpringBoot对于SpringMVC的自动化配置都是在WebMvcAutoConfiguration
类中,因此默认的静态资源过滤策略可以从这个类中一探究竟。
在WebMvcAutoConfiguration
类中有一个静态内部类WebMvcAutoConfigurationAdapter
,同样它也实现了前面提到的WebMvcConfigurer
接口。而WebMvcConfigurer
接口中有一个addResourceHandlers
方法,该方法用于配置静态资源过滤。这个addResourceHandlers
方法在WebMvcAutoConfigurationAdapter
类中得到了实现,里面有一段比较核心的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 public void addResourceHandlers(ResourceHandlerRegistry registry) { //其他代码 String staticPathPattern = this.mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}) .addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties .getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)) .setCacheControl(cacheControl)); } } }
SpringBoot在这里进行了默认的静态资源过滤配置,其中staticPathPattern
默认定义在WebMvcProperties
中(通过无参的构造方法来进行设置),定义的内容如下:
1 2 3 4 5 public WebMvcProperties() { ...... this.staticPathPattern = "/**"; ...... }
回到addResourceHandlers
方法中,看下面这行代码:this.resourceProperties.getStaticLocations();
,它获取到的默认静态资源位置定义在ResourceProperties
类中,相应的代码为:
1 private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
看到没有,这里一共有4个方法,但是这个WebMvcAutoConfiguration.getResourceLocations()
方法又对这4个静态资源位置进行了扩充,代码如下:
1 2 3 4 5 6 static String[] getResourceLocations(String[] staticLocations) { String[] locations = new String[staticLocations.length + SERVLET_LOCATIONS.length]; System.arraycopy(staticLocations, 0, locations, 0, staticLocations.length); System.arraycopy(SERVLET_LOCATIONS, 0, locations, staticLocations.length, SERVLET_LOCATIONS.length); return locations; }
再来看一下这行代码private static final String[] SERVLET_LOCATIONS = new String[]{"/"};
,它告诉我们这个SERVLET_LOCATIONS
其实是一个字符串数组,值为{"/"}
。
通过前面的分析可以知道,SpringBoot默认会过滤所有的静态资源,而静态资源一共有5个位置,分别为:classpath:/META-INF/resources/
、classpath:/resources/
、classpath:/static/
、classpath:/public/
、和/
,也就是说开发者可以将静态资源放到这5个位置中的任意一个。注意需要按照定义的顺序,5个静态资源位置的优先级依次降低。但是通常SpringBoot项目不需要webapp目录,所以第5个/
一般不会考虑。
你可以尝试按照上面的方式来创建对应的4个目录,4个目录中放入同名的静态资源(序号表示优先级):
启动项目,在浏览器地址栏中输入http://localhost:8080/avatar.jpg
就可以访问到classpath:/META-INF/resources/
目录下的avatar.jpg
。如果将classpath:/META-INF/resources/
目录下的avatar.jpg
删除掉,那么访问的就是classpath:/resources/
目录下的avatar.jpg
文件,以此类推。如果你使用IDEA来创建SpringBoot项目,那么它会默认创建出classpath:/static/
,也就是序号3对应的路径,此时静态资源一般就放在这个目录下。
自定义策略 如果默认的静态资源过滤策略不能满足开发需求,也可以自定义静态资源过滤策略,同样自定义静态资源过滤策略有两种方式:
(1)在配置文件中定义。 这种方式是最简单的,只需要在application.properties
配置文件中直接定义过滤规则和静态资源位置:
1 2 3 4 # 设置访问路径前缀 spring.mvc.static-path-pattern=/static/** # 设置静态资源路径 spring.resources.static-locations=classpath:/static/
这里设置了过滤规则为/static/**
,静态资源位置为classpath:/static/
。
启动项目,在浏览器地址栏中输入http://localhost:8080/static/avatar.jpg
就可以访问到classpath:/static/
目录下的avatar.jpg
资源文件。
(2)通过java代码定义。 其实也可以通过书写一个类,只要它实现WebMvcConfigurer
接口中的addResourceHandlers
方法即可,相应的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.envy.webspringboot.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /**静态文件访问配置**/ @Configuration public class MyResourceWebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //第一个方法设置访问路径前缀,第二个方法设置资源路径 registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); } }
同样启动项目,在浏览器地址栏中输入http://localhost:8080/static/avatar.jpg
依然可以访问到classpath:/static/
目录下的avatar.jpg
资源文件。
文件上传 SpringMVC对文件上传做了简化,在SpringBoot中这种简化就更彻底了,几乎是零配置。
Java中文件的上传涉及到两个组件:CommonsMultipartResolver
和StandardServletMultipartResolver
。其中CommonsMultipartResolver
使用commons-fileupload
来处理mutilpart请求,而StandardServletMultipartResolver
则是基于Servlet3.0来处理mutilpart请求,所以如果你使用CommonsMultipartResolver
,则不需要添加额外的Jar包。众所周知,Tomcat自7.0版本开始就支持Servlet3.0,而此处使用的SpringBoot版本为2.1.14,内嵌的Tomcat版本为9.0.34,所以其实不用添加额外的Jar包,也是可以直接使用StandardServletMultipartResolver
的。SpringBoot提供的文件上传自动配置类MultipartAutoConfiguration
中,默认也是使用了StandardServletMultipartResolver
,部分源码为:
1 2 3 4 5 6 7 8 9 @Bean( name = {"multipartResolver"} ) @ConditionalOnMissingBean({MultipartResolver.class}) public StandardServletMultipartResolver multipartResolver() { StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver(); multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily()); return multipartResolver; }
注意到没有,它使用了@ConditionalOnMissingBean
注解,也就是说如果开发者没有提供MultipartResolver
,那么就会采用默认的MultipartResolver
也就是StandardServletMultipartResolver
。所以前面就说了,SpringBoot基本上做到了上文文件时的零配置。
单文件上传 首先创建一个SpringBoot的Web项目(名称为filespringboot),然后在resources目录的static目录下新建一个fileupload.html
文件,该文件用于测试单文件上传:
1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>单文件上传</title> </head> <body> <form action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="uploadFile" value="请选择文件"> <input type="submit" value="开始上传"> </form> </body> </html>
这个页面非常的简单,就是一个演示单文件上传的demo,上传使用的接口是/upload
,请求方式是Post,这个name控件中的uploadFile
就是后续action方法中传入的MultipartFile
对象。特别注意就是需要设置enctype="multipart/form-data"
,这个配置非常重要,博主曾经就因为这个而导致无法修复Bug。。。
接着就是新建一个FileUploadController
,用于处理文件上传接口,这样我们让文件上传后返回一个String格式的URL地址。里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package com.envy.filespringboot.controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.UUID; @RestController public class FileUploadController { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd/"); @PostMapping("/upload") public String upload(MultipartFile uploadFile, HttpServletRequest request){ String realPath = request.getSession().getServletContext().getRealPath("/uploadFile/"); String format = simpleDateFormat.format(new Date()); File folder = new File(realPath+format); if(!folder.isDirectory()){ folder.mkdirs(); } String oldName = uploadFile.getOriginalFilename(); String newName = UUID.randomUUID().toString()+oldName.substring(oldName.lastIndexOf("."), oldName.length()); try{ uploadFile.transferTo(new File(folder,newName)); String filePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+"/uploadFile/"+format+newName; return filePath; }catch (IOException e){ e.printStackTrace(); } return "文件上传失败"; } }
解释一下上面代码的含义:首先我们设置上传文件的保存路径为项目运行目录下的uploadFile
文件夹(目前就先这样,后期会结合自定义静态资源访问路径来让上传的文件保存在static目录下),并通过日期对所上传的文件进行归类保存。同时为避免文件名称重复,使用UUID给上传的文件进行了重命名。然后就是一些文件的保存操作。最后就是生成上传文件的访问路径,并将访问路径返回。
启动项目,在浏览器地址栏中输入http://localhost:8080/fileupload.html
即可访问到该页面,选择一张图片后,点击上传按钮,即可返回对应的URL地址:
你可能会好奇这里的图片怎么直接能被访问呢?其实静态资源位置除了classpath下面的4个路径之外,还有一个”/“,而此处的图片就是放在了”/“下,因此这里的图片虽说是静态资源,却可以直接访问的到。
这样一个简单的图片上传逻辑就完成了,开发者并没有花太多的时间在配置上,而是专注于图片逻辑的实现。
当然如果你想对图片上传的细节进行配置,如设置上传图片的大小限制,本地存放地址等,这些都是可以的。你只需要在application.properties
配置文件中添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 # 是否开启文件上传支持,默认为true spring.servlet.multipart.enabled=true # 文件写入磁盘的阈值,默认为0 spring.servlet.multipart.file-size-threshold=0 # 上传文件的临时保存位置 spring.servlet.multipart.location=F:\\temp # 上传的单个文件的最大大小,默认为1MB spring.servlet.multipart.max-file-size=1MB # 多文件上传时文件的总大小,默认为10MB spring.servlet.multipart.max-request-size=10MB # 文件是否延迟解析,默认为false spring.servlet.multipart.resolve-lazily=false
多文件上传 说完了单文件的上传,接下来介绍多文件的上传,它几乎和单文件上传一致。首先新建一个uploads.html文件,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>多文件上传</title> </head> <body> <form action="/uploads" method="post" enctype="multipart/form-data"> <input type="file" name="uploadFiles" value="请选择文件" multiple> <input type="submit" value="开始上传"> </form> </body> </html>
这个页面也简单,就是一个演示多文件上传的demo,上传使用的接口是/uploads,请求方式是Post,这个name控件中的uploadFiles就是后续action方法中传入的MultipartFile[]
对象。特别注意就是需要设置enctype="multipart/form-data"
和在input输入框后使用multiple
来标明这个是多文件上传按钮。
接着在FileUploadController
类中新建一个uploads方法,用于处理多文件上传的逻辑。同样文件上传成功后返回一个JSON格式的数组,里面包含各个图片的访问地址。里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /** * 多文件上传 * **/ @PostMapping("/uploads") public String uploads (MultipartFile[] uploadFiles, HttpServletRequest request){ String realPath = request.getSession().getServletContext().getRealPath("/uploadFile/"); String format = simpleDateFormat.format(new Date()); File folder = new File(realPath+format); if(!folder.isDirectory()){ folder.mkdirs(); } List<String>stringList = new ArrayList<>(); for(MultipartFile uploadFile:uploadFiles){ String oldName = uploadFile.getOriginalFilename(); String newName = UUID.randomUUID().toString()+oldName.substring(oldName.lastIndexOf("."), oldName.length()); try{ uploadFile.transferTo(new File(folder,newName)); String filePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+"/uploadFile/"+format+newName; stringList.add(filePath); }catch (IOException e){ e.printStackTrace(); return "文件上传失败"; } } JSONArray jsonArray = JSONUtil.parseArray(stringList); return jsonArray.toString(); }
这里其实和单文件的逻辑几乎一致,唯一不同之处,在于多了一个遍历uploadFiles的步骤,以及最后将java list对象序列化为jsonarray对象。
@ControllerAdvice 接下来介绍@ControllerAdvice
注解相关的内容。顾名思义,@ControllerAdvice
就是@Controller
的增强版,因为在SpringMVC中Advice就是通知,增强的意思,特别是在AOP中,用的更为广泛。@ControllerAdvice
主要用来处理全局数据,一般搭配@ExceptionHandler
、@ModelAttribute
以及@InitBinder
使用。这个在日常工作过程中使用的还是较为频繁,特别是搭配@ExceptionHandler
用于处理全局异常,在前后端分离的情况下,对于返回统一的数据格式显得尤为重要。
全局异常处理 @ControllerAdvice
最常见的使用场景就是全局异常处理。前面通过在aplication.properties
配置文件中设置了上传文件大小的限制的配置,如果用户上传的文件超过了限制大小,此时就会抛异常,此时你就可以通过@ControllerAdvice
结合@ExceptionHandler
注解来定义全局异常捕获机制。在com.envy.filespringboot
包内新建一个handler
包,接着在里面新建一个CustomExceptionHandler
类,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 @ControllerAdvice public class CustomExceptionHandler { @ExceptionHandler(MaxUploadSizeExceededException.class) public void uploadException(MaxUploadSizeExceededException e, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=utf-8"); PrintWriter out = response.getWriter(); out.write("上传文件大小超出限制"); out.flush(); out.close(); } }
只需要在系统中定义CustomExceptionHandler
类,然后添加@ControllerAdvice
注解即可。当系统启动时,该类就会被扫描到Spring容器中,然后定义uploadException
方法,需要在该方法上添加@ExceptionHandler
注解,其中定义的MaxUploadSizeExceededException.class
表明该方法用来处理MaxUploadSizeExceededException
类型的异常。如果想让该方法处理所有的异常,只需将MaxUploadSizeExceededException
换成Exception
即可。方法的参数可以有异常实例、HttpServletResponse
、HttpServletRequest
、Model
等。同样它的返回值可以是一个JSON对象、一个ModelAndView
、一个逻辑视图名等。下现在尝试启动项目,上传一个超大的文件,看是否有错误提示给用户:
如果返回参数是一个ModelAndView
,假设使用的页面模板为Thymeleaf
(注意你需要在pom.xml文件中添加Thymeleaf
相关依赖),此时异常处理方法定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 /** * 使用了Thymeleaf模板 * **/ @ControllerAdvice public class CustomExceptionHandler { @ExceptionHandler(MaxUploadSizeExceededException.class) public ModelAndView uploadException(MaxUploadSizeExceededException e) throws IOException { ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("msg","上传文件大小超出限制"); modelAndView.setViewName("error"); return modelAndView; } }
同时为了让错误信息得到显示,你还需要在resources/templates
目录下新建一个error.html
文件:
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html> <html lang="en" xmlns="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>错误信息</title> </head> <body> <div th:text="${ msg }"> </div> </body> </html>
然后运行项目,可以发现这个也能实现之前相同的效果。
添加全局数据 @ControllerAdvice
是一个全局数据处理组件,因此也可以在@ControllerAdvice
中配置全局数据,使用@ModelAttribute
注解进行配置,你可以新建一个GlobalConfig
类,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 @ControllerAdvice public class GlobalConfig { @ModelAttribute(value = "info") public Map<String,String> userInfo(){ Map<String,String> map =new HashMap<>(); map.put("username","吴承恩"); map.put("gender","男"); return map; } }
这里定义了一个全局配置类GlobalConfig
,并添加了userInfo方法,返回一个map对象。该方法有一个 @ModelAttribute
注解,其中的value属性表示这条返回数据的key,而方法的返回值是返回数据的value。这样在任意请求的Controller中,通过方法参数中的Model都可以获取info的数据。在之前的FileUploadController
类中新增一个hello方法,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** * 演示ModelAttribute信息获取 * **/ @GetMapping("/hello") public void hello(Model model){ Map<String,Object> map = model.asMap(); Set<String> keySet= map.keySet(); Iterator<String> iterator = keySet.iterator(); while (iterator.hasNext()){ String key = iterator.next(); Object value = map.get(key); System.out.println(key+">>>>>>"+value); } }
这个Model是一个接口,其中定义了一些方法,此处就是使用了它的asMap方法:
1 2 3 4 5 6 7 8 9 public interface Model { Model addAttribute(String var1, @Nullable Object var2); Model addAttribute(Object var1); Model addAllAttributes(Collection<?> var1); Model addAllAttributes(Map<String, ?> var1); Model mergeAttributes(Map<String, ?> var1); boolean containsAttribute(String var1); Map<String, Object> asMap(); }
然后启动项目,访问hello方法,可以发现控制台输出以下信息:
1 info>>>>>>{gender=男, username=吴承恩}
可以发现这个key确实是info,也就是前面@ModelAttribute
中设置的value的值。
请求参数预处理 @ControllerAdvice
结合@InitBinder
还能实现请求参数预处理,即将表单中的数据绑定到实体类上时进行一些额外的处理。
在pojo包中定义两个实体类Book和Author,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.envy.filespringboot.pojo; public class Book { private String name; private String author; //getter、setter和toString方法 } public class Author { private String name; private int age; //getter、setter和toString方法 }
可能你也注意到此时故意在Book和Author类中定义了同名属性name,这个在实际开发过程中也是经常会遇到的问题。现在需要在Controller上接收两个实体类的数据,那么可以在Controller中定义一个book方法,里面的代码为:
1 2 3 4 5 6 7 /** * 请求参数预处理 * **/ @GetMapping("book") public String book(Book book, Author author){ return book.toString()+" >>>>>> "+author.toString(); }
那么在参数传递时,两个实体类中的name属性就会被混淆,此时你可以使用@ControllerAdvice
结合@InitBinder
注解就能解决这个问题。配置步骤如下。首先给Controller中book方法的参数 添加@ModelAttribute
注解,代码如下:
1 2 3 4 5 6 7 /** * 请求参数预处理 * **/ @GetMapping("book") public String book(@ModelAttribute("b")Book book,@ModelAttribute("a") Author author){ return book.toString()+" >>>>>> "+author.toString(); }
既然使用了@ModelAttribute
注解,那么需要定义一个GlobalConfig
配置类,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /** * 配合InitBinder注解 * **/ @ControllerAdvice public class GlobalConfig { @InitBinder("b") public void init(WebDataBinder binder){ binder.setFieldDefaultPrefix("b."); } @InitBinder("a") public void init2(WebDataBinder binder){ binder.setFieldDefaultPrefix("a."); } }
解释一下上述代码的含义,在GlobalConfig
类中定义了两个方法:第一个@InitBinder("b")
表示该方法是处理@ModelAttribute("b")
对应的参数,第二个@InitBinder("a")
表示该方法是处理@ModelAttribute("a")
对应的参数。然后在每个init方法中都给相应的Filed设置了一个前缀,然后运行项目,在浏览器地址栏中输入http://localhost:8080/book?b.name=西游记&b.author=吴承恩&a.name=罗贯中&a.age=188
,这样就能成功地区分出name的属性。浏览器页面输出:
1 Book{name='西游记', author='吴承恩'} >>>>>> Author{name='罗贯中', age=188}
当然在WebDataBinder
对象中,还可以设置允许的字段、禁止的字段、必填字段以及验证器等等。