开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

七日打卡-使用nodejs和express搭建http we

发表于 2021-01-15

[toc]

简介

nodejs作为一个优秀的异步IO框架,其本身就是用来作为http web服务器使用的,nodejs中的http模块,提供了很多非常有用的http相关的功能。

虽然nodejs已经带有http的处理模块,但是对于现代web应用程序来说,这或许还不太够,于是我们有了express框架,来对nodejs的内容进行扩展。

今天我们将会介绍一下使用nodejs和express来开发web应用程序的区别。

使用nodejs搭建HTTP web服务

nodejs提供了http模块,我们可以很方便的使用http模块来创建一个web服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码const http = require('http')

const hostname = '127.0.0.1'
const port = 3000

const server = http.createServer((req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.end('welcome to www.flydean.com\n')
})

server.listen(port, hostname, () => {
console.log(`please visit http://${hostname}:${port}/`)
})

上面创建的http服务监听在3000端口。我们通过使用createServer方法来创建这个http服务。

该方法接受一个callback函数,函数的两个参数分别是 req (http.IncomingMessage 对象)和一个res(http.ServerResponse 对像)。

在上面的例子中,我们在response中设置了header和body值,并且以一个end方法来结束response。

请求nodejs服务

我们创建好http web服务之后,一般情况下是从web浏览器端进行访问和调用。但是我们有时候也需要从nodejs后端服务中调用第三方应用的http接口,下面的例子将会展示如何使用nodejs来调用http服务。

先看一个最简单的get请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js复制代码const http = require('http')
const options = {
hostname: 'www.flydean.com',
port: 80,
path: '/',
method: 'GET'
}

const req = http.request(options, res => {
console.log(`status code: ${res.statusCode}`)

res.on('data', d => {
console.log(d);
})
})

req.on('error', error => {
console.error(error)
})

req.end()

上面代码我们使用了http.request来创建一个request,并且传入了我们自带的options参数。

我们通过res的回调事件来进行相应的处理。

再看一个简单的post请求:

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
js复制代码const http = require('http')

const data = JSON.stringify({
name: 'flydean'
})

const options = {
hostname: 'www.flydean.com',
port: 80,
path: '/',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': data.length
}
}

const req = http.request(options, res => {
console.log(`status code: ${res.statusCode}`)

res.on('data', d => {
console.log(d);
})
})

req.on('error', error => {
console.error(error)
})

req.write(data)
req.end()

post和get相似,不同的是options中的method不一样,同时put可以有多种请求类型,所以我们需要在headers中指定。

同样的,PUT 和 DELETE 也可以使用同样的方式来调用。

第三方lib请求post

直接使用nodejs底层的http.request有点复杂,我们需要自己构建options,如果使用第三方库,比如axios可以让post请求变得更加简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码const axios = require('axios')

axios
.post('http://www.flydean.com', {
name: 'flydean'
})
.then(res => {
console.log(`status code: ${res.statusCode}`)
console.log(res)
})
.catch(error => {
console.error(error)
})

上面的例子中,我们直接使用axios的post请求,并将请求结果封存成了promise,然后通过then和catch来进行相应数据的处理。非常的方便。

获取http请求的正文

在上面的例子中,我们通过监听req的data事件来输出http请求的正文:

1
2
3
4
js复制代码  res.on('data', d => {
console.log(d);
})
})

这样做其实是有问题的,并不一定能够获得完整的http请求的正文。

因为res的on data事件是在服务器获得http请求头的时候触发的,这个时候请求的正文可能还没有传输完成,换句话说,请求回调中的request是一个流对象。

我们需要这样处理:

1
2
3
4
5
6
7
8
9
js复制代码const server = http.createServer((req, res) => {
let data = []
req.on('data', chunk => {
data.push(chunk)
})
req.on('end', () => {
console.log(JSON.parse(data));
})
})

当每次触发data事件的时候,我们将接受到的值push到一个数组里面,等所有的值都接收完毕,触发end事件的时候,再统一进行输出。

这样处理显然有点麻烦。

我们介绍一个在express框架中的简单方法,使用 body-parser 模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码const bodyParser = require('body-parser')

app.use(
bodyParser.urlencoded({
extended: true
})
)

app.use(bodyParser.json())

app.post('/', (req, res) => {
console.log(req.body)
})

上面的例子中,body-parser对req进行了封装,我们只用关注与最后的结果即可。

Express和使用express搭建http web服务

express是什么呢?

express是基于 Node.js 平台,快速、开放、极简的 web 开发框架。它提供一系列强大的特性,帮助你创建各种 Web 和移动设备应用。

丰富的 HTTP 快捷方法和任意排列组合的 Connect 中间件,让你创建健壮、友好的 API 变得既快速又简单。

Express 不对 Node.js 已有的特性进行二次抽象,我们只是在它之上扩展了 Web 应用所需的基本功能。

express helloworld

我们看一下怎么使用Express来搭建一个helloworld:

1
2
3
4
5
6
7
8
9
10
js复制代码var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello World!');
});
var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});

简单的使用app.listen即可搭建好一个http web服务。

express路由

有了web服务,我们需要对不同的请求路径和请求方式进行不同的处理,这时候就需要使用到了express路由功能:

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码// 对网站首页的访问返回 "Hello World!" 字样
app.get('/', function (req, res) {
res.send('Hello World!');});
// 网站首页接受 POST 请求
app.post('/', function (req, res) {
res.send('Got a POST request');});
// /user 节点接受 PUT 请求
app.put('/user', function (req, res) {
res.send('Got a PUT request at /user');});
// /user 节点接受 DELETE 请求
app.delete('/user', function (req, res) {
res.send('Got a DELETE request at /user');});

更高级一点的,我们还可以在请求路径中做路由匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码// 匹配 acd 和 abcd
app.get('/ab?cd', function(req, res) {
res.send('ab?cd');});
// 匹配 abcd、abbcd、abbbcd等
app.get('/ab+cd', function(req, res) {
res.send('ab+cd');
});
// 匹配 abcd、abxcd、abRABDOMcd、ab123cd等
app.get('/ab*cd', function(req, res) {
res.send('ab*cd');
});
// 匹配 /abe 和 /abcde
app.get('/ab(cd)?e', function(req, res) {
res.send('ab(cd)?e');});

// 匹配任何路径中含有 a 的路径:
app.get(/a/, function(req, res) {
res.send('/a/');
});

// 匹配 butterfly、dragonfly,不匹配 butterflyman、dragonfly man等
app.get(/.*fly$/, function(req, res) {
res.send('/.*fly$/');
});

Express 路由句柄中间件

有时候,一个请求可能有多个处理器,express提供了路由句柄(中间件)的功能,我们可自由组合处理程序。

注意,在路由句柄中,我们需要调用next方法,来触发下一个路由方法。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码var cb0 = function (req, res, next) {
console.log('CB0');
next();}
var cb1 = function (req, res, next) {
console.log('CB1');
next();}
app.get('/example/d', [cb0, cb1], function (req, res, next) {
console.log('response will be sent by the next function ...');
next();
}, function (req, res) {
res.send('Hello from D!');
});

