spring mvc复习整理三

悟空

spring mvc 复习整理三(仗剑走天涯)

跨域

当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同时,就会产生跨域

这里先写点spring mvc相关的跨域吧 剩下的知识等学完的在写点吧

同源策略

同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)

跨域的案例

当前页面url 被请求页面url 是否跨域 原因
http://www.codexb.cn http://www.codexb.cn/index.html 不跨域 同源(协议、域名、端口号相同)
http://www.codexb.cn https://www.codexb.cn 跨域 协议不同(http/https)
http://www.codexb.cn http://www.baidu.com 跨域 主域名不同(codexb/baidu)
http://www.codexb.cn http://bolg.codexb.cn 跨域 子域名不同(www/bolg)
http://www.codexb.cn:80 http:www.codexb.cn:8080 跨域 端口号不同(80/8080)

非同源限制

无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB。

无法接触非同源网页的 DOM

无法向非同源地址发送 AJAX 请求

两种请求

跨域资源共享”(Cross-origin resource sharing)

简单请求

请求方法是下面的其中一个:HEAD GET POST

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段

HTTP的头信息不超出以下几种字段:Accept Accept-Language Content-Language Last-Event-ID Content-Type

Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

对于简单请求,浏览器直接发出CORS请求。就是在头信息之中,增加一个Origin字段 Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求

1
2
3
4
5
6
GET /cors HTTP/1.1
Origin: http://127.0.0.1:8848
Host: api.ydlclass.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段

1
2
3
4
Access-Control-Allow-Origin: http://127.0.0.1:8848
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

Access-Control-Allow-Origin:该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求

Access-Control-Allow-Credentials: 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可

Access-Control-Expose-Headers:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定

withCredentials 属性:CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段 为true 另一方面,必须在AJAX请求中打开withCredentials属性

1
2
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理 但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials

1
xhr.withCredentials = false;

如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie

非简单请求

预检请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。OPTIONS

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错

浏览器发现,这是一个非简单请求,就自动发出一个”预检”请求

1
2
3
4
5
6
7
8
OPTIONS /cors HTTP/1.1
Origin: http://127.0.0.1:8848
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.ydlclass.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

预检请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源

Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT

Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header

预检请求的响应

服务器收到”预检”请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应

如果服务器否定了预检请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。

服务器回应的其他CORS相关字段

1
2
3
4
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

Access-Control-Allow-Methods:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

Access-Control-Allow-Headers:如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在预检中请求的字段。

Access-Control-Allow-Credentials:该字段与简单请求时的含义相同。

Access-Control-Max-Age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求

跨域案例

  • html
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>商品</title>
</head>
<body>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<h1>商品页面</h1>

<script>


// const instance = axios.create({
// baseURL:'http://127.0.0.1:8080'
// });

// // 为给定 ID 的 user 创建请求
// instance.get('/goods/findAll')
// .then(function (response) {
// console.log(response);
// })
// .catch(function (error) {
// console.log(error);
// });

// // 上面的请求也可以这样做
// instance.get('/goods/findAll', {
// params: {
// id: 1
// }
// })
// .then(function (response) {
// console.log(response);
// })
// .catch(function (error) {
// console.log(error);
// });

// instance.post('/goods/save', {
// name: '电脑',
// price: 10000
// })
// .then(function (response) {
// console.log(response);
// })
// .catch(function (error) {
// console.log(error);
// });




$.ajax( {
type : "GET",
url : "http://localhost:8080/goods/findAll",
dataType : "json",
success : function(data) {
console.log("get请求!---------------------")
console.log(data)
}
});

$.ajax( {
type : "GET",
url : "http://localhost:8080/goods/findAll/1",
dataType : "json",
success : function(data) {
console.log("get请求!---------------------")
console.log(data)
}
});

$.ajax( {
type : "DELETE",
url : "http://localhost:8080/goods/delete/1",
dataType : "json",
success : function(data) {
console.log("delete请求!---------------------")
console.log(data)
}
});

$.ajax( {
type : "put",
url : "http://localhost:8080/goods/update",
dataType : "json",
data: {name:"电脑",price:123},
success : function(data) {
console.log("get请求!---------------------")
console.log(data)
}
});

