SpringMVC
回顾servlet
Servlet 处理请求和响应
JSP 可以嵌套到html中,最终转换成servlet
Filter 过滤URL进行拦截
Listener 监听事件 例如:ServletContext、HttpSession的创建和销毁
如何使用SpringMVC
1. 依赖
org.springframework:spring-context:5.2.0.RELEASE
org.springframework:spring-webmvc:5.2.0.RELEASE
org.springframework:spring-jdbc:5.2.0.RELEASE
javax.annotation:javax.annotation-api:1.3.2
io.pebbletemplates:pebble-spring5:3.1.2
ch.qos.logback:logback-core:1.2.3
ch.qos.logback:logback-classic:1.2.3
com.zaxxer:HikariCP:3.4.2
org.hsqldb:hsqldb:2.5.0
以及provided依赖:
org.apache.tomcat.embed:tomcat-embed-core:9.0.26
org.apache.tomcat.embed:tomcat-embed-jasper:9.0.26
2. 目录
spring-web-mvc
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── itranswarp
│ └── learnjava
│ ├── AppConfig.java
│ ├── DatabaseInitializer.java
│ ├── entity
│ │ └── User.java
│ ├── service
│ │ └── UserService.java
│ └── web
│ └── UserController.java
├── resources
│ ├── jdbc.properties
│ └── logback.xml
└── webapp
├── WEB-INF
│ ├── templates
│ │ ├── _base.html
│ │ ├── index.html
│ │ ├── profile.html
│ │ ├── register.html
│ │ └── signin.html
│ └── web.xml
└── static
├── css
│ └── bootstrap.css
└── js
└── jquery.js
3.
src/main/webapp是标准web目录,WEB-INF存放web.xml,编译的class,第三方jar,
以及不允许浏览器直接访问的View模版,static目录存放所有静态文件
4. 编写配置
@Configuration
@ComponentScan
@EnableWebMvc // 启用Spring MVC
@EnableTransactionManagement
@PropertySource("classpath:/jdbc.properties")
public class AppConfig {
...
@Bean
WebMvcConfigurer createWebMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/static/");
}
};
}
// 配置魔板引擎
@Bean
ViewResolver createViewResolver(@Autowired ServletContext servletContext) {
PebbleEngine engine = new PebbleEngine.Builder().autoEscaping(true)
.cacheActive(false)
.loader(new ServletLoader(servletContext))
.extension(new SpringExtension())
.build();
PebbleViewResolver viewResolver = new PebbleViewResolver();
viewResolver.setPrefix("/WEB-INF/templates/");
viewResolver.setSuffix("");
viewResolver.setPebbleEngine(engine);
return viewResolver;
}
}
如何启动web应用程序
通过Listener启动,
通过Servlet启动,
使用XML配置,
使用注解配置
第一步,我们在web.xml中配置Spring MVC提供的DispatcherServlet:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.itranswarp.learnjava.AppConfig</param-value>
</init-param>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
初始化参数contextClass指定使用注解配置的AnnotationConfigWebApplicationContext,
配置文件的位置参数contextConfigLocation指向AppConfig的完整类名,最后,把这个Servlet映射到/*,即处理所有URL。
上述配置可以看作一个样板配置,有了这个配置,
Servlet容器会首先初始化Spring MVC的DispatcherServlet,
在DispatcherServlet启动时,
它根据配置AppConfig创建了一个类型是WebApplicationContext的IoC容器,
完成所有Bean的初始化,并将容器绑到ServletContext上。
因为DispatcherServlet持有IoC容器,
能从IoC容器中获取所有@Controller的Bean,
因此,DispatcherServlet接收到所有HTTP请求后,
根据Controller方法配置的路径,
就可以正确地把请求转发到指定方法,并根据返回的ModelAndView决定如何渲染页面。
最后,我们在AppConfig中通过main()方法启动嵌入式Tomcat:
public static void main(String[] args) throws Exception {
Tomcat tomcat = new Tomcat();
tomcat.setPort(Integer.getInteger("port", 8080));
tomcat.getConnector();
Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
WebResourceRoot resources = new StandardRoot(ctx);
resources.addPreResources(
new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/"));
ctx.setResources(resources);
tomcat.start();
tomcat.getServer().await();
}
上述Web应用程序就是我们使用Spring MVC时的一个最小启动功能集。由于使用了JDBC和数据库,用户的注册、登录信息会被持久化
使用Spring MVC时,整个Web应用程序按如下顺序启动:
启动Tomcat服务器;
Tomcat读取web.xml并初始化DispatcherServlet;
DispatcherServlet创建IoC容器并自动注册到ServletContext中。
启动后,浏览器发出的HTTP请求全部由DispatcherServlet接收,并根据配置转发到指定Controller的指定方法处理。
使用Spring写Rest接口
rest就是接受json 返回json的接口
1. 所以必须引入json的操作
对应的Maven工程需要加入Jackson这个依赖:com.fasterxml.jackson.core:jackson-databind:2.11.0
2. controller
@PostMapping(value = "/rest",
consumes = "application/json;charset=UTF-8",
produces = "application/json;charset=UTF-8")
@ResponseBody
public String rest(@RequestBody User user) {
return "{\"restSupport\":true}";
}
这个@RequestBody User user
输入的JSON则根据注解@RequestBody直接被Spring反序列化为User这个JavaBean。
这个是把json进行反序列化 这个是spring把传入的json进行反序列化
简写
@RestController
@RequestMapping("/api")
public class ApiController {
@Autowired
UserService userService;
@GetMapping("/users")
public List<User> users() {
return userService.getUsers();
}
@GetMapping("/users/{id}")
public User user(@PathVariable("id") long id) {
return userService.getUserById(id);
}
@PostMapping("/signin")
public Map<String, Object> signin(@RequestBody SignInRequest signinRequest) {
try {
User user = userService.signin(signinRequest.email, signinRequest.password);
return Map.of("user", user);
} catch (Exception e) {
return Map.of("error", "SIGNIN_FAILED", "message", e.getMessage());
}
}
public static class SignInRequest {
public String email;
public String password;
}
}
每个方法都是一个API接口,输入和输出只要能被Jackson序列化或反序列化为JSON就没有问题
jackson
{"user":{"id":1,"email":"bob@example.com","password":"bob123","name":"Bob",...
像这样的用户 我们肯定是想让password能保存 不让password显示出去返回
这时候就要在对象的字段passwor上加上jackson的注解
public class User {
...
@JsonProperty(access = Access.WRITE_ONLY)
public String getPassword() {
return password;
}
...
}
同样的,可以使用@JsonProperty(access = Access.READ_ONLY)允许输出
@JsonIgnore 输入 输出都忽略
Filter 这是servlet中的一个概念
SpringMVC中如何使用呢
Servlet默认不是以UTF-8 编码的 所以需要
把编码设置一下
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
以上的设置比较简单
注意到CharacterEncodingFilter其实和Spring的IoC容器没有任何关系,两者均互不知晓对方的存在,因此,配置这种Filter十分简单
如果我们写的一个类里边用到了IOC里边的bean怎么办呢?
这个配置文件可是直接给Servelet加载的 访问不到Spring的IOC
这也没关系,Spring提供了一个代理DelegatingFilterProxy
所以配置就要这样写了
<web-app>
<filter>
<filter-name>aaaFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<!-- 指定Bean的名字 --> 如果这个名字authFilter 跟 aaaFilter 一样就不需要再进行配置了
<init-param>
<param-name>targetBeanName</param-name>
<param-value>authFilter</param-value>
</init-param>
<filter-mapping>
<filter-name>aaaFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>
这样我们自己写的一个类就可以用了 这个类里可以用Spring的组件了
这个原理是什么呢?
Servlet容器从web.xml中读取配置,
实例化DelegatingFilterProxy,注意命名是authFilter;
Spring容器通过扫描@Component实例化AuthFilter。
当DelegatingFilterProxy生效后,
它会自动查找注册在ServletContext上的Spring容器,
再试图从容器中查找名为authFilter的Bean,
也就是我们用@Component声明的AuthFilter。
DelegatingFilterProxy将请求代理给AuthFilter,
一句通俗的话就是 我在Servlet里写的类如果要想跟Spring扯上关系就得用DelegatingFilterProxy进行代理
@Component
public class AuthFilter implements Filter {
@Autowired
UserService userService;
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
// 获取Authorization头:
String authHeader = req.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Basic ")) {
// 从Header中提取email和password:
String email = prefixFrom(authHeader);
String password = suffixFrom(authHeader);
// 登录:
User user = userService.signin(email, password);
// 放入Session:
req.getSession().setAttribute(UserController.KEY_USER, user);
}
// 继续处理请求:
chain.doFilter(request, response);
}
}
│ ▲
▼ │
┌───────┐
│Filter1│
└───────┘
│ ▲
▼ │
┌───────┐
┌ ─ ─ ─│Filter2│─ ─ ─ ─ ─ ─ ─ ─ ┐
└───────┘
│ │ ▲ │
▼ │
│ ┌─────────────────┐ │
│DispatcherServlet│<───┐
│ └─────────────────┘ │ │
│ ┌────────────┐
│ │ │ModelAndView││
│ └────────────┘
│ │ ▲ │
│ ┌───────────┐ │
│ ├───>│Controller1│────┤ │
│ └───────────┘ │
│ │ │ │
│ ┌───────────┐ │
│ └───>│Controller2│────┘ │
└───────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
虚线框是Filter的拦截范围, 可见filter对底层业务没有了解。把所有的都过滤掉了
Filter组件实际上并不知道后续内部处理是通过Spring MVC提供的DispatcherServlet还是其他Servlet组件,
因为Filter是Servlet规范定义的标准组件,它可以应用在任何基于Servlet的程序中
如果要想对具体的业务进行过滤该怎么办呢?使用Interceptor
│ ▲
▼ │
┌───────┐
│Filter1│
└───────┘
│ ▲
▼ │
┌───────┐
│Filter2│
└───────┘
│ ▲
▼ │
┌─────────────────┐
│DispatcherServlet│<───┐
└─────────────────┘ │
│ ┌────────────┐
│ │ModelAndView│
│ └────────────┘
│ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ▲
│ ┌───────────┐ │
├─┼─>│Controller1│──┼─┤
│ └───────────┘ │
│ │ │ │
│ ┌───────────┐ │
└─┼─>│Controller2│──┼─┘
└───────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ┘
这个虚线是对Controller进行的拦截
CROS
rest开发 前后端分离 前端的同源策略会引发跨域问题
解决办法:
1.使用@CrossOrigin
//@CrossOrigin(origins = {"http://a.com", "https://www.b.com"})
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api")
public class ApiController {
...
}
2. 使用CorsRegistry
@Bean
WebMvcConfigurer createWebMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://local.liaoxuefeng.com:8080")
.allowedMethods("GET", "POST")
.maxAge(3600);
// 可以继续添加其他URL规则:
// registry.addMapping("/rest/v2/**")...
}
};
}
3. 使用CorsFilter
第三种方法是使用Spring提供的CorsFilter,
我们在集成Filter中详细介绍了将Spring容器内置的Bean暴露为Servlet容器的Filter的方法,
由于这种配置方式需要修改web.xml,也比较繁琐,所以推荐使用第二种方式。
异步处理
这块是理解实现高并发的基础
Servlet模型中,每一个用户每一个请求过来都会由后台启动的一个线程处理,处理之后,反会给用户.整个过程都是由同一个线程完成的
这样做的优点是:事务处理 肯定得是一个线程的
这样做的缺点是: 如果一个进程的耗时特别长,那么这个进程就不能及时得到释放,再有用户请求过来,就会起动新的线程。线程数达到一定数目服务器就挂了
优化就是在不改变优点的前提下改善缺点:
如果把长时间处理的请求改为异步处理,
那么线程池的利用率就会大大提高。
Servlet从3.0规范开始添加了异步支持,
允许对一个请求进行异步处理
Servlet是如何支持线程的
<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_3_1.xsd"
version="3.1">
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.itranswarp.learnjava.AppConfig</param-value>
</init-param>
<load-on-startup>0</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
实现异步有三种方法:返回一个Callable 返回一个DeferredResult对象 使用Filter
推荐使用DeferredResult 因为操作比较灵活
Servlet 3.0规范添加的异步支持是针对同步模型打了一个“补丁”,
虽然可以异步处理请求,
但高并发异步请求时,
它的处理效率并不高,
因为这种异步模型并没有用到真正的“原生”异步。
Java标准库提供了封装操作系统的异步IO包java.nio,
是真正的多路复用IO模型,
可以用少量线程支持大量并发。
使用NIO编程复杂度比同步IO高很多,
因此我们很少直接使用NIO。
实现长连接
websocket
第一步
首先,我们需要在pom.xml中加入以下依赖:
org.apache.tomcat.embed:tomcat-embed-websocket:9.0.26
org.springframework:spring-websocket:5.2.0.RELEASE
第二步
在config配置里增加bean
@Bean
WebSocketConfigurer createWebSocketConfigurer(
@Autowired ChatHandler chatHandler,
@Autowired ChatHandshakeInterceptor chatInterceptor)
{
return new WebSocketConfigurer() {
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 把URL与指定的WebSocketHandler关联,可关联多个:
registry.addHandler(chatHandler, "/chat").addInterceptors(chatInterceptor);
}
};
}
第三步骤
Spring提供了TextWebSocketHandler和BinaryWebSocketHandler分别处理文本消息和二进制消息
@Component
public class ChatHandler extends TextWebSocketHandler {
// 保存所有Client的WebSocket会话实例:
private Map<String, WebSocketSession> clients = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 新会话根据ID放入Map:
clients.put(session.getId(), session);
session.getAttributes().put("name", "Guest1");
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
clients.remove(session.getId());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String s = message.getPayload();
String r = ... // 根据输入消息构造待发送消息
broadcastMessage(r); // 推送给所有用户
}
}
推送给指定用户
@Component
public class ChatHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
public ChatHandshakeInterceptor() {
// 指定从HttpSession复制属性到WebSocketSession:
super(List.of(UserController.KEY_USER));
}
}
在Servlet中使用WebSocket需要3.1及以上版本;
通过spring-websocket可以简化WebSocket的开发