上面的请求会经过cb0,cb1和自定义的两个function,最终结束。

Express 响应方法

express提供了很多响应方法API,可以方便我们的代码编写:

方法 描述
res.download() 提示下载文件。
res.end() 终结响应处理流程。
res.json() 发送一个 JSON 格式的响应。
res.jsonp() 发送一个支持 JSONP 的 JSON 格式的响应。
res.redirect() 重定向请求。
res.render() 渲染视图模板。
res.send() 发送各种类型的响应。
res.sendFile 以八位字节流的形式发送文件。
res.sendStatus() 设置响应状态代码,并将其以字符串形式作为响应体的一部分发送。

Express 的静态资源

通常来说,静态资源是不需要服务端进行处理的,在express中,可以使用express.static来指定静态资源的路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码app.use(express.static('public'));
现在,public 目录下面的文件就可以访问了。
http://localhost:3000/images/kitten.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/images/bg.png
http://localhost:3000/hello.html
//多个静态资源目录
app.use(express.static('public'));
app.use(express.static('files'));
//静态前缀
app.use('/static', express.static('public'));
http://localhost:3000/static/images/kitten.jpg
http://localhost:3000/static/css/style.css

Express 使用模板引擎

web应用当然需要html文件,express中可以使用多种模板语言,让编写html页面更加容易。如果想要使用模板引擎。我们可以使用下面的步骤:

  1. views, 放模板文件的目录,比如: app.set(‘views’, ‘./views’)
  2. view engine, 模板引擎,比如: app.set(‘view engine’, ‘jade’)
  3. 在 views 目录下生成名为 index.jade 的 Jade 模板文件,内容如下:
1
2
3
4
5
html复制代码html
head
title!= title
body
h1!= message
  1. 在nodejs服务端配置route规则
1
2
3
4
js复制代码//配置route 规则
app.get('/', function (req, res) {
res.render('index', { title: 'Hey', message: 'Hello there!'});
});

总结

nodejs和express是非常方便的http web服务框架,希望大家能够喜欢。

本文作者:flydean程序那些事

本文链接:www.flydean.com/nodejs-http…

本文来源:flydean的博客

欢迎关注我的公众号:「程序那些事」最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Feign调用GET方法,入参POJO对象有LocalDat

发表于 2021-01-15

情景复现

  • 当用Feign调用另外一个服务的GET方法,入参POJO对象有数据类型为LocalDateTime的属性时;
1
2
3
java复制代码    // 服务调用方入参 OrderReq 类中有一个属性为 LocalDateTime 
@RequestMapping(value = "find-all", method = RequestMethod.GET)
ServerResponse<PageImpl<OrderDTO>> findAll(@SpringQueryMap OrderReq req);

url 经过解码之后,会莫名其妙的转成默认的ISO-8601的日期格式即中间多了个T

/find-all?startTime=2020-05-23T23:23:23

这是我的全局配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码        @Bean
public Jackson2ObjectMapperBuilderCustomizer customJackson() {
return jacksonObjectMapperBuilder -> {
//若POJO对象的属性值为null,序列化时不进行显示
/*jacksonObjectMapperBuilder.serializationInclusion(JsonInclude.Include.NON_NULL);*/

//针对于Date类型,文本格式化
jacksonObjectMapperBuilder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");

//针对于JDK新时间类。序列化时带有T的问题,自定义格式化字符串
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
jacksonObjectMapperBuilder.modules(javaTimeModule);
};
}

当时尝试了很多办法,被调用方还是接不到数据类型为LocalDateTime的属性。

解决办法

经过和同事的讨论以及同事 debugger feign 对GET方法的实现。解决方案如下:

  • 方法一:把 GET 方法变成 POST ,这是最简单的。(手动狗头)
  • 方法二:加上 @org.springframework.format.annotation.DateTimeFormat 注解
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
java复制代码// 方法一:把 GET 方法变成 POST ,这是最简单的。(手动狗头)
// 方法二:加上 @org.springframework.format.annotation.DateTimeFormat 注解。

enum ISO {
/**
* The most common ISO Date Format {@code yyyy-MM-dd},
* e.g. "2000-10-31".
*/
DATE,

/**
* The most common ISO Time Format {@code HH:mm:ss.SSSXXX},
* e.g. "01:30:00.000-05:00".
*/
TIME,

/**
* The most common ISO DateTime Format {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX},
* e.g. "2000-10-31T01:30:00.000-05:00".
* <p>This is the default if no annotation value is specified.
*/
DATE_TIME,

/**
* Indicates that no ISO-based format pattern should be applied.
*/
NONE
}
@ApiModelProperty(value = "开始时间", name = "startTime")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private LocalDateTime startTime;

解析

知识点:spring 框架提供的org.springframework.format.annotation.DateTimeFormat和 jackson 提供的com.fasterxml.jackson.annotation.JsonFormat。

feign 对 GET 方法的处理

feign.Feign中的静态内部类Builder中的 queryMapEncoder 属性。FieldQueryMapEncoder实现了QueryMapEncoder接口并实现了Map<String, Object> encode(Object object)方法。这个方法的作用是把GET方法的入参变成Map<String, Object>格式。这个方法仅在BuildTemplateByResolvingArgs#toQueryMap方法中被引用。

1
java复制代码private QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder();

重点是BuildTemplateByResolvingArgs#addQueryMapQueryParameters的这个方法。从这个方法中可以看到Map<String, Object>被遍历解析并判断数据类型currValue instanceof Iterable<?>是不是可迭代的。最终所有的参数都是通过currValue.toString()方法被解析的。这也就是为什么LocalDateTime数据类型的属性被解析成url字符串中间带了一个T。所以最终的解决办法也就是加上注解@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)。

整个的调用流程是:其实都是在create方法中完成的。

  1. BuildTemplateByResolvingArgs#create(Object[] argv)
  2. BuildTemplateByResolvingArgs#toQueryMap(Object value)
  3. BuildTemplateByResolvingArgs#addQueryMapQueryParameters(Map<String, Object> queryMap, RequestTemplate mutable)
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
java复制代码@SuppressWarnings("unchecked")
private RequestTemplate addQueryMapQueryParameters(Map<String, Object> queryMap,
RequestTemplate mutable) {
for (Entry<String, Object> currEntry : queryMap.entrySet()) {
Collection<String> values = new ArrayList<String>();

boolean encoded = metadata.queryMapEncoded();
Object currValue = currEntry.getValue();
if (currValue instanceof Iterable<?>) {
Iterator<?> iter = ((Iterable<?>) currValue).iterator();
while (iter.hasNext()) {
Object nextObject = iter.next();
values.add(nextObject == null ? null
: encoded ? nextObject.toString()
: UriUtils.encode(nextObject.toString()));
}
} else {
values.add(currValue == null ? null
: encoded ? currValue.toString() : UriUtils.encode(currValue.toString()));
}

mutable.query(encoded ? currEntry.getKey() : UriUtils.encode(currEntry.getKey()), values);
}
return mutable;
}

