MVC框架
SpringMVC是一种基于Java,实现了Web MVC设计模式,请求驱动类型的轻量级Web框架,即使用了MVC架构模式的思想,将Web层进行职责解耦。基于请求驱动指的就是使用请求-响应模型,框架的目的就是帮助我们简化开发,SpringMVC也是要简化我们日常Web开发。
引入依赖
SpringBoot 集成SpringMVC框架并实现自动配置,只需要在pom中添加以下依赖即可,不需要其他任何配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
JSON配置
JSON是目前主流的前后端数据传输方式,SpringMVC中使用消息转换器HttpMessageConverter对JSON 的转换提供了很好的支持。添加上述的依赖后,我们的项目就可以返回一段JSON了。
默认使用
@Data
public class Table {
//主键
private Integer id;
//名称
private String name;
//时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date date;
}
@RestController
public class TableController {
@Resource
private TableDao tableDao;
/**
* @return java.util.List<com.fengfan.web.entity.Table>
* @description 查询所有数据
* @author fengfan
* @date 2022/5/10 14:23
*/
@PostMapping("/queryAll")
public List<Table> queryAll() {
return tableDao.queryAll(new Table());
}
}
@SpringBootTest
public class TableControllerTest {
@Resource
private WebApplicationContext wac;
private MockMvc mockMvc;
@BeforeEach
public void init(){
//初始化MockMvc对象
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void queryAll() throws Exception {
String str = mockMvc.perform(MockMvcRequestBuilders.post("/queryAll"))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
System.out.println(str);
}
}
这是SpringBoot自带的处理方式,通过Spring中默认提供的MappingJackson2HttpMessageConvert类来实现的。如果采用这种方式,那么对于字段忽略、日期格式化等常见需求都可以通过注解来解决。
自定义转换器
常见的JSON处理器除了jackson-databind外,还有 Gson、fastjson等。
使用Gson
- 添加依赖
<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的配置类GsonHttpMessageConvertersConfiguration,因此Gson的依赖添加成功后,可以像使用jackson-databind那样直接使用。但是如果想对日期数据进行格式化,那么还需要自定义HttpMessageConverter,代码如下:
@Configuration
public class WebBeanConfig {
/**
* Gson配置
* @return
*/
@Bean
GsonHttpMessageConverter gsonHttpMessageConverter() {
GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setDateFormat("yyyy-MM-dd HH:mm:ss");
gsonBuilder.excludeFieldsWithModifiers(Modifier.PROTECTED);
converter.setGson(gsonBuilder.create());
return converter;
}
}
使用fastjson
fastjson是阿里巴巴的一个开源JSON框架是目前JSON解析速度最快的开源框架,该框架也可以集成到 Spring Boot 中。不同于Gson, fastjson承完成之后并不能立马使用, 需要提供相应的httpMessageConverter后才能使用,配置如下:
- 添加依赖
<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.70</version>
</dependency>
- 对于FastJsonHttpMessageConverter的配置,有一个WebMvcAutoConfiguration类提供了对
SpringMVC最基本的配置,如果某一项自动化配置不满足开发需求,可以针对该项自定义配置,只需要实现WebMvcConfigurer对应的接口即可,配置如下:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* fastJson配置
*
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//创建fastJson消息转换器
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
//新版本(当前版本1.2.70)需要配置支持的MediaType, 不然就会报错
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML);
supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
supportedMediaTypes.add(MediaType.APPLICATION_PDF);
supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML);
supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
supportedMediaTypes.add(MediaType.APPLICATION_XML);
supportedMediaTypes.add(MediaType.IMAGE_GIF);
supportedMediaTypes.add(MediaType.IMAGE_JPEG);
supportedMediaTypes.add(MediaType.IMAGE_PNG);
supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM);
supportedMediaTypes.add(MediaType.TEXT_HTML);
supportedMediaTypes.add(MediaType.TEXT_MARKDOWN);
supportedMediaTypes.add(MediaType.TEXT_PLAIN);
supportedMediaTypes.add(MediaType.TEXT_XML);
fastConverter.setSupportedMediaTypes(supportedMediaTypes);
FastJsonConfig config = new FastJsonConfig();
config.setDateFormat("yyyy-MM-dd HH:mm:ss");
//格式化的规则
config.setSerializerFeatures(
SerializerFeature.WriteDateUseDateFormat,
SerializerFeature.WriteNullListAsEmpty,
SerializerFeature.WriteMapNullValue
);
fastConverter.setFastJsonConfig(config);
converters.add(fastConverter);
}
}
静态资源访问
在SpringMVC中,对于静态资源都需要手动配置静态资源过滤。SpringBoot中对此也提供自动化配置,可以简化静态资源过滤配置。
默认配置
在WebMvcAutoConfiguration类中有一个静态内部类WebMvcAutoConfigurationAdapter实现了
WebMvcConfigurer接口。WebMvcConfigurer接口中有一个个方法addRsourceHandlers,
是用来配置静态资源过滤的。方法在WebMvcAutoConfigurationAdapter中得到了实现。
其中默认添加了下面几个文件路径:
在registration.addResourceLocations(resource)中添加了"/“路径
综上可以得到,SpringBoot过滤所有的静态资源,而静态资源的位置一共有5个,分别是"classpath:/META-INF/resources”、“classpath: /resources/”、“classpath: /static/”、“classpath :/public/”、“/”。也就是可以将静态资源放到这5个位置中的任意一个,其中按照定义的顺序优先级依次降低。一般情况下SpringBoot项目不需要webapp目录,所以第5个"/"可以暂不考虑。
自定义配置
如果默认静态资源过滤策略不能满足需求 ,也可以自定义静态资源过滤策略。
- 在配置文件中配置
spring:
mvc:
static-path-pattern: /static/**
过滤规则为/static/**,静态资源位置为classpath:/static/
- 代码定义
只需要实WebMvcConfigurer接口即可,然后实现接口中的addResourceHandlers方法,代码如下:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 静态资源过滤配置
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
}
异常处理
在SpringBoot默认情况下,如果用户在发起请求时发生了错误,SpringBoot 会有一些默认的页面展示给用户,如下所示:
Controller异常处理
- @RestControllerAdvice或者最常见的使用场景:当我们的controller抛出一个Exception,此时可以通过@RestControllerAdvice结合@ExceptionHandler定义全局异常捕获机制,然后返回给客户端一个页面或者json描述。缺点:只能捕获请求进入了controller的异常,再之前的异常和一些Filter的异常无法捕获。
@Data
public class WebResponse <T> implements Serializable {
private int code;
private String type;
private String message;
private T data;
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public WebResponse exceptionHandler(Exception e) {
WebResponse webResponse = new WebResponse();
webResponse.setCode(-1);
webResponse.setType("error");
webResponse.setMessage(e.getMessage());
return webResponse;
}
}
- 在SpringMVC中HandlerExceptionResolver接口负责统一异常处理,这里使用其实现类AbstractHandlerExceptionResolver来实现。
@Configuration
public class WebHandlerExceptionResolver extends AbstractHandlerExceptionResolver {
@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
WebResponse webResponse = new WebResponse();
webResponse.setCode(-1);
webResponse.setType("error");
webResponse.setMessage(ex.getMessage());
mv.addAllObjects(webResponse.toMap());
return mv;
}
}
public class WebBeanConfig {
/**
* 异常处理类配置
* @return
*/
@Bean
public WebHandlerExceptionResolver webHandlerExceptionResolver() {
return new WebHandlerExceptionResolver();
}
}
非Controller抛出的异常
上述两种方法一般用来处理应用级别的异常,有一些不是controller抛出的异常就处理不了,例如Filter中抛出异常等。因此,SpringBoot提供BasicErrorControll作为默认的ErrorController,我们如果需要更灵活地对Error视图和数据进行处理,那么只需要提供自己的ErrorController即可。
@RestController
public class WebErrorController extends BasicErrorController {
public WebErrorController(ErrorAttributes errorAttributes) {
super(errorAttributes, new ErrorProperties());
}
@Override
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
WebResponse webResponse = new WebResponse();
webResponse.setCode(-1);
webResponse.setType("error");
webResponse.setMessage((String) body.get("message"));
mv.addAllObjects(webResponse.toMap());
mv.setStatus(getStatus(request));
return mv;
}
@Override
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
WebResponse webResponse = new WebResponse();
webResponse.setCode(-1);
webResponse.setType("error");
webResponse.setMessage((String) body.get("message"));
return new ResponseEntity<Map<String, Object>>(webResponse.toMap(), getStatus(request));
}
@Override
protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
ErrorAttributeOptions options = ErrorAttributeOptions.of(ErrorAttributeOptions.Include.EXCEPTION,
ErrorAttributeOptions.Include.STACK_TRACE, ErrorAttributeOptions.Include.MESSAGE,
ErrorAttributeOptions.Include.BINDING_ERRORS);
if (getErrorProperties().isIncludeException()) {
options = options.including(ErrorAttributeOptions.Include.EXCEPTION);
}
if (isIncludeStackTrace(request, mediaType)) {
options = options.including(ErrorAttributeOptions.Include.STACK_TRACE);
}
if (isIncludeMessage(request, mediaType)) {
options = options.including(ErrorAttributeOptions.Include.MESSAGE);
}
if (isIncludeBindingErrors(request, mediaType)) {
options = options.including(ErrorAttributeOptions.Include.BINDING_ERRORS);
}
return options;
}
}
@WebFilter("/*")
public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("init");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("doFilter");
throw new RuntimeException("我是Filter抛出的异常,测试BasicErrorController的作用");
// filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
CORS支持
CORS (Cross-Origin Resource Sharing)是由W3C制定的一种跨域资源共享技术标准,其目的
就是为了解决前端的跨域请求,一般我们都是使用代理来解决跨域问题,一般不需要在代码中配置。
@CrossOrigin
@PostMapping("/queryAll")
@CrossOrigin(value = "http://localhost:8080", maxAge = 1800, allowedHeaders = "*")
public List<Table> queryAll() {
return tableDao.queryAll(new Table());
}
- value:表示支持的域,这里表示来自localhost:8080域的请求是支持跨域的
- maxAge:请求的有效期
- allowedHeaders:允许的请求头,*表示所有的请求头都被允许
代码配置
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 跨域配置
*
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/web/**")
.allowedHeaders("*")
.allowedMethods("*")
.maxAge(1800)
.allowedOrigins("http://localhost:8080");
}
}
注册拦截器
public class InterceptorTest implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("InterceptorTest-------preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("InterceptorTest-------postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
System.out.println("InterceptorTest-------afterCompletion");
}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 拦截器配置
*
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new InterceptorTest())
.addPathPatterns("/**")
.excludePathPatterns("/query");
}
}
拦截器中的方法将按 preHandle→Controller→postHandle→afterCompletion的顺序执行。注意,只有 preHandle方法返回true时后面的方法才会执行。当拦截器链内存在多个拦截器时,postHandler在拦截器链内的所有拦截器返回成功时才会调用,而afterCompletion只有preHandle返回true才调用,但若拦截器链内的第一个拦截器preHandle方法返回false后面的方法都不会执行。
启动系统任务
有一些特殊的任务需要在系统启动时执行,例如配置文件加载、数据库初始化等操作。如果没有使用SpringBoot,这些问题可以在Listener中解决。SpringBoot对此提供了两种解决方案:CommandLineRunner和ApplicationRunner。CommandLineRunner和ApplicationRunner基本一致
差别主要体现在参数上。
CommandlineRunner
@Component
@Order(1)
public class WebCommandlineRunner1 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("我是Runner1,参数为" + Arrays.toString(args));
}
}
@Component
@Order(1)
public class WebCommandlineRunner2 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("我是Runner2,参数为" + Arrays.toString(args));
}
}
- Order:用来描述CommandLineRunner的执行顺序,数字越小越先执行
- run方法:是调用的核心逻辑,参数是系统启动时传入的参数,即入口类main方法的参数
ApplicationRunner
@Component
@Order(1)
public class WebApplicationRunner1 implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("我是WebApplicationRunner1,参数为" + args.getNonOptionArgs());
}
}
@Component
@Order(2)
public class WebApplicationRunner2 implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("我是WebApplicationRunner2,参数为" + args.getNonOptionArgs());
}
}
- Order:执行顺序 ,数字越小越优先执行
- 这里run方法的参数是ApplicationArguments对象,如果想从ApplicationArguments对象中获取入口类中 main方法接收的参数,调 ApplicationArguments中的getNonOptionArgs方法即可。ApplicationArguments中的getOptionNam方法用来获取项目启动命令行中参数key,getOptionNames方法获取到的是name,getOptionValues则是获取相应的value。
Servlet、Filter、Listener配置
@WebServlet("/testServlet")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("Servlet-------测试servlet的使用");
}
}
@WebFilter("/*")
public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filterr-------测试Filter的使用");
// throw new RuntimeException("我是Filter抛出的异常,测试BasicErrorController的作用");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
@WebListener
public class TestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("Listener-------requestDestroyed");
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("Listener-------requestInitialized");
}
}
在项目入口类上添加@ServletComponentScan注解,实现对 Servlet、Filter以及Listener的扫描
REST
REST(Representational State Transfer)是Web软件架构风格,它是一种风格,而不是标准,匹配或兼容这种架构风格的网络服务称为REST服务。REST服务简洁并且有层次,REST通常基于 HTTP、URI和XML以及HTML这些现有的广泛流行的协议和标准。在 REST 中,资源是URI 来指定的,对资源的增删改查操作可以通过HTTP协议提供的GET、POST、PUT、DELETE等方法实现。使用REST可以更高效地利用缓存来提高响应速度,同时REST中的通信会话状态由客户端来维护,这可以让不同的服务器 处理一系列请求中的不同请求,进而提高服务器的扩展性。前后端分离项目中,一个设计良好的Web软件架构必然要满足REST风格。SpringMVC框架中,开发者可以通过@RestController注解开发一个RESTful 服务。
标识资源
将资源名称放到 也中,如果资源有层级关系,则放入层级关系:
http://localhost:8080/web/api/table
确定HTTP Method
在REST中,HTTP Method常常对应以下含义:
- POST:代表增加资源;
- PUT:代表更改资源,客户端提供需完整的资源属性;
- GET:代表查询资源;
- PATCH:更新资源,客户端提供仅需要更改的资源属性;
- DELETE:通常用于删除资源;
- HEAD:类似GET,但仅仅只有 HTTP 头信息,头信息包含了需要查找的信息;
- OPTIONS:用于获取URI所支持的方法,响应信息会在HTTP头中包含一个个名为Allow
的头,值是所支持的方法,如GET、POST;
确定HTTP Status
服务器向客户端返回HTPP Status以表示操作是否成功,常用的如下:
- 200:0K,用户请求成功,如查询数据成功返回;
- 400:错误的的请求,URI匹配上SpringBoot中的Controller,但方法参
数匹配错误,就会抛出错误; - 404:NOT Found,用户发出的请求针对的资源不存在;
- 405:用来访问本页面的HTTP Method不被允许,比如通过HTTP GET方式访问了一个@PostMapping Controller 方法;
- 406:表示无法使用请求的内容特性来响应请求的资源,比如在SpringBoot中,请求后缀以html结尾,但同时请求的HTTP头中又包含了Accept:application/json;
- 500:服务器内部错误,无法完成请求,通常是 Controller抛出的异常。
SpringBoot集成REST
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
代码实现
server:
servlet:
context-path: /rest
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: none
database: mysql
show-sql: true
mvc:
hiddenmethod:
filter:
enabled: true
@Data
@Entity
public class Student {
//主键
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
//名称
private String name;
//时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date time;
}
@RepositoryRestResource
public interface StudentRepository extends JpaRepository<Student, Integer> {
}
自定义路径
@RepositoryRestResource(path = "teachers", collectionResourceRel = "t", itemResourceRel = "teacher")
public interface StudentRepository extends JpaRepository<Student, Integer> {
}
@RepositoryRestResource注解:
- path:表示将所有请求路径中的students都修改为teachers;
- collectionResourceRel:表示将返回的JSON集合中students集合的key修改为t
- itemResourceRel:表示将返回JSON集合中的单个student的key修改为teacher