$.ajax( {
type : "post",
url : "http://localhost:8080/goods/save",
dataType : "json",
data: {name:"电脑",price:123},
success : function(data) {
console.log("get请求!---------------------")
console.log(data)
}
});


</script>


</body>
</html>
  • controller
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
38
39
40
41
42
43
@RestController
@RequestMapping(value = "/goods")
@CrossOrigin(origins = "http://127.0.0.1:8848")
public class RestfulController {


@GetMapping(value = "/findAll")
public R findAll(){

List<Goods> goods = List.of(
new Goods("电脑", new BigDecimal("20000")),
new Goods("电话",new BigDecimal("4000"))
);

return R.success(goods);
}

@GetMapping(value = "/findAll/{id}")
public R findAllById(@PathVariable Integer id){
Goods goods = new Goods("电话", new BigDecimal("4000"));
return R.success(goods);

}

@PostMapping(value = "/save")
public R insert(@RequestBody Goods goods){
Goods goodss = new Goods(goods.getName(), goods.getPrice());
return R.success(goodss);
}

@PutMapping(value = "/update")
public R update(Goods goods){
Goods goods1 = new Goods(goods.getName(), goods.getPrice());
return R.success(goods1);
}

@DeleteMapping(value = "/delete/{id}")
public R deleteById(@PathVariable Integer id){

return R.error();
}

}

文件上传和下载

文件上传

MultipartResolver用于处理文件上传。当收到请求时,DispatcherServlet 的 checkMultipart() 方法会调用 MultipartResolver 的 isMultipart() 方法判断请求中 是否包含文件。如果请求数据中包含文件,则调用 MultipartResolver 的 resolveMultipart() 方法对请求的数据进行解析,然后将文件数据解析成 MultipartFile 并封装在 MultipartHttpServletRequest (继承了 HttpServletRequest) 对象中,最后传递给 Controller

MultipartResolver 默认不开启,需要手动开启

文件上传前端表单的要求

表单 必须 将 method设置为POST 并将enctype设置为 multipart/form-data 只有在这样的情况下,浏览器才会把用户选择的文件以二进制数据发送给服务器

application/x-www-form-urlencoded:默认方式,只处理表单域中的 value 属性值,采用这种编码方式的表单会将表单域中的值处理成 URL 编码方式

multipart/form-data:这种编码方式会以二进制流的方式来处理表单数据,这种编码方式会把文件域指定文件的内容也封装到请求参数中,不会对字符编码

文件上床操作

需要两个依赖 commons-fileupload commons-io

  • pom
1
2
3
4
5
6
<!--文件上传-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
  • spring-mvc.xml
1
2
3
4
5
6
7
8
<!--文件上传配置-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 请求的编码格式,必须和jSP的pageEncoding属性一致,以便正确读取表单的内容,默认为ISO-8859-1 -->
<property name="defaultEncoding" value="utf-8"/>
<!-- 上传文件大小上限,单位为字节(10485760=10M) -->
<property name="maxUploadSize" value="10485760"/>
<property name="maxInMemorySize" value="40960"/>
</bean>
  • html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上传</title>
</head>
<body>

<form action="/file/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="上传">
</form>

</body>
</html>
  • controller
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
@RestController
@RequestMapping(value = "/file")
public class FileController {


@PostMapping(value = "/upload")
public R fileUpload(@RequestParam("file") CommonsMultipartFile multipartFile){

String originalFilename = multipartFile.getOriginalFilename();

String s = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.indexOf("."));

MyFile myFile = new MyFile();

myFile.setOriginalFileName(multipartFile.getOriginalFilename());
myFile.setNewFileName(s);
myFile.setSize(multipartFile.getSize());

try {
multipartFile.transferTo(new File("D://" + s));
} catch (IOException e) {
e.printStackTrace();
}

return R.success();
}
}

CommonsMultipartFile常用方法

String getOriginalFilename():获取上传文件的原名

InputStream getInputStream():获取文件流

void transferTo(File dest):将上传文件保存到一个目录文件中

文件下载

两种方式