@Override
public RequestTemplate create(Object[] argv) {
RequestTemplate mutable = RequestTemplate.from(metadata.template());
mutable.feignTarget(target);
if (metadata.urlIndex() != null) {
int urlIndex = metadata.urlIndex();
checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
mutable.target(String.valueOf(argv[urlIndex]));
}
Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
for (Entry<Integer, Collection<String>> entry : metadata.indexToName().entrySet()) {
int i = entry.getKey();
Object value = argv[entry.getKey()];
if (value != null) { // Null values are skipped.
if (indexToExpander.containsKey(i)) {
value = expandElements(indexToExpander.get(i), value);
}
for (String name : entry.getValue()) {
varBuilder.put(name, value);
}
}
}

RequestTemplate template = resolve(argv, mutable, varBuilder);
if (metadata.queryMapIndex() != null) {
// add query map parameters after initial resolve so that they take
// precedence over any predefined values
Object value = argv[metadata.queryMapIndex()];
Map<String, Object> queryMap = toQueryMap(value);
template = addQueryMapQueryParameters(queryMap, template);
}

if (metadata.headerMapIndex() != null) {
template =
addHeaderMapHeaders((Map<String, Object>) argv[metadata.headerMapIndex()], template);
}

return template;
}

private Map<String, Object> toQueryMap(Object value) {
if (value instanceof Map) {
return (Map<String, Object>) value;
}
try {
// 就是在这里被引用了
return queryMapEncoder.encode(value);
} catch (EncodeException e) {
throw new IllegalStateException(e);
}
}

POST 方法

用jackson序列化,post请求包括都是经过序列化器序列化。我的项目用了全局序列化器,也可以自定义序列化器。

@ResponseBody 响应

用jackson序列化,post请求包括都是经过序列化器序列化。我的项目用了全局序列化器,也可以自定义序列化器。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一个登录功能也能玩出这么多花样?sa-token带你轻松搞定

发表于 2021-01-15

需求场景

说起登录,你可能会不屑一顾,还有比这更简单的功能吗?

获取一下用户提交参数 username + password 和数据库中一比对,有记录返回[登录成功],无记录返回[用户名或密码错误]

什么,就这?

当你熟练的打包、部署、启动项目开始了一天的摸鱼之后,产品经理开始坐不住了

“小顺子啊,你看咱们的APP登录能不能加一个功能,就是那种……那个……一个用户登录之后,能把上一个登录的自动挤下线”

此时的你陷入了沉思,怎么让他在登录之后,把上一个登录者的会话给挤下线呢?

难道说要在每次登录之后循环一遍Session列表,找到与此用户同账号的会话将其注销,聪明如你马上想到了这种方案将会给服务器带来巨大的性能压力!

那怎么办?难道要建个Map以userId做key、Session做value,建立起映射关系,然后手动取出Session做上标记[已被挤下线]?

说干就干,当你撸起袖子,噼里啪啦敲好上述逻辑之后,然后测试、打包、部署、上传一气呵成,又开始了一天的摸鱼……

然而你还是低估了产品经理的脑洞能力

“小顺子,你看你写的功能有点小问题啊,我每次一登录,就会把其它登录地给挤掉线啊。”

此时的你下意识反驳到: “有什么问题?这难道不就是你想要的效果吗?”

“en….就是….咱们能不能这样,我在手机上登录,能不能只把别的手机上给挤下线,但是我电脑上已经登录的不受影响”

“挤掉肯定是全部挤掉啊,怎么可能只留下你电脑端不挤掉呢?你要的功能不可能做到”

只见此时产品经理嘴角轻轻一笑,放出了大招:

“那人家腾讯QQ是怎么做到的呢?”

一句话暴击99999+,顿时你哑口无言,是呀,腾讯QQ怎么做到这种功能的呢?一个QQ号可以在手机和电脑上同时在线,但是却不能两个手机同时在线

难道说在登录时再记录时每次登录的设备标识?循环检测登录列表的设备名称,同设备挤下线,不同设备保持登录?

产品经理冲上咖啡,带着胜利的微笑离开了房间,只留下一脸愁容的你,冥思苦想着实现方案……


正题

好了,说了这么多,下面进入今天的主题————sa-token,一个可以让你轻松解决各种登录问题的权限认证框架!

如上述场景所言,你遇到的问题不过是三个典型的登录模型:多地登录、单地登录、同端互斥登录

  • 多地登录:指同一账号可以在任意地点同时登录,互不影响
  • 单地登录:在同一时间一个账号只能在一个地点登录,新登录会挤掉旧登录者
  • 同端互斥登录:在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线

接下来让我们看看使用sa-token是如何轻松处理这三种登录问题的

多地登录

此模式较为简单,sa-token默认模式即为多地登录模式

  1. 首先添加pom.xml框架
1
2
3
4
5
6
xml复制代码<!-- sa-token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.12.1</version>
</dependency>
  1. 在用户登录时将账号id写入会话中
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@RestController
@RequestMapping("user")
public class UserController {
@RequestMapping("doLogin")
public String doLogin(String username, String password) {
// 此处仅作示例模拟,真实项目需要从数据库中查询数据进行比对
if("zhang".equals(username) && "123456".equals(password)) {
StpUtil.setLoginId(10001);
return "登录成功";
}
return "登录失败";
}
}
  1. 新建启动类启动
1
2
3
4
5
6
7
java复制代码@SpringBootApplication
public class SaTokenDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SaTokenDemoApplication.class, args);
System.out.println("\n启动成功:sa-token配置如下:" + SaTokenManager.getConfig());
}
}

至此,我们已经完成了多地点登录的全部代码,上述代码在多人登录同一账号时将不会对旧会话做任何处理,同一账号可以在多个地点任意登录,互不影响

单地登录

单地登录与多地登录唯一的差异就是, 需要改一下yml配置文件

1
2
3
4
java复制代码spring: 
# sa-token配置
sa-token:
allow-concurrent-login: false

配置项 allow-concurrent-login 的含义为:是否允许同一账号并发登录 (此值为true时允许一起登录, 为false时新登录挤掉旧登录)

其它代码与[多地登录]无异,当我们在两个浏览器分别登录同一账号时,旧会话再次访问系统将会得到如下提示:

1
2
3
4
5
6
java复制代码{
"code": 401,
"msg": "token已被顶下线",
"data": null,
"dataCount": null
}

同端互斥登录

好了,终于到了最终难题,同端互斥登录可以让我们像腾讯QQ一样,在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线

那么在sa-token中如何做到同端互斥登录呢?

首先如单地登录一样,在配置文件中,将 allowConcurrentLogin 配置为false,然后调用登录等相关接口时声明设备标识即可:

指定设备标识登录
1
java复制代码StpUtil.setLoginId(10001, "PC");

调用此方法登录后,同设备的会被顶下线(不同设备不受影响),再次访问系统时会抛出 NotLoginException 异常,场景值=-4

