SpringMVC是一种基于Java的Web框架,?用于构建Web应用程序。?
SpringMVC的核心在于其设计模式——MVC(?Model-View-Controller)?,?这种设计模式将应用程序的数据处理、?用户接口和控制逻辑分开,?使得代码结构更加清晰,?便于维护和扩展。?在SpringMVC中,?Model代表数据和相关的业务逻辑,?View负责显示数据给用户,?而Controller则是协调Model和View的桥梁,?处理用户请求并返回相应的视图。
MVC全称Model View Controller,是一种设计创建Web应用程序的模式。这三个单词分别代表Web应用程序的三个部分:
(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...)
HandlerMapping负责根据用户请求url找到Handler即处理器,springmvc提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。
Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。
ViewResolver通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。
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对象,从从而将相应结果渲染给客户。
<!-- 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>
<?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>
<%@ 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>
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;
}
}
<%@ 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>
<?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>
(1)Tomcat9.0下载 https://tomcat.apache.org/download-90.cgi
(2)IDEA配置Tomcat
打开配置选项
找到左侧Tomcat图标,新建,选择下载好并解压的Tomcat路径
部署
正常运行,成功
(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)日志输出格式
Spring MVC 框架中的常用注解主要包括在控制器层(Controller)、服务层(Service)、数据访问层(Repository)、实体类(Entity)、请求参数(Request Parameters)等方面。以下是这些注解的主要含义和用例 @Autowired、@ComponentScan、@Configuration 和 @Bean 是 Spring 框架中常用的注解,用于实现依赖注入和配置管理。
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) {
// ...
}
}
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) {
// ...
}
}
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();
}
}
1、@Repository: 含义: 标识一个类为数据访问层的组件,通常与 Spring 的数据访问异常转换一起使用。 用例:
@Repository
public class MyRepository {
// Repository methods
}
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
}
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
}
}
含义: 用于自动装配,将指定类型的 Bean 注入到属性、构造函数或方法参数中。 用例:
@Service
public class MyService {
private final MyRepository repository;
@Autowired
public MyService(MyRepository repository) {
this.repository=repository;
}
}
在上例中,MyService 类通过 @Autowired 注解将 MyRepository 类型的 Bean 自动注入到构造函数中。
含义: 扫描指定包路径,寻找标有 @Component、@Service、@Repository、@Controller 注解的类,并将其注册为 Spring Bean。 用例:
@Configuration
@ComponentScan(basePackages="com.example")
public class AppConfig {
// Configuration content
}
在上例中,@ComponentScan 注解扫描 com.example 包路径下的所有类,将带有相应注解的类注册为 Spring Bean。
含义: 声明当前类是一个配置类,通常与 @Bean 注解一起使用,用于配置 Spring 应用上下文。 用例:
@Configuration
public class AppConfig {
// Bean declarations using @Bean
}
在上例中,AppConfig 被声明为配置类,用于定义 Spring 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。
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>
@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来进行处理。
@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响应中。
就是利用容器自身的默认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去进行处理
<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相应状态码,提示客户端使用浏览器缓存的数据,而非将静态资源的内容输出到客户端,以充分节省带宽,提高程序性能。
第一种方式是要定义的Interceptor类要实现Spring的HandlerInterceptor 接口
第二种方式是继承实现了抽象类HandlerInterceptorAdapter
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;
}
}
<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>
@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";
}
}
<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>
直接访问success页面被拦截
访问登录页面,因为配置了不进行拦截的路径,所以显示如下
输入账号密码登录成功
对于@ControllerAdvice,我们比较熟知的用法是结合@ExceptionHandler用于全局异常的处理,但其作用不仅限于此。ControllerAdvice拆分开来就是Controller Advice,关于Advice,前面我们讲解Spring Aop时讲到,其是用于封装一个切面所有属性的,包括切入点和需要织入的切面逻辑。这里ContrllerAdvice也可以这么理解,其抽象级别应该是用于对Controller进行“切面”环绕的,而具体的业务织入方式则是通过结合其他的注解来实现的。@ControllerAdvice是在类上声明的注解,其用法主要有三点:
在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的这三种使用方式分别进行讲解。
系统比较庞大时很多的异常是不能控制,或者未知的,不能将所有的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)、访问效果
@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
先看看@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)、输出效果
在控制器方法中只需要实现页面跳转(只设置页面视图名称)功能而没有其他业务,此时可以在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。 此时,可以不需要控制器方法。
通过@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来匹配.
@RequestMapping(params={"param1"}),表示请求参数中param1必须出现
@RequestMapping(params={"!param1"}),表示请求参数中param1不能出现
@RequestMapping(params={"param1=value1"}),表示请求参数中param1必须出现且为value1
@RequestMapping(params={"param1!value1"}),表示请求参数中param1必须出现且不为value1
多个值之间是与的关系
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();
}
});
}
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类,
Spring MVC 为文件上传提供了直接支持,这种支持是通过即插即用的 MultipartResolver 实现的。Spring 使用 Jakarta Commons FileUpload 技术实现了一个 MultipartResolver 实现类:CommonsMultipartResolver。
在 Spring MVC 上下文中默认没有装配 MultipartResolver,因此默认情况下不能处理文件的上传工作。如果想使用 Spring 的文件上传功能,则需要先在上下文中配置 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 的类包添加到类路径下。
在 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() 方法还可将文件存储到硬件中,具体说明如下。
负责上传文件的表单和一般表单有一些区别,表单的编码类型必须是 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>
在Spring MVC中,HttpMessageConverter主要用于将HTTP请求的输入内容转换为指定的Java对象,以及将Java对象转换为HTTP响应的输出内容。这种灵活的消息转换机制就是利用HttpMessageConverter来实现的。
Spring MVC提供了多个默认的HttpMessageConverter实现,包括处理JSON、XML、文本等格式的Converter。另外,我们也可以自定义HttpMessageConverter来处理其他格式的数据。
Spring MVC提供了两个注解:@RequestBody和@ResponseBody,分别用于完成请求报文到对象和对象到响应报文的转换。
然而,有时候默认的HttpMessageConverter无法满足特定的需求,例如,当我们需要处理的数据格式没有默认的Converter时,或者我们需要对现有的Converter进行扩展时,就需要自定义HttpMessageConverter。
自定义HttpMessageConverter可以让我们更加灵活地控制数据转换的过程,例如我们可以自定义转换规则、异常处理等。
接下来我们通过一个实例讲解如何自定义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方法。
请求参数由于添加了@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中一个强大的工具,它可以帮助开发者更加灵活地控制数据转换的过程,满足特定的需求。
MappingJackson2HttpMessageConverter是springboot中默认的Json消息转换器。这个类的继承图如下:
这个类的主要实现逻辑是在AbstractJackson2HttpMessageConverter抽象类中实现的。这个列实现序列化与反序列化的最核心组件是ObjectMapper这个类。
MappingJackson2HttpMessageConverter是一个Spring消息转换器,用于在Web应用程序中处理请求和响应内容。它的工作原理如下:
MappingJackson2HttpMessageConverter通过实现HttpMessageConverter接口并重写相关方法,完成请求和响应内容的转换。当Spring处理请求或生成响应时,它会自动选择合适的消息转换器,并使用它来处理请求和响应内容。
MappingJackson2HttpMessageConverter如何将请求内容转换为Java对象和响应内容转换为JSON
(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;
}
如果不生效:
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;
}
}
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>
运行结果:
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+对请求进行了异步处理。
1、什么是 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
用来构建请求的,其主要有两个子类 MockHttpServletRequestBuilder 和MockMultipartHttpServletRequestBuilder(如文件上传使用),即用来 Mock 客户端请求需要的所有数据。
6)MockMvcResultMatchers
(1)用来匹配执行完请求后的结果验证
(2)如果匹配失败将抛出相应的异常
(3)包含了很多验证 API 方法
7)MockMvcResultHandlers
(1)结果处理器,表示要对结果做点什么事情
(2)比如此处使用 MockMvcResultHandlers.print() 输出整个响应结果信息
8)MvcResult
(1)单元测试执行结果,可以针对执行结果进行自定义验证逻辑。
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 的种形式。
随着异步 I/O 和 Netty 等框架的流行,响应式编程逐渐走入大众的视野。但是,响应式编程本身并不是太新的概念,这个术语最早出现在 1985 年 David Harel 和 Amir Pnueli 的论文“响应式系统的开发”之中,他们对复杂计算机系统的特征进行了归纳,提出了一种新颖的二分方式:转换式(Transformative)与响应式(Reactive)系统。转换式系统接收已知的一组输入,转换这些输入并产生输出,而响应式系统则会持续受到外部环境的刺激,它们的角色就是持续响应刺激。在构建响应式 Web 服务上,Spring 5 中引入了全新的编程框架,那就是 Spring WebFlux。作为一款新型的 Web 服务开发框架,它与传统的 WebMVC 相比具体有哪些优势呢?
介绍
Spring WebFlux 作为一个响应式 (reactive-stack) web 框架补充,在 5.0 的版本开始加入到 Spring 全家桶。这是一个完全非阻塞的,支持 Reactive Streams, 运行在诸如 Netty, Undertow, 以及 Servlet 3.1+ 容器上的,Spring WebFlux 构建在 Reactor 框架之上,提供了基于注解和函数式两种方式来配置和运行。Spring WebFlux 可以让你使用更少的线程去处理并发请求,同时能够让你使用更少的硬件资源来拓展你的应用。WebFlux 使用Netty作为默认的web服务器,其依赖于非阻塞IO,并且每次写入都不需要额外的线程进行支持。也可以使用Tomcat、Jetty容器,不同与SpringMVC依赖于Servlet阻塞IO,并允许应用程序在需要时直接使用Servlet API,WebFlux依赖于Servlet 3.1非阻塞IO。使用Undertow作为服务器时,WebFlux直接使用Undertow API而不使用Servlet API。
特点
场景
WebFlux 用于构建响应式 Web 服务。微服务架构的兴起为 WebFlux 的应用提供了一个很好的场景。我们知道在一个微服务系统中,存在数十乃至数百个独立的微服务,它们相互通信以完成复杂的业务流程。这个过程势必会涉及大量的 I/O 操作,尤其是阻塞式 I/O 操作会整体增加系统的延迟并降低吞吐量。如果能够在复杂的流程中集成非阻塞、异步通信机制,我们就可以高效处理跨服务之间的网络请求。针对这种场景,WebFlux 是一种非常有效的解决方案。控制层一旦使用 Spring WebFlux,它下面的安全认证层、数据访问层都必须使用 Reactive API。其次,Spring Data Reactive Repositories 目前只支持 MongoDB、Redis 和 Couchbase 等几种不支持事务管理的 NOSQL。技术选型时一定要权衡这些弊端和风险。
响应式编程
响应式编程是一种面向数据流和变化传播的编程范式,这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播,电子表格程序就是响应式编程的。
响应式应用应该具备如下的四个特点:
并发模型
WebFlux模型主要依赖响应式编程库Reactor,Reactor 有两种模型,Flux 和 Mono,提供了非阻塞、支持回压机制的异步流处理能力。WebFlux API接收普通Publisher作为输入,在内部使其适配Reactor类型,使用它并返回Flux或Mono作为输出。
介绍
SpringMvc是一种基于java的实现Mvc设计模式的请求驱动类型的轻量级web框架,属于SpringFrameWork的后续产品,已经融合在Spring Web Flow中,SpringMvc已经成为目前最主流的MVC框架之一,并且随着Spring3.0的发布,全面超越Struts2,成为最优秀的mvc框架,他通过一套注解,让一个简单的java类成为处理请求的控制器,他无需实现任何接口,同时他还支持RESTful编程风格的请求。
特点
MVC
Spring web MVC框架提供了MVC(模型 - 视图 - 控制器)架构和用于开发灵活和松散耦合的Web应用程序的组件。 MVC模式导致应用程序的不同方面(输入逻辑,业务逻辑和UI逻辑)分离,同时提供这些元素之间的松散耦合。
并发模型
servlet由servlet container进行生命周期管理。container启动时构造servlet对象并调用servlet init()进行初始化;container关闭时调用servlet destory()销毁servlet;container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service()。
处理请求的时候同步操作,一个请求对应一个线程来处理,并发上升,线程数量就会上涨(上线文切换,内存消耗大)影响请求的处理时间。现代系统多数都是IO密集的,同步处理让线程大部分时间都浪费在了IO等待上面。虽然Servlet3.0后提供了异步请求处理与非阻塞IO支持,但是使用它会远离Servlet API的其余部分,比如其规范是同步的(Filter, Servlet)或阻塞的(getParameter,getPart),而且其对响应的写入仍然是阻塞的。
Spring WebFlux 不是 Spring MVC 的替代方案,Spring WebFlux 是 Spring Framework 5.0中引入的新的响应式web框架。与Spring MVC不同,它不需要Servlet API,是完全异步且非阻塞的,并且通过Reactor项目实现了Reactive Streams规范。Spring MVC依然构建在 Servlet API 以及 Servlet 容器之上;Spring Security 为两种不同的技术栈提供了安全性的支持,Spring Data 分别为两种不同的技术栈实现了 Repository;在数据访问方面,响应式 Repository 已经涵盖了 Mongo、Cassandra、Redis 以及 Couchbase。但是在关系型数据库方面,因为 JBDC 规范本身就是阻塞式的,所以进展并不明显。但是,像 PostgreSQL 和 MySQL 已经有了异步驱动。异步非阻塞并不会使程序运行得更快。WebFlux 并不能使接口的响应时间缩短,它仅仅能够提升吞吐量和伸缩性。Spring WebFlux 是一个异步非阻塞的 Web 框架,所以,它特别适合应用在 IO 密集型的服务中,比如微服务网关这样的应用中。
言
做 Java Web 开发的你,一定听说过SpringMVC的大名,作为现在运用最广泛的Java框架,它到目前为止依然保持着强大的活力和广泛的用户群。
本文介绍如何用eclipse一步一步搭建SpringMVC的最小系统,所谓最小系统,就是足以使项目在SpringMVC框架下成功跑起来,并且能够做一些简单的事情(比如访问页面)的系统。
话不多说,让我们开始吧。所有的源代码和jar包都会在最后给出。
其他环境:
操作系统:Windos 10
Tomcat : v7.0
JDK : 1.7
正文
1. 新建一个项目
Paste_Image.png
我们用eclipse新建项目,选择Dynamic Web Project(动态的Web项目)。
点击Next
Paste_Image.png
Project name里面写上 springmvc,这就是我们项目的名称,其他不用改,直接点击Finish 。
Paste_Image.png
OK,项目就建好了。
接下来一定要将项目的字符集改为UTF-8
右键项目——properties
Paste_Image.png
改为UTF-8,点击OK。
2. 编写 web.xml
当我们打开WebContent/WEB-INF目录的时候,发现里面只有一个lib目录,这是存放各种jar包的地方。我们知道一个web项目必须要有一个web.xml文件才行。
既然没有,我们自己写一个咯。
右键WEB-INF——new——file,新建一个web.xml文件。
点击Finish
将以下内容填进去即可。
<?xml version="1.0" encoding="UTF-8"?><web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
这样就完成了基本的配置,我的意思是说,现在这个项目就已经是一个标准的web项目了。
3. 验证web项目是否搭建成功
为了验证到目前为止的正确性,我们在WebContent目录下面新建一个jsp文件。
名字就叫index.jsp
Paste_Image.png
内容如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><html>
我们现在就将这个项目部署到Tomcat,来验证是否可以跑起来。
在项目上右键——Debug As——Debug on Server
直接点击Finish
经过一段时间,控制台开始打印日志信息,当我们看到这些信息的时候,说明Tomcat已经启动完毕了。
Paste_Image.png
让我们打开浏览器,在地址栏输入以下信息
http://localhost:8088/springmvc/index.jsp
我电脑上Tomcat配置的端口号是8088,具体情况视你自己的Tomcat决定,可能是8080等。
Paste_Image.png
可见,能够成功访问页面了,这说明我们到目前为止的操作是正确的。
3. 集成SpringMVC
我们在web.xml文件里面添加下面的配置
3.1 配置监听器
<listener>
3.2 配置过滤器,解决POST乱码问题
<filter>
3.3 配置SpringMVC分发器,拦截所有请求
<servlet>
在这个配置中,我们规定了 DispatcherServlet 的关联 XML 文件名称叫做 dispatcher-servlet。
注意,这里的路径是相对于web.xml来说的,也就是说,这个文件也在WEB-INF的根目录下。
所以,我们需要在WEB-INF的根目录下新建一个dispatcher-servlet.xml文件。
Paste_Image.png
至此,web.xml文件的编写就告一段落了。
3.4 编写dispatcher-servlet.xml
dispatcher-servlet.xml 的作用就是配置SpringMVC分发器。
配置如下:
<?xml version="1.0" encoding="UTF-8" ?><beans xmlns="http://www.springframework.org/schema/beans"
根据配置,有三个需要注意的地方。
它会扫描 com.springmvc 包下所有的Java类,但凡是遇到有注解的,比如@Controller , @Service , @Autowired ,就会将它们加入到Spring的bean工厂里面去。
所有的静态资源文件,比如说 js , css , images 都需要放在/resources目录下,这个目录现在我们还没有建。
所有的展示页面,比如jsp文件,都需要放置在/WEB-INF/pages目录下,这个目录现在我们也没有建。
OK,我们把对应的目录加上。
首先是Java文件的目录。
Paste_Image.png
我们在这个地方右键,新建一个 com 包,再在里面建一个 springmvc 包,或者用 . 的方式一起建。
Paste_Image.png
点击Finish
Paste_Image.png
根据SpringMVC的分层,我们在springmvc 包下面建三个包,分别是controller , service , dao
Paste_Image.png
这样的话, 当我们项目一旦启动,springmvc就会扫描这三个包,将里面但凡是有注解的类都提取起来,放进Spring容器(或者说Spring的bean工厂),借由Spring容器来统一管理。这也就是你从来没有去new一个Controller的原因。
接下来,我们来建静态资源的目录。
在WebContent目录下新建一个resources文件夹。
然后顺便把js,css,img的文件夹都建一下,这里就存放我们的静态资源文件。
Paste_Image.png
最后,我们在WEB-INF目录下建一个pages文件夹,作为展示页面的存放目录。
Paste_Image.png
将之前的index.jsp拷贝进来。
Paste_Image.png
这样就配置的差不多了。
5. 导包和验证
我们将jar包放到lib目录:
Paste_Image.png
然后启动项目,验证一下到目前为止的构建是否正确。
打开Servers视图,点击如图像是甲虫一样的图标。
Paste_Image.png
发现报错了,错误信息如下:
Paste_Image.png
错误:
Could not open ServletContext resource [/WEB-INF/applicationContext.xml]
它说我们在WEB-INF下面少了一个applicationContext.xml 这个文件,原来,我们少了对SpringBean工厂的配置,它的意思就是说,我们要规定一下,在Spring容器启动的时候,需要自动加载哪些东西?
于是,我们把 applicationContext.xml 加上。
Paste_Image.png
<?xml version="1.0" encoding="UTF-8"?>
里面我们啥也不配置,再次启动Tomcat。
Paste_Image.png
这回不报错了。
5. 配置ViewController
我们知道,WEB-INF目录下的任何资源都是无法直接通过浏览器的url地址去访问的,保证了安全性。这也是我们为什么把页面都放在该目录下的原因。
为了有所区分,我们还单独建立了一个pages文件夹,将这些页面保存起来。
Paste_Image.png
现在,为了访问这个页面,我们需要用到SpringMVC的页面跳转机制。
我们在Controller包下新建一个ViewController
Paste_Image.png
点击Finish
Paste_Image.png
ViewController 代码:
package com.springmvc.controller;import javax.servlet.http.HttpServletRequest;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.servlet.ModelAndView;@Controllerpublic class ViewController { @RequestMapping("/view")
我只需要将想要访问的页面放在path里面,通过url传进来就行了。
因为添加了java类,因此我们重新启动Tomcat。
启动完成后,在地址栏输入:
http://localhost:8088/springmvc/view?path=index
结果:
Paste_Image.png
没关系,我们看他报什么错。
message /springmvc/WEB-INF/pagesindex.jsp
pagesindex.jsp是什么鬼??
原来,在dispatcher-servlet.xml中,我们少写了一个 "/"
Paste_Image.png
添上去就行了。
Paste_Image.png
保存后,因为修改了XML配置文件,因此我们还是需要重新启动Tomcat。
启动完成后,继续!
Paste_Image.png
成功了。
6. 引入静态资源
比如,我在resources/img目录下放了一张图片,怎么引入到index.jsp呢?
Paste_Image.png
background : url(http://localhost:8088/springmvc/resources/img/bg.jpg);background-size : 100% 100%;
的确,这是一种方式。可是,它有一个缺点就是根路径写死了,我们肯定不希望这样的。
其实,我们可以在viewController里面拿到项目根路径,然后传递到jsp页面就OK了。
Paste_Image.png
我们把调试信息 “恭喜,web項目已經成功搭建!” 删掉。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><html>
${contextPath} 可以取到Controller传过来的contextPath值。
成功了!
大家可以点击加入群:606187239【JAVA大牛学习交流】
里面有Java高级大牛直播讲解知识点 走的就是高端路线
(如果你想跳槽换工作 但是技术又不够 或者工作上遇到了
瓶颈 我这里有一个JAVA的免费直播课程 讲的是高端的知识点
基础不好的误入哟 只要你有1-5年的开发经验
可以加群找我要课堂链接 注意:是免费的 没有开发经验误入哦)
1、具有1-5工作经验的,面对目前流行的技术不知从何下手,
需要突破技术瓶颈的可以加。2、在公司待久了,过得很安逸,
但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的可以加。
3、如果没有工作经验,但基础非常扎实,对java工作机制,
常用设计思想,常用java开发框架掌握熟练的,可以加。
4、觉得自己很牛B,一般需求都能搞定。
但是所学的知识点没有系统化,很难在技术领域继续突破的可以加。
5. 群号:高级架构群 606187239备注好信息!
6.阿里Java高级大牛直播讲解知识点,分享知识,
多年工作经验的梳理和总结,带着大家全面、
科学地建立自己的技术体系和技术认知!
*请认真填写需求信息,我们会在24小时内与您取得联系。