传统方式

  • pom
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@GetMapping("/download1")
@ResponseBody
public R download1(HttpServletResponse response){
FileInputStream fileInputStream = null;
ServletOutputStream outputStream = null;
try {
// 这个文件名是前端传给你的要下载的图片的id
// 然后根据id去数据库查询出对应的文件的相关信息,包括url,文件名等
String fileName = "小博.jpg";

//1、设置response 响应头,处理中文名字乱码问题
response.reset(); //设置页面不缓存,清空buffer
response.setCharacterEncoding("UTF-8"); //字符编码
response.setContentType("multipart/form-data"); //二进制传输数据
//设置响应头,就是当用户想把请求所得的内容存为一个文件的时候提供一个默认的文件名。
//Content-Disposition属性有两种类型:inline 和 attachment
//inline :将文件内容直接显示在页面
//attachment:弹出对话框让用户下载具体例子:
response.setHeader("Content-Disposition",
"attachment;fileName="+ URLEncoder.encode(fileName, "UTF-8"));

// 通过url获取文件
File file = new File("D:/upload/"+fileName);
fileInputStream = new FileInputStream(file);
outputStream = response.getOutputStream();

byte[] buffer = new byte[1024];
int len;
while ((len = fileInputStream.read(buffer)) != -1){
outputStream.write(buffer,0,len);
outputStream.flush();
}

return R.success();
} catch (IOException e) {
e.printStackTrace();
return R.fail();
}finally {
if( fileInputStream != null ){
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if( outputStream != null ){
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

直接向response的输出流中写入对应的文件流

使用ResponseEntity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping("/download2")
public ResponseEntity<byte[]> download2(){
try {
String fileName = "小博.jpg";
byte[] bytes = FileUtils.readFileToByteArray(new File("D:/upload/"+fileName));
HttpHeaders headers=new HttpHeaders();
// Content-Disposition就是当用户想把请求所得的内容存为一个文件的时候提供一个默认的文件名。
headers.set("Content-Disposition","attachment;filename="+ URLEncoder.encode(fileName, "UTF-8"));
headers.set("charsetEncoding","utf-8");
headers.set("content-type","multipart/form-data");
ResponseEntity<byte[]> entity=new ResponseEntity<>(bytes,headers, HttpStatus.OK);
return entity;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}

使用 ResponseEntity<byte[]>来向前端返回文件

WebSocket

ebSocket 协议提供了一种标准化方式,可通过单个 TCP 连接在客户端和服务器之间建立全双工、双向通信通道。它是与 HTTP 不同的 TCP 协议,但旨在通过 HTTP 工作,使用端口 80 和 443

Http与WebSocket区别

Http

在 HTTP 和 REST 中,一个应用程序被建模为多个 URL。为了与应用程序交互,客户端访问这些 URL,请求-响应样式。服务器根据 HTTP URL、方法和请求头将请求路由到适当的处理程序

WebSocket

WebSocket中,通常只有一个 URL 用于初始连接。随后,所有应用程序消息都在同一个 TCP 连接上流动

短轮询

用JS写个死循环(setInterval),不停的去请求服务器中的库存量是多少,然后刷新到这个页面当中,这其实就是所谓的短轮询。

短轮询有明显的坏处,那就是你很浪费服务器和客户端的资源。如果有1000个人停留在某个商品详情页面,那就是说会有1000个客户端不停的去请求服务器获取库存量,这显然是不合理的

长轮询

长轮询中,服务器如果检测到库存量没有变化的话,将会把当前请求挂起一段时间(这个时间也叫作超时时间,一般是几十秒)。在这个时间里,服务器会去检测库存量有没有变化,检测到变化就立即返回,否则就一直等到超时为止

长轮询的坏处:因为把请求挂起同样会导致资源的浪费,假设还是1000个人停留在某个商品详情页面,那就很有可能服务器这边挂着1000个线程,在不停检测库存量,这依然是有问题的

应用

如果消息量相对较低(例如监控网络故障,新闻、邮件和社交提要需要动态更新,但每隔几分钟更新一次可能也完全没问题),HTTP轮询可以提供有效的解决方案。低延迟、高频率和高容量的组合是使用 WebSocket 的最佳案例

代码案例

  • pom
1
2
3
4
5
6
7
8
9
10
11
<!--        websocket-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.2.18.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>5.2.18.RELEASE</version>
</dependency>
  • html
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<!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>websocket调试页面</title>
</head>
<body>
<div style="float: left; padding: 20px">
<strong>location:</strong> <br />
<input type="text" id="serverUrl" size="35" value="" /> <br />
<button onclick="connect()">connect</button>
<button onclick="wsclose()">disConnect</button>
<br /> <strong>message:</strong> <br /> <input id="txtMsg" type="text" size="50" />
<br />
<button onclick="sendEvent()">发送</button>
</div>

<div style="float: left; margin-left: 20px; padding-left: 20px; width: 350px; border-left: solid 1px #cccccc;"> <strong>消息记录</strong>
<div style="border: solid 1px #999999;border-top-color: #CCCCCC;border-left-color: #CCCCCC; padding: 5px;width: 100%;height: 172px;overflow-y: scroll;" id="echo-log"></div>
<button onclick="clearLog()" style="position: relative; top: 3px;">清除消息</button>
</div>

</div>
</body>
<!-- 下面是h5原生websocket js写法 -->
<script type="text/javascript">
let output ;
let websocket;
function connect(){ //初始化连接
output = document.getElementById("echo-log")
let inputNode = document.getElementById("serverUrl");
let wsUri = inputNode.value;
try{
websocket = new WebSocket(wsUri);
}catch(ex){
console.log(ex)
alert("对不起websocket连接异常")
}

connecting();
window.addEventListener("load", connecting, false);
}


function connecting()
{
websocket.onopen = function(evt) { onOpen(evt) };
websocket.onclose = function(evt) { onClose(evt) };
websocket.onmessage = function(evt) { onMessage(evt) };
websocket.onerror = function(evt) { onError(evt) };
}

function sendEvent(){
let msg = document.getElementById("txtMsg").value
doSend(msg);
}

//连接上事件
function onOpen(evt)
{
writeToScreen("CONNECTED");
doSend("WebSocket 已经连接成功!");
}

//关闭事件
function onClose(evt)
{
writeToScreen("连接已经断开!");
}

//后端推送事件
function onMessage(evt)
{
writeToScreen('<span style="color: blue;">服务器: ' + evt.data+'</span>');
}

function onError(evt)
{
writeToScreen('<span style="color: red;">异常信息:</span> ' + evt.data);
}

function doSend(message)
{
writeToScreen("客户端A: " + message);
websocket.send(message);
}

//清除div的内容
function clearLog(){
output.innerHTML = "";
}

//浏览器主动断开连接
function wsclose(){
websocket.close();
}

function writeToScreen(message)
{
let pre = document.createElement("p");
pre.innerHTML = message;
output.appendChild(pre);
}
</script>
</html>
  • config
1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MessageHandler(), "/message")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.setAllowedOrigins("*"); //允许跨域访问
}
}
  • handler
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
38
39
public class MessageHandler extends TextWebSocketHandler {

Logger log = LoggerFactory.getLogger(MessageHandler.class);

//用来保存连接进来session
private List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();

/**
* 关闭连接进入这个方法处理,将session从 list中删除
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessions.remove(session);
log.info("{} 连接已经关闭,现从list中删除 ,状态信息{}", session, status);
}

/**
* 三次握手成功,进入这个方法处理,将session 加入list 中
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
log.info("用户{}连接成功.... ",session);
}

/**
* 处理客户发送的信息,将客户发送的信息转给其他用户
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
log.info("收到来自客户端的信息: {}",message.getPayload());
session.sendMessage(new TextMessage("当前时间:"+
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss")) +",收到来自客户端的信息!"));
for(WebSocketSession wss : sessions)
if(!wss.getId().equals(session.getId())){
wss.sendMessage(message);
}
}
}

你知道的越多 你不知道的越多 嘿 我是小博 带你一起看我目之所及的世界……

-------------本文结束 感谢您的阅读-------------

本文标题:spring mvc复习整理三

文章作者:小博

发布时间:2022年05月29日 - 19:57

最后更新:2022年05月29日 - 19:58

原始链接:https://codexiaobo.github.io/posts/120786435/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。