指定设备标识强制注销(踢人下线)
1
java复制代码StpUtil.logoutByLoginId(10001, "PC");

如果第二个参数填写null或不填,代表将这个账号id所有在线端踢下线,被踢出者再次访问系统时会抛出 NotLoginException 异常,场景值=-5

查询当前登录的设备标识
1
java复制代码StpUtil.getLoginDevice();

结尾

以上就是sa-token框架在处理登录问题时的各种技巧,可以看出不管是简单的多地登录还是复杂的同端互斥登录,在sa-token都有完成的解决方案

sa-token是近期开源的国产优秀权限认证框架,除了各种登录认证,sa-token还可以轻松解决项目中的各种权限认证问题,
比如:踢人下线、自动续签、身份临时切换等常见业务均可以一行代码调用实现,接下来的文章我会逐一介绍这些特性,让大家对sa-token有一个全面的了解

如果觉得文章写得不错还请大家不要吝惜为文章点个赞,您的支持是我更新的最大动力!

最后附上项目链接:

  • 官网文档:sa-token.dev33.cn/
  • Gitee开源地址: gitee.com/sz6/sa-toke…
  • GitHub开源地址: github.com/click33/sa-…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

jdk18的hashmap真的是大于8就转换成红黑树,小于

发表于 2021-01-15

免责声明

本文夹杂部分笔者个人观点,如描述有误,欢迎指正

前言

写这篇文章,是因为最近研究hashmap源码的时候,会结合网上的一些博客来促进理解。而关于红黑树和链表相互转换这一块,大部分的文章都会这样描述:hashmap中定义了两个常量:

1
2
arduino复制代码static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;

当链表元素个数大于8的时候,就会转换为红黑树;当红黑树元素个数小于6的时候,就会转换回链表。
笔者通过仔细观察,发现这种说法并不严谨。hashMap中确实定义了这两个常量,但并非简单通过元素个数的判断来进行转换。

链表转换为红黑树

链表转换为红黑树的最终目的,是为了解决在map中元素过多,hash冲突较大,而导致的读写效率降低的问题。在源码的putVal方法中,有关红黑树结构化的分支为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码                //此处遍历链表
for (int binCount = 0; ; ++binCount) {
//遍历到链表最后一个节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果链表元素个数大于等于TREEIFY_THRESHOLD
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//红黑树转换逻辑
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}

即网上所说的,链表的长度大于8的时候,就转换为红黑树,我们来看看treeifyBin方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//先判断table的长度是否小于 MIN_TREEIFY_CAPACITY (64)
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//小于64,则扩容
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//否则才将链表转换为红黑树
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}

可以看到在treeifyBin中并不是简单地将链表转换为红黑树,而是先判断table的长度是否大于64,如果小于64,就通过扩容的方式来解决,避免红黑树结构化。原因呢?笔者个人觉得链表长度大于8有两种情况:

  • 1、table长度足够,hash冲突过多
  • 2、hash没有冲突,但是在计算table下标的时候,由于table长度太小,导致很多hash不一致的key计算的下标一致

第二种情况是可以用扩容的方式来避免的,扩容后链表长度变短,读写效率自然提高。另外,扩容相对于转换为红黑树的好处在于可以保证数据结构更简单。
由此可见并不是链表长度超过8就一定会转换成红黑树,而是先尝试扩容

红黑树转换为链表

基本思想是当红黑树中的元素减少并小于一定数量时,会切换回链表。而元素减少有两种情况:

  • 1、调用map的remove方法删除元素

hashMap的remove方法,会进入到removeNode方法,找到要删除的节点,并判断node类型是否为treeNode,然后进入删除红黑树节点逻辑的removeTreeNode方法中,该方法有关解除红黑树结构的分支如下:

1
2
3
4
5
6
java复制代码//判断是否要解除红黑树的条件
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}

可以看到,此处并没有利用到网上所说的,当节点数小于UNTREEIFY_THRESHOLD时才转换,而是通过红黑树根节点及其子节点是否为空来判断。而满足该条件的最大红黑树结构如下:
image.png
节点数为10,大于 UNTREEIFY_THRESHOLD(6),但是根据该方法的逻辑判断,是需要转换为链表的

  • 2、resize的时候,对红黑树进行了拆分

resize的时候,判断节点类型,如果是链表,则将链表拆分,如果是TreeNode,则执行TreeNode的split方法分割红黑树,而split方法中将红黑树转换为链表的分支如下:

1
2
3
4
5
6
7
8
9
java复制代码//在这之前的逻辑是将红黑树每个节点的hash和一个bit进行&运算,
//根据运算结果将树划分为两棵红黑树,lc表示其中一棵树的节点数
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}

这里才用到了 UNTREEIFY_THRESHOLD 的判断,当红黑树节点元素小于等于6时,才调用untreeify方法转换回链表

总结

  • 1、hashMap并不是在链表元素个数大于8就一定会转换为红黑树,而是先考虑扩容,扩容达到默认限制后才转换
  • 2、hashMap的红黑树不一定小于6的时候才会转换为链表,而是只有在resize的时候才会根据 UNTREEIFY_THRESHOLD 进行转换。(原因?不太明白hhh)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

聊一聊:MyBatis和Spring Data JPA的选择

发表于 2021-01-15

从个人开发角度来说,Spring Data JPA更好用,是因为开发起来更快。

但从团队角度,我们希望更好的维护性,spring data jpa就差一些,或者说对后期人的要求更高。

很容易出现这种情况:

监控系统发现某个慢查询了,运维把SQL发到开发群里,大家自查一下。

此时很可能发现根本没人回应,都说没有这句SQL。

然后运维定位到某个库,找到这个库的使用人,让他去看。

他可能也就拿着SQL全局去搜,发现还是搜不到。

如果这个人责任心不强,可能就说 没找到这个SQL,责任心强调的,对Spring Data JPA熟悉点的,就要开始去分析这个SQL可能在哪里,然后找到对应的实现地方去修改。

这就是Spring Data JPA在团队作战时候,容易引发维护成本高的真实场景。

P.S. 我开发自己独立产品的时候,还是喜欢用它的,因为自己再熟悉不过,不会有这样的场景。所以果断选择,但如果团队作战,我还是会选在MyBatis。

那么你怎么看呢?留言区见!

欢迎关注我的公众号:程序猿DD,获得独家整理的学习资源、日常干货及福利赠送。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

师兄:说说你理解的 Token

发表于 2021-01-14

前言

在学 Javaweb 的时候,我们就一直被强调 javaweb 中有四大作用域:pageContext域,request域,session域和 application域。

  • pageContext域里的变量只能在当前页面使用。
  • request域中对象的有效范围是当前的请求范围。
  • session域中对象的有效范围是当前的会话范围。
  • application域中对象的有效范围是整个应用活跃的方位。

了解基本概念后,综上我们可以得出若要进行网络通信,我们一般都要使用到 request 和 session。由于 request 的作用域只是在请求范围内有效,每次请求后数据就会自动销毁。但有时候我们有想要存储一些能够存储久的数据,例如登录信息,用户信息等,request域是做不到的。

服务端的 session 和 客户端(浏览器)的 cookie 的性质很相似,例如他们都能存储一定期限的数据。如果说Cookie机制是客户身上的“通行证”,那么Session机制就是多个“通行证”组成的“客户明细表”。「从理论上讲,这两个内存间是联系不到一块的。但是由于这两者相似的性质, java 的 jsp ,spring 的 thymeleaf模板就将他们关联了起来。让人们产生 session和 cookie 是一致的。」 以 jsp 为例,jsp 是一个特殊的 servlet 程序,虽然我们在里面编写的是 html 语句,但他实际上是会通过 servlet 的一些手段从而转变为前端页面。而 spring 和 thymeleaf 模板也用了这样的方式。因为 session 能够存储时间久的数据,故我们能够用 session 来存储 登录信息和用户信息等。

image

session/cookie 的弊端:

  • cookie 不是很安全,别人可以分析存放在本地的 cookie 并进行 cookie 欺骗。
  • 单个 cookie 在客户端的限制是 3K,就是说一个站点在客户端存放的 cookie 不能超过 3k。
  • session 会在一定时间内保存在服务器上。当访问量怎多,会占用服务器的性能。
  • 如果是在服务器集群下,或者跨域的服务导向架构,就要求 session 域中的数据共享,即每台服务器都能够读取 session。这个时候我们就只能将 session 数据持久化,这是一个非常糟糕的办法。

更加重大的问题

如果用户在浏览器的设置里禁用了 cookie;或者你的网站是使用前后分离的技术来实现。根本就无法简单的使用 session 就能解决了。一是由于浏览器中没了 cookie,你的数据无法存储了,二是前后端的联系被撕裂了,单独设置服务端的 session 无法同步到 浏览器的 cookie 中。此时我们就需要用另一个思想:自己在前后端都分别开辟一个空间来存储用户信息。

token 就是这样的一种思想。其流程为:

  1. 客户端使用用户名跟密码请求登录。
  2. 服务端收到请求,去验证用户名与密码。
  3. 验证成功后,服务端再生成一个独一无二的字符串(Token 令牌),并且将这个令牌保存到服务器的缓存中,再把这个 令牌发送给客户端。
  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 LocalStorage 里。
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token。
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据。

image

token 带来的好处

此外,token 除了能解决这个现实问题,也带来了许多的好处。

  1. 无状态,可扩展:在客户端存储的 tokens 是无状态的,并且能够被扩展。基于这种无状态和不存储 session 信息,负载均衡器能够将用户信息从一个服务器传到其他服务器上。
  2. 安全:请求中发送 token 而不再发送 cookie,能够防止 CSRF(跨站请求伪造)。即使在客户端使用 cookie 存储 token,cookie 也仅仅是一个存储机制而不是用于认证。并且 token 是有时效的,一段时间后用户需要重新验证。
  3. 可扩展性:能够与其他程序共享数据。
  4. 多平台跨域:每次都是携带绑定自己信息的令牌访问服务器,当在服务集群环境时,不是只能访问某一台指定的服务器,而是通过从缓存服务器中拿出数据,并更具负载均衡器来选择处理的服务器。

Token 的实现

准备阶段

  1. 使用 Mysql 数据库
  2. 开启 Redis 缓存
  3. 使用 Springboot 框架

代码段

1. 登录

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
复制代码<form id="login_form" class="login_form">
    用户名:<input type="text" name="username">
    <br>
    密码:<input type="password" name="password">
    <br>
    <input type="button" value="提交" onclick="login()">
</form>
<script type="application/javascript">
    function login(){
        $.ajax({
            type:"post",    // post 请求
            dataType:"json",    // 返回 json 格式
            url:"http://localhost:8080/login",     // 请求路径
            data:$("#login_form").serialize(),     // 表单序列化
            success: function(result){
                console.log(result)                // 打印结果
                if(result.code == 200){
                    // 存储 token
                    localStorage.token = result.data;     // 将 token 存到 localStorage 里
                    location.href = "/index";           // 跳转到首页
                }
            },fail:function(err){
                console.log(err)
            }
        });
    }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码@Resource
RedisTemplate<String, Object> redisTemplate;       // redis 缓存模板

@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public Result login(String username, String password){
    System.out.println(username);
    System.out.println(password);
    User user = userService.getUser(username, password);
    if(user != null){
        // 登录成功,生成 token 令牌
        String token = UUID.randomUUID() + "";     // 使用 uuid 生成唯一 key,以此作为 token 令牌
        redisTemplate.opsForValue().set(token, user, Duration.ofMinutes(30L));    // 将 token 放到 redis 缓存中,存半个小时
        return new Result(200, "请求成功", token);
    }
    return new Result(400, "请求失败", null);
}

2. 资源访问请求

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码function requestToken(){
    $.ajax({
        type:"get",                           // 请求方式
        dataType: "json",                     // 返回格式
        url:"http://localhost:8080/getUser",   // 请求地址
        headers : {
            "token" : localStorage.token        // 设置 header 头,将 token 放到请求头中更加安全
        },
        success:function(result){
            console.log(result)
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
复制代码@RequestMapping("/getUser")
@ResponseBody
public Result getUserOfLogin(HttpServletRequest request){
    String token = request.getHeader("token");   // 从请求头中获取 token
    Object user = redisTemplate.opsForValue().get(token);   // 在 redis 缓存中寻找 token 对应的信息

    if(user != null){
        // 获取成功,封装返回值
        return new Result(200, "请求成功", user);
    }
    return new Result(400, "找不到用户信息", null);    // 可能没存在这个 token 或者 token 以过期
}

3. 注销

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码function logout(){
    $.ajax({
        type:"get",
        url:"http://localhost:8080/logout",
        dataType:"json",
        headers:{
            "token":localStorage.token
        },
        success:function(result){
            localStorage.removeItem("token");   // 在浏览器端清除 token
        },fail : function(err){
            console.log(err)
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
复制代码@RequestMapping("/logout")
@ResponseBody
public Result logout(HttpServletRequest request){
    String token = request.getHeader("token");     // 从请求头中获取 token
    Boolean delete = redisTemplate.delete(token);   // 根据 token 删除 redis 缓存里对应的信息
    if(delete){
        return new Result(200, "注销成功", null);
    }else{
        return new Result(400, "注销失败", null);

    }
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于SpringBoot实现文件的上传下载 (一)概述 (二

发表于 2021-01-14

听说微信搜索《Java鱼仔》会变更强哦!

本文收录于JavaStarter ,里面有我完整的Java系列文章,学习或面试都可以看看哦

(一)概述

文件上传下载一直都是一个系统最常用也是最基本的功能点,刚好最近公司的项目上有用到这个功能,于是自己就用SpringBoot也写了一个简化的版本,已实现文件的上传和下载功能。

(二)创建项目

首先创建一个SpringBoot的项目,接着引入相关的依赖,因为涉及到数据库的操作,所以依赖会比较多一些。

2.1 依赖引入

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
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

2.2 接口通用返回类编写

编写一个接口的通用返回体,这个在之前的博客中我专门写过,现在就直接拿来用了:

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
java复制代码public enum ResponseCode {
// 系统模块
SUCCESS(0, "操作成功"),
ERROR(1, "操作失败"),
SERVER_ERROR(500, "服务器异常"),

// 通用模块 1xxxx
ILLEGAL_ARGUMENT(10000, "参数不合法"),
REPETITIVE_OPERATION(10001, "请勿重复操作"),
ACCESS_LIMIT(10002, "请求太频繁, 请稍后再试"),
MAIL_SEND_SUCCESS(10003, "邮件发送成功"),

// 用户模块 2xxxx
NEED_LOGIN(20001, "登录失效"),
USERNAME_OR_PASSWORD_EMPTY(20002, "用户名或密码不能为空"),
USERNAME_OR_PASSWORD_WRONG(20003, "用户名或密码错误"),
USER_NOT_EXISTS(20004, "用户不存在"),
WRONG_PASSWORD(20005, "密码错误"),

// 文件模块 3xxxx
FILE_EMPTY(30001,"文件不能空"),
FILE_NAME_EMPTY(30002,"文件名称不能为空"),
FILE_MAX_SIZE(30003,"文件大小超出"),
;

ResponseCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
private Integer code;
private String msg;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}

返回体:

1
2
3
4
5
6
7
8
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {
private int code;
private String message;
private Object data;
}

2.3 配置一下解决跨域问题的配置类

在SpringBoot中有多种解决跨域的方法,这里选择其中一种

1
2
3
4
5
6
7
8
9
10
java复制代码public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.maxAge(3600)
.allowCredentials(true);
}
}

到这里为止,基本的项目配置就算结束了,接下来就是功能的实现了。

(三)实现文件上传下载

3.1 创建表

首先创建一张表来记录文件的路径、名称、后缀等信息:

1
2
3
4
5
6
7
java复制代码CREATE TABLE `file` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`filePath` varchar(255) DEFAULT NULL,
`fileName` varchar(255) DEFAULT NULL,
`fileSuffix` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

3.2 编写实体类

写一个文件对象,和数据库中的字段相对应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
public class Files implements Serializable {

private static final long serialVersionUID=1L;
/**
* 文件存储路径
*/
private String filePath;
/**
* 文件名称
*/
private String fileName;
/**
* 文件后缀名
*/
private String fileSuffix;

}

3.3 配置application.properties

在配置文件中将服务端口,数据库连接方式以及文件的保存路径配置一下:

1
2
3
4
5
6
7
8
9
java复制代码server.port=8080

spring.datasource.url=jdbc:mysql://localhost:3306/test7?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

mybatis.mapper-locations=classpath:mapper/*.xml
file.save-path=E:/temp/files

3.4 编写Controller

新建一个类叫FileController,用来写接口,文件上传下载接口的代码已经给了注释,Spring中提供了一个MultipartFile类,用来接收前台传过来的文件,这里直接使用即可。

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
java复制代码@RestController
@RequestMapping("/api")
public class FileController {
@Autowired
private FileService fileService;

@RequestMapping(value = "/upload",method = RequestMethod.POST)
public Result upLoadFiles(MultipartFile multipartFile){
if (multipartFile.isEmpty()){
return new Result(ResponseCode.FILE_EMPTY.getCode(),ResponseCode.FILE_EMPTY.getMsg(),null);
}
return fileService.upLoadFiles(multipartFile);
}

@RequestMapping(value = "/download/{id}",method = RequestMethod.GET)
public void downloadFiles(@PathVariable("id") String id, HttpServletRequest request, HttpServletResponse response){
OutputStream outputStream=null;
InputStream inputStream=null;
BufferedInputStream bufferedInputStream=null;
byte[] bytes=new byte[1024];
Files files = fileService.getFileById(id);
String fileName = files.getFileName();
// 获取输出流
try {
response.setHeader("Content-Disposition", "attachment;filename=" + new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
response.setContentType("application/force-download");
inputStream=fileService.getFileInputStream(files);
bufferedInputStream=new BufferedInputStream(inputStream);
outputStream = response.getOutputStream();
int i=bufferedInputStream.read(bytes);
while (i!=-1){
outputStream.write(bytes,0,i);
i=bufferedInputStream.read(bytes);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (inputStream!=null){
inputStream.close();
}
if (outputStream!=null){
outputStream.close();
}
if (bufferedInputStream!=null){
bufferedInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}

}
}

}

所有的业务都写在service中,因此需要有fileService接口以及fileServiceImpl实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public interface FileService {
/**
* 文件上传接口
* @param file
* @return
*/
Result upLoadFiles(MultipartFile file);

/**
* 根据id获取文件
* @param id
* @return
*/
Files getFileById(String id);

/**
* 根据id获取数据流
* @param files
* @return
*/
InputStream getFileInputStream(Files files);
}

fileServiceImpl实现类:

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
java复制代码@Service
public class FileServiceImpl implements FileService {

@Value("${file.save-path}")
private String savePath;
@Autowired
private FileMapper fileMapper;

@Override
public Result upLoadFiles(MultipartFile file) {
//设置支持最大上传的文件,这里是1024*1024*2=2M
long MAX_SIZE=2097152L;
//获取要上传文件的名称
String fileName=file.getOriginalFilename();
//如果名称为空,返回一个文件名为空的错误
if (StringUtils.isEmpty(fileName)){
return new Result(ResponseCode.FILE_NAME_EMPTY.getCode(),ResponseCode.FILE_NAME_EMPTY.getMsg(),null);
}
//如果文件超过最大值,返回超出可上传最大值的错误
if (file.getSize()>MAX_SIZE){
return new Result(ResponseCode.FILE_MAX_SIZE.getCode(),ResponseCode.FILE_MAX_SIZE.getMsg(),null);
}
//获取到后缀名
String suffixName = fileName.contains(".") ? fileName.substring(fileName.lastIndexOf(".")) : null;
//文件的保存重新按照时间戳命名
String newName = System.currentTimeMillis() + suffixName;
File newFile=new File(savePath,newName);
if (!newFile.getParentFile().exists()){
newFile.getParentFile().mkdirs();
}
try {
//文件写入
file.transferTo(newFile);
} catch (IOException e) {
e.printStackTrace();
}
//将这些文件的信息写入到数据库中
Files files=new Files(newFile.getPath(),fileName,suffixName);
fileMapper.insertFile(files);
return new Result(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMsg(),"数据上传成功");
}

//根据id获取文件信息
@Override
public Files getFileById(String id) {
Files files = fileMapper.selectFileById(id);
return files;
}

//将文件转化为InputStream
@Override
public InputStream getFileInputStream(Files files) {
File file=new File(files.getFilePath());
try {
return new FileInputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return null;
}
}

3.5 对数据库的操作

需要将数据写入到数据库中,这里用到的是mybatis,新建一个FileMapper接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Mapper
@Repository
public interface FileMapper {
/**
* 将数据信息插入到数据库
* @param files
*/
void insertFile(Files files);

/**
* 根据id获取文件
* @param id
* @return
*/
Files selectFileById(String id);
}

编写对应的xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<?xml version="1.0" encoding="UTF8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.javayz.fileuploadanddownload.mapper.FileMapper">
<insert id="insertFile" parameterType="com.javayz.fileuploadanddownload.entity.Files">
insert into file(filepath,filename,filesuffix) values(#{filePath},#{fileName},#{fileSuffix});
</insert>

<select id="selectFileById" parameterType="string" resultType="com.javayz.fileuploadanddownload.entity.Files">
select * from file where id=#{id};
</select>
</mapper>

代码已上传至github,欢迎自取:github

(四)测试

将项目运行起来,首先测试文件上传,通过postman直接上传一个文件

在这里插入图片描述

点击send后得到操作成功的返回值,我们可以在自己设置的路径下看到这个文件,同时在数据库中也存在该文件的信息:

在这里插入图片描述

接下来测试文件下载,因为是get请求,直接在浏览器中访问:
http://localhost:8080/api/download/4
即可调用下载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Maven的安装与下载 Maven的安装与下载 Maven在

发表于 2021-01-14

Maven的安装与下载

maven官网

s
下载maven压缩包,maven的源文件结构非常简单,将解压得到的bin目录中的mvn.cmd配置到环境变量中即可

检查是否安装成功

打开cmd面板,输入mvn -v 命令
cmd面板

Maven在idea中的使用

在idea中选中setting,选择maven选项进行配置。
maven在idea中的使用

中央仓库的设置

maven默认会从国外的服务器下载需要的jar包,而对于国内来说,从国外的服务器下载比较耗费时间且对网络的要求较高。最好的办法是将中央仓库的地址改为阿里云提供的国内的仓库,阿里云仓库是免费的、国内最好的仓库。在maven配置中添加如下代码即可。只有当需要的包无法从阿里云仓库下载时,maven才会从国外服务器惊醒下载。

阿里云中央仓库

1
2
3
4
5
6
7
xml复制代码 <repositories>
<repository>
<id>aliyun</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>

怎么知道需要的包的groupId及artifactId呢?

maven依赖查询
直接进行搜索即可。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

HashMap为什么线程不安全

发表于 2021-01-14

一、学习目标

1、HashMap线程不安全原因:

原因:

  • JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap#transfer(),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。
  • JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap#putVal(),具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

改善:

  • 数据丢失、死循环已经在在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁移。

2、HashMap线程不安全的体现:

  • JDK1.7 HashMap线程不安全体现在:死循环、数据丢失
  • JDK1.8 HashMap线程不安全体现在:数据覆盖

二、HashMap线程不安全、死循环、数据丢失、数据覆盖的原因

1、JDK1.7 扩容引发的线程不安全

HashMap的线程不安全主要是发生在扩容函数中,其中调用了JDK1.7 HshMap#transfer():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}

这段代码是HashMap的扩容操作,重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。理解了头插法后再继续往下看是如何造成死循环以及数据丢失的。

2、扩容造成死循环和数据丢失

假设现在有两个线程A、B同时对下面这个HashMap进行扩容操作:

image

正常扩容后的结果是下面这样的:

image

但是当线程A执行到上面transfer函数的第11行代码时,CPU时间片耗尽,线程A被挂起。即如下图中位置所示:

image

此时线程A中:e=3、next=7、e.next=null

image

当线程A的时间片耗尽后,CPU开始执行线程B,并在线程B中成功的完成了数据迁移

image

重点来了,根据Java内存模式可知,线程B执行完数据迁移后,此时主内存中newTable和table都是最新的,也就是说:7.next=3、3.next=null。

随后线程A获得CPU时间片继续执行newTable[i] = e,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:

image

接着继续执行下一轮循环,此时e=7,从主内存中读取e.next时发现主内存中7.next=3,此时next=3,并将7采用头插法的方式放入新数组中,并继续执行完此轮循环,结果如下:

image

此时没任何问题。

上轮next=3,e=3,执行下一次循环可以发现,3.next=null,所以此轮循环将会是最后一轮循环。

接下来当执行完e.next=newTable[i]即3.next=7后,3和7之间就相互连接了,当执行完newTable[i]=e后,3被头插法重新插入到链表中,执行结果如下图所示:

image

上面说了此时e.next=null即next=null,当执行完e=null后,将不会进行下一轮循环。到此线程A、B的扩容操作完成,很明显当线程A执行完后,HashMap中出现了环形结构,当在以后对该HashMap进行操作时会出现死循环。

并且从上图可以发现,元素5在扩容期间被莫名的丢失了,这就发生了数据丢失的问题。

3、JDK1.8中的线程不安全

上面的扩容造成的数据丢失、死循环已经在在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁移。

为什么说 JDK1.8会出现数据覆盖的情况? 我们来看一下下面这段JDK1.8中的put操作代码:

image

其中第六行代码是判断是否出现hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

除此之前,还有就是代码的第38行处有个++size,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到第38行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。

三、如何使HashMap在多线程情况下进行线程安全操作?

使用 Collections.synchronizedMap(map),包装成同步Map,原理就是在HashMap的所有方法上synchronized。

例如:Collections.SynchronizedMap#get()

1
2
3
4
5
vbnet复制代码public V get(Object key) {
synchronized (mutex) {
return m.get(key);
}
}

四、总结

1、HashMap线程不安全原因:

原因:

  • JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap#transfer(),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。
  • JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap#putVal(),具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

改善:

  • 数据丢失、死循环已经在在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁移。

2、HashMap线程不安全的体现:

  • JDK1.7 HashMap线程不安全体现在:死循环、数据丢失
  • JDK1.8 HashMap线程不安全体现在:数据覆盖

五、参考

blog.csdn.net/swpu_ocean/…
coolshell.cn/articles/96…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java微服务 vs Go微服务,究竟谁更强!?

发表于 2021-01-14

前言

Java微服务能像Go微服务一样快吗?

这是我最近一直在思索地一个问题。

去年8月份的the Oracle Groundbreakers Tour 2020 LATAM大会上,Mark Nelson和Peter Nagy就对此做过一系列基础的的测试用以比较。接下来就给大家介绍下。

在程序员圈子里,普遍的看法是Java老、慢、无聊 ,而Go是快、新、酷

为了尽可能的进行一个相对公平的测试,他们使用了一个非常简单的微服务,没有外部依赖关系(比如数据库),代码路径非常短(只是操纵字符串),使用了小型的、轻量级的框架(Helidon for Java和Go工具包for Go),试验了不同版本的Java和不同的jvm。

对决双雄

我们先来看下擂台两边的选手:

  • 身穿深色战服的选手是JAVA

Java是由被甲骨文收购的Sun Microsystems开发的。它的1.0版本是1996年发布的,最新的版本是2020年的Java15。主要的设计目标是Java虚拟机和字节码的可移植性,以及带有垃圾收集的内存管理。它是全世界最流行的语言之一,在开源环境下开发。

我们先看下JAVA的问题,大家普遍认为它最大的问题就是速度慢,已经慢到让人觉得不再是合理的,而是更具历史意义的。不过这么多年来,Java诞生了很多不同的垃圾收集算法用来加快它运行的速度。

Oracle实验室最近已经开发了一个新的Java虚拟机GraalVM,它有一个新的编译器和一些令人兴奋的新特性,比如能够将Java字节码转换成一个本机映像,可以在没有javavm的情况下运行等。

  • 而它的对手就是年轻充满活力的GO

GO是由谷歌的罗伯特·格里默、罗伯·派克和肯·汤姆森创建的。他们对UNIX、B、C、Plan9、UNIX窗口系统等做出了重大贡献。GO是开源的,在2012年发布了1.0版本(比JAVA晚了16年),在2020年发布了1.15版本。无论是在采用方面,还是在语言和工具生态系统本身方面,它都在快速增长。

GO受C、Python、JavaScript和C++等多种语言的影响。被设计成高性能网络和多处理的最佳语言。

StackOverflow有27872个带“Go”的问题,而Java只有1702730个。足见长江后浪推前浪。

Go是一种静态类型的编译语言。它有称为goroutines的轻量级进程(这些不是OS线程),它们之间有独特的通信通道(类型化的,FIFO)。Go是许多CNCF项目的首选语言,例如Kubernetes、Istio、Prometheus和Grafana

赛前对比

从个人感觉来说,Go相比JAVA来说,优点在于:

  • Go更容易实现复合、纯函数、不变状态等功能模式。
  • Go处于生命周期的早期,因此它没有向后兼容性的沉重负担—Go仍然可以轻易打破某些限制来改进。
  • Go编译成一个本机静态链接的二进制文件-没有虚拟机层-二进制文件拥有运行程序所需的一切,这对于“从头开始”的容器来说非常好。
  • Go体积小、启动快、执行快(目前是的)
  • Go没有OOP,继承,泛型,断言,指针算法
  • Go写法上较少的括号
  • Go没有循环依赖、没有未使用的变量或导入、没有隐式类型转换的强制
  • Go样板代码少得多

缺点是:

  • Go工具生态系统还不成熟,尤其是依赖关系管理——有几个选项,没有一个是完美的,特别是对于非开源开发;仍然存在兼容性挑战。
  • 构建具有新的/更新的依赖项的代码非常慢(比如Maven著名的“下载Internet”问题)
  • 导入将代码绑定到存储库,这使得在存储库中移动代码成为一场噩梦。
  • 调试、评测等仍然是一个挑战
  • 用到了指针
  • 需要实现一些基本的算法
  • 没有动态链接
  • 没有太多旋钮来调优执行或垃圾收集、概要文件执行或优化算法。

比赛开始

使用JMeter来运行负载测试。这些测试多次调用这些服务,并收集有关响应时间、吞吐量(每秒事务数)和内存使用情况的数据。对于Go,收集驻留集大小;对于Java,跟踪本机内存。

在测量之前,使用1000次服务调用对应用程序进行预热。

应用程序本身的源代码以及负载测试的定义都在这个GitHub存储库中:github.com/markxnelson…

第一回合

在第一轮测试中,在一台“小型”机器上进行了测试,是一台2.5GHz双核Intel core i7笔记本电脑,16GB内存运行macOS。测试运行了100个线程,每个线程有10000个循环,上升时间为10秒。Java应用程序运行在JDK11和Helidon2.0.1上。使用Go 1.13.3编译的Go应用程序。

结果如下:

file

file

可以看出,第一回合是Go赢了!

JAVA占的内存太多了;预热对JVM有很大的影响—我们知道JVM在运行时会进行优化,所以这是有意义的

在第一回合的基础上,意犹未尽的又引入GraalVM映像以使 Java 应用程序的执行环境更接近于 Go 应用程序的环境,添加了 GraalVM 映像测试(用 GraalVM EE 20.1.1ー JDK 11构建的本机映像)的结果是:

file

file

通过使用 GraalVM 映像在 JVM 上运行应用程序,我们没有看到吞吐量或响应时间方面的任何实质性改进,但是内存占用的确变小了。

下面是一些测试的响应时间图:

file

第二回合

在第二轮测试中,使用一台更大的机器上运行测试。36核(每个核两个线程)、256GB内存、运行oraclelinux7.8的机器。

和第一轮类似,使用了100个线程,每个线程使用了10,000个循环,10秒的加速时间,以及相同版本的 Go,Java,Helidon 和 GraalVM。

结果如下:

file

这一回合是GraalVM 映像赢了!

下面是一些测试的响应时间图:

file

file

file

在这个测试中,Java变体的表现要好得多,并且在没有使用Java日志记录的情况下,它的性能大大超过了Go。Java似乎更能使用硬件提供的多核和执行线程(与Go相比)。

这一轮的最佳表现来自GraalVM native image,平均响应时间为0.25毫秒,每秒事务数为82426个,而Go的最佳结果为1.59毫秒和39227个tps,然而这是以多占用两个数量级的内存为代价的!

GraalVM映像比在jvm上运行的同一应用程序快大约30–40%!

第三回合

这次,比赛在Kubernetes集群中运行这些应用程序,这是一个更自然的微服务运行时环境。

这次使用了一个Kubernetes 1.16.8集群,它有三个工作节点,每个节点有两个内核(每个内核有两个执行线程)、14GB的RAM和oraclelinux7.8。

应用程序访问是通过Traefik入口控制器进行的,JMeter在Kubernetes集群外运行,用于一些测试,而对于其他测试,使用ClusterIP并在集群中运行JMeter。

与前面的测试一样,我们使用了100个线程,每个线程使用了10,000个循环,以及10秒的加速时间。

下面是各种不同容器的大小:

  • Go 11.6MB 11.6 MB
  • Java/Helidon 1.41GB 1.41 GB
  • Java/Helidon JLinked 150MB 150mb
  • Native image 25.2MB 25.2 MB

结果如下:

file

下面是一些测试的响应时间图:

file

在这一轮中,我们观察到 Go 有时更快,GraalVM 映像有时更快,但这两者之间的差别很小(通常小于5%)。

Java似乎比Go更善于使用所有可用的内核/线程—我们在Java测试中看到了更好的CPU利用率。Java性能在拥有更多内核和内存的机器上更好,Go性能在较小/功能较弱的机器上更好。在一台“生产规模”的机器上,Java很容易就和Go一样快,或者更快

最后

接下来会做更多的测试比赛,来看一看究竟谁更好!有兴趣的你也可以自己试一试,记得告诉我们结果哦!

本文参考:medium.com/helidon/can…

欢迎关注我的公众号:程序猿DD,获得独家整理的学习资源、日常干货及福利赠送。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…736737738…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%