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

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


  • 首页

  • 归档

  • 搜索

并发编程的扩展-Future&CompletableFutu

发表于 2021-08-03

Future模式是多线程并发编程的扩展,这种方式支持返回结果,使用get()方法阻塞当前线程。它的核心思想是异步调用,当我们执行某个函数时,它可能很慢,但是我们又不着急要结果。因此,我们可以让它立即返回,让它异步去执行这个请求,当我们需要结果时,阻塞调用线程获取结果。

一个Future<V>接口表示一个未来可能会返回的结果,它定义的方法有:

  • get():获取结果(可能会等待)
  • get(long timeout, TimeUnit unit):获取结果,但只等待指定的时间;
  • cancel(boolean mayInterruptIfRunning):取消当前任务;
  • isDone():判断任务是否已完成。

商品查询

一个简单的生产实践例子,在维护促销活动时需要查询商品信息(包括商品基本信息、商品价格、商品库存、商品图片、商品销售状态等)。这些信息分布在不同的业务中心,由不同的系统提供服务。假设一个接口需要50ms,那么一个商品查询下来就需要200ms-300ms,这对于我们来说时不满意的。如果使用Future改造则需要的就是最长耗时服务的接口,也就是50ms左右。

Future商品查询.png

当然这里并不能解决的是一个接口服务突然很慢的问题,如果要解决这个问题,需要辅助其他组件(流控,降级等)。
伪代码如下:

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
java复制代码public class FutureTest {
static class T1Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T1:查询商品基本信息...");
TimeUnit.MILLISECONDS.sleep(50);
return "商品基本信息查询成功";
}
}

static class T2Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T2:查询商品价格...");
TimeUnit.MILLISECONDS.sleep(50);
return "商品价格查询成功";
}
}

static class T3Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T3:查询商品库存...");
TimeUnit.MILLISECONDS.sleep(50);
return "商品库存查询成功";
}
}

static class T4Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T4:查询商品图片...");
TimeUnit.MILLISECONDS.sleep(50);
return "商品图片查询成功";
}
}

static class T5Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T5:查询商品销售状态...");
TimeUnit.MILLISECONDS.sleep(50);
return "商品销售状态查询成功";
}
}

public static void main(String[] args) throws InterruptedException {
FutureTask<String> ft1 = new FutureTask<>(new T1Task());
FutureTask<String> ft2 = new FutureTask<>(new T2Task());
FutureTask<String> ft3 = new FutureTask<>(new T3Task());
FutureTask<String> ft4 = new FutureTask<>(new T4Task());
FutureTask<String> ft5 = new FutureTask<>(new T5Task());
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(ft1);
executorService.submit(ft2);
executorService.submit(ft3);
executorService.submit(ft4);
executorService.submit(ft5);
// 创建阻塞队列
BlockingQueue<String> bq = new LinkedBlockingQueue<>();
System.out.println(System.currentTimeMillis());
executorService.execute(() -> {
try {
bq.put(ft1.get());
bq.put(ft2.get());
bq.put(ft3.get());
bq.put(ft4.get());
bq.put(ft4.get());
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println(System.currentTimeMillis());
for (int i = 0; i < 5; i++) {
System.out.println(bq.take());
}
executorService.shutdown();
}
}

工作原理

Future.png

增强的Future:CompletableFuture

CompletableFuture时Java8新增的一个超大型工具类。它不仅实现了Future接口,还实现了CompletionStage接口,该接口总共拥有40多种方法(为了函数式编程中的流程调用准备的)。

使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方法都不是很好,因为主线程也会被迫等待。

CompletableFuture针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
简单示例:

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
java复制代码public class CompletableFutureDemo {
public static void main(String[] args) throws Exception {
// 创建异步执行任务:
CompletableFuture<Double> cf = CompletableFuture.supplyAsync(
CompletableFutureDemo::fetchPrice);
// 如果执行成功:
cf.thenAccept((result) -> System.out.println("price: " + result));
// 如果执行异常:
cf.exceptionally((e) -> {
e.printStackTrace();
return null;
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(200);
}

static Double fetchPrice() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
if (Math.random() < 0.3) {
throw new RuntimeException("fetch price failed!");
}
return 5 + Math.random() * 20;
}
}

创建一个CompletableFuture是通过CompletableFuture.supplyAsync()实现的,它需要一个实现了Supplier接口的对象:

1
2
3
java复制代码public interface Supplier<T> {
T get();
}

这里我们用lambda语法简化了一下,直接传入CompletableFutureDemo::fetchPrice,因为CompletableFutureDemo.fetchPrice()静态方法的签名符合Supplier接口的定义(除了方法名外)。

紧接着,CompletableFuture已经被提交给默认的线程池执行了,我们需要定义的是CompletableFuture完成时和异常时需要回调的实例。完成时,CompletableFuture会调用Consumer对象:

1
2
3
java复制代码public interface Consumer<T> {
void accept(T t);
}

异常时,CompletableFuture会调用:

1
2
3
java复制代码public interface Function<T, R> {
R apply(T t);
}

可见CompletableFuture的优点是:

  • 异步任务结束时,会自动回调某个对象的方法;
  • 异步任务出错时,会自动回调某个对象的方法;
  • 主线程设置好回调后,不再关心异步任务的执行。

除此之外,CompletableFuture还允许将多个CompletableFuture进行组合。

CompletionStage接口:

描述and汇聚关系:

  1. thenCombine:任务合并,有返回值
  2. thenAccepetBoth:两个任务执行完成后,将结果交给thenAccepetBoth消耗,无返回值。
  3. runAfterBoth:两个任务都执行完成后,执行下一步操作(Runnable)。

描述or汇聚关系

  1. applyToEither:两个任务谁执行的快,就使用那一个结果,有返回值。
  2. acceptEither: 两个任务谁执行的快,就消耗那一个结果,无返回值。
  3. runAfterEither: 任意一个任务执行完成,进行下一步操作(Runnable)。

CompletableFuture类自己也提供了anyOf()和allOf()用于支持多个CompletableFuture并行执行。

重写商品查询

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
java复制代码public class CompletableFutureTest {

public static void main(String[] args) {


List<String> queryList = Lists.newArrayList("商品基本信息", "商品价格", "商品库存", "商品图片", "商品销售状态");

List<CompletableFuture<String>> futureList = queryList.stream()
.map(v -> CompletableFuture.supplyAsync(() -> doQuery(v)))
.collect(Collectors.toList());

CompletableFuture<Void> allCompletableFuture = CompletableFuture
.allOf(futureList.toArray(new CompletableFuture[0]));

List<String> resultList = allCompletableFuture.thenApply(e ->
futureList.stream().map(CompletableFuture::join).collect(Collectors.toList())).join();
System.out.println(resultList);

}

private static String doQuery(String type) {
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 省略代码了
return type + "查询成功";
}
}

CompletableFuture还有很多花样,毕竟有那么多接口方法,留待读者们自己去尝试吧。感谢阅读!

本文转载自: 掘金

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

Springboot 配置文件、隐私数据脱敏的最佳实践(原理

发表于 2021-08-03

大家好!我是小富~

这几天公司在排查内部数据账号泄漏,原因是发现某些实习生小可爱居然连带着账号、密码将源码私传到GitHub上,导致核心数据外漏,孩子还是没挨过社会毒打,这种事的后果可大可小。

说起这个我是比较有感触的,之前我TM被删库的经历,到现在想起来心里还难受,我也是把数据库账号明文密码误提交到GitHub,然后被哪个大宝贝给我测试库删了,后边我长记性了把配置文件内容都加密了,数据安全问题真的不容小觑,不管工作汇还是生活,敏感数据一定要做脱敏处理。

如果对脱敏概念不熟悉,可以看一下我之前写过的一篇大厂也在用的6种数据脱敏方案,里边对脱敏做了简单的描述,接下来分享工作中两个比较常见的脱敏场景。

配置脱敏

实现配置的脱敏我使用了Java的一个加解密工具Jasypt,它提供了单密钥对称加密和非对称加密两种脱敏方式。

单密钥对称加密:一个密钥加盐,可以同时用作内容的加密和解密依据;

非对称加密:使用公钥和私钥两个密钥,才可以对内容加密和解密;

以上两种加密方式使用都非常简单,咱们以springboot集成单密钥对称加密方式做示例。

首先引入jasypt-spring-boot-starter jar

1
2
3
4
5
6
xml复制代码 <!--配置文件加密-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>

配置文件加入秘钥配置项jasypt.encryptor.password,并将需要脱敏的value值替换成预先经过加密的内容ENC(mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l)。

这个格式我们是可以随意定义的,比如想要abc[mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l]格式,只要配置前缀和后缀即可。

1
2
3
4
5
java复制代码jasypt:
encryptor:
property:
prefix: "abc["
suffix: "]"

ENC(XXX)格式主要为了便于识别该值是否需要解密,如不按照该格式配置,在加载配置项的时候jasypt将保持原值,不进行解密。

1
2
3
4
5
6
7
8
9
10
java复制代码spring:
datasource:
url: jdbc:mysql://1.2.3.4:3306/xiaofu?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: xiaofu
password: ENC(mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l)

# 秘钥
jasypt:
encryptor:
password: 程序员内点事(然而不支持中文)

秘钥是个安全性要求比较高的属性,所以一般不建议直接放在项目内,可以通过启动时-D参数注入,或者放在配置中心,避免泄露。

1
java复制代码java -jar -Djasypt.encryptor.password=1123  springboot-jasypt-2.3.3.RELEASE.jar

预先生成的加密值,可以通过代码内调用API生成

1
2
3
4
5
6
7
java复制代码@Autowired
private StringEncryptor stringEncryptor;

public void encrypt(String content) {
String encryptStr = stringEncryptor.encrypt(content);
System.out.println("加密后的内容:" + encryptStr);
}

或者通过如下Java命令生成,几个参数D:\maven_lib\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar为jasypt核心jar包,input待加密文本,password秘钥,algorithm为使用的加密算法。

1
java复制代码java -cp  D:\maven_lib\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="root" password=xiaofu  algorithm=PBEWithMD5AndDES

一顿操作后如果还能正常启动,说明配置文件脱敏就没问题了。

敏感字段脱敏

生产环境用户的隐私数据,比如手机号、身份证或者一些账号配置等信息,入库时都要进行不落地脱敏,也就是在进入我们系统时就要实时的脱敏处理。

用户数据进入系统,脱敏处理后持久化到数据库,用户查询数据时还要进行反向解密。这种场景一般需要全局处理,那么用AOP切面来实现在适合不过了。

首先自定义两个注解@EncryptField、@EncryptMethod分别用在字段属性和方法上,实现思路很简单,只要方法上应用到@EncryptMethod注解,则检查入参字段是否标注@EncryptField注解,有则将对应字段内容加密。

1
2
3
4
5
6
7
java复制代码@Documented
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {

String[] value() default "";
}
1
2
3
4
5
6
7
java复制代码@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptMethod {

String type() default ENCRYPT;
}

切面的实现也比较简单,对入参加密,返回结果解密。为了方便阅读这里就只贴出部分代码,完整案例Github地址:github.com/chengxy-nds…

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
java复制代码@Slf4j
@Aspect
@Component
public class EncryptHandler {

@Autowired
private StringEncryptor stringEncryptor;

@Pointcut("@annotation(com.xiaofu.annotation.EncryptMethod)")
public void pointCut() {
}

@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) {
/**
* 加密
*/
encrypt(joinPoint);
/**
* 解密
*/
Object decrypt = decrypt(joinPoint);
return decrypt;
}

public void encrypt(ProceedingJoinPoint joinPoint) {

try {
Object[] objects = joinPoint.getArgs();
if (objects.length != 0) {
for (Object o : objects) {
if (o instanceof String) {
encryptValue(o);
} else {
handler(o, ENCRYPT);
}
//TODO 其余类型自己看实际情况加
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

public Object decrypt(ProceedingJoinPoint joinPoint) {
Object result = null;
try {
Object obj = joinPoint.proceed();
if (obj != null) {
if (obj instanceof String) {
decryptValue(obj);
} else {
result = handler(obj, DECRYPT);
}
//TODO 其余类型自己看实际情况加
}
} catch (Throwable e) {
e.printStackTrace();
}
return result;
}
。。。
}

紧接着测试一下切面注解的效果,我们对字段mobile、address加上注解@EncryptField做脱敏处理。

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
java复制代码@EncryptMethod
@PostMapping(value = "test")
@ResponseBody
public Object testEncrypt(@RequestBody UserVo user, @EncryptField String name) {

return insertUser(user, name);
}

private UserVo insertUser(UserVo user, String name) {
System.out.println("加密后的数据:user" + JSON.toJSONString(user));
return user;
}

@Data
public class UserVo implements Serializable {

private Long userId;

@EncryptField
private String mobile;

@EncryptField
private String address;

private String age;
}

请求这个接口,看到参数被成功加密,而返回给用户的数据依然是脱敏前的数据,符合我们的预期,那到这简单的脱敏实现就完事了。

知其然知其所以然

Jasypt工具虽然简单好用,但作为程序员我们不能仅满足于熟练使用,底层实现原理还是有必要了解下的,这对后续调试bug、二次开发扩展功能很重要。

个人认为Jasypt配置文件脱敏的原理很简单,无非就是在具体使用配置信息之前,先拦截获取配置的操作,将对应的加密配置解密后再使用。

具体是不是如此我们简单看下源码的实现,既然是以springboot方式集成,那么就先从jasypt-spring-boot-starter源码开始入手。

starter代码很少,主要的工作就是通过SPI机制注册服务和@Import注解来注入需前置处理的类JasyptSpringBootAutoConfiguration。

在前置加载类EnableEncryptablePropertiesConfiguration中注册了一个核心处理类EnableEncryptablePropertiesBeanFactoryPostProcessor。

它的构造器有两个参数,ConfigurableEnvironment用来获取所有配属信息,EncryptablePropertySourceConverter对配置信息做解析处理。

顺藤摸瓜发现具体负责解密的处理类EncryptablePropertySourceWrapper,它通过对Spring属性管理类PropertySource<T>做拓展,重写了getProperty(String name)方法,在获取配置时,凡是指定格式如ENC(x) 包裹的值全部解密处理。

既然知道了原理那么后续我们二次开发,比如:切换加密算法或者实现自己的脱敏工具就容易的多了。

案例Github地址:github.com/chengxy-nds…

PBE算法

再来聊一下Jasypt中用的加密算法,其实它是在JDK的JCE.jar包基础上做了封装,本质上还是用的JDK提供的算法,默认使用的是PBE算法PBEWITHMD5ANDDES,看到这个算法命名很有意思,段个句看看,PBE、WITH、MD5、AND、DES 好像有点故事,继续看。

PBE算法(Password Based Encryption,基于口令(密码)的加密)是一种基于口令的加密算法,其特点在于口令是由用户自己掌握,在加上随机数多重加密等方法保证数据的安全性。

PBE算法本质上并没有真正构建新的加密、解密算法,而是对我们已知的算法做了包装。比如:常用的消息摘要算法MD5和SHA算法,对称加密算法DES、RC2等,而PBE算法就是将这些算法进行合理组合,这也呼应上前边算法的名字。

既然PBE算法使用我们较为常用的对称加密算法,那就会涉及密钥的问题。但它本身又没有钥的概念,只有口令密码,密钥则是口令经过加密算法计算得来的。

口令本身并不会很长,所以不能用来替代密钥,只用口令很容易通过穷举攻击方式破译,这时候就得加点盐了。

盐通常会是一些随机信息,比如随机数、时间戳,将盐附加在口令上,通过算法计算加大破译的难度。

源码里的猫腻

简单了解PBE算法,回过头看看Jasypt源码是如何实现加解密的。

在加密的时候首先实例化秘钥工厂SecretKeyFactory,生成八位盐值,默认使用的jasypt.encryptor.RandomSaltGenerator生成器。

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
java复制代码public byte[] encrypt(byte[] message) {
// 根据指定算法,初始化秘钥工厂
final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm1);
// 盐值生成器,只选八位
byte[] salt = saltGenerator.generateSalt(8);
//
final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iterations);
// 盐值、口令生成秘钥
SecretKey key = factory.generateSecret(keySpec);

// 构建加密器
final Cipher cipherEncrypt = Cipher.getInstance(algorithm1);
cipherEncrypt.init(Cipher.ENCRYPT_MODE, key);
// 密文头部(盐值)
byte[] params = cipherEncrypt.getParameters().getEncoded();

// 调用底层实现加密
byte[] encryptedMessage = cipherEncrypt.doFinal(message);

// 组装最终密文内容并分配内存(盐值+密文)
return ByteBuffer
.allocate(1 + params.length + encryptedMessage.length)
.put((byte) params.length)
.put(params)
.put(encryptedMessage)
.array();
}

由于默认使用的是随机盐值生成器,导致相同内容每次加密后的内容都是不同的。

那么解密时该怎么对应上呢?

看上边的源码发现,最终的加密文本是由两部分组成的,params消息头里边包含口令和随机生成的盐值,encryptedMessage密文。

加密

而在解密时会根据密文encryptedMessage的内容拆解出params内容解析出盐值和口令,在调用JDK底层算法解密出实际内容。

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
java复制代码@Override
@SneakyThrows
public byte[] decrypt(byte[] encryptedMessage) {
// 获取密文头部内容
int paramsLength = Byte.toUnsignedInt(encryptedMessage[0]);
// 获取密文内容
int messageLength = encryptedMessage.length - paramsLength - 1;
byte[] params = new byte[paramsLength];
byte[] message = new byte[messageLength];
System.arraycopy(encryptedMessage, 1, params, 0, paramsLength);
System.arraycopy(encryptedMessage, paramsLength + 1, message, 0, messageLength);

// 初始化秘钥工厂
final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm1);
final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKey key = factory.generateSecret(keySpec);

// 构建头部盐值口令参数
AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(algorithm1);
algorithmParameters.init(params);

// 构建加密器,调用底层算法
final Cipher cipherDecrypt = Cipher.getInstance(algorithm1);
cipherDecrypt.init(
Cipher.DECRYPT_MODE,
key,
algorithmParameters
);
return cipherDecrypt.doFinal(message);
}

解密

我是小富,下期见~

整理了几百本各类技术电子书,有需要的同学自取。技术群快满了,想进的同学可以加我好友,和大佬们一起吹吹技术。

电子书地址

个人公众号: 程序员内点事,欢迎交流

本文转载自: 掘金

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

再议DDD分层 8月更文挑战

发表于 2021-08-03

之前整理过《DDD分层》 以及《分层架构》

最近看网友讨论,整理一些有亮点的地方

现在分层架构+整洁架构似乎是个万金油组合了

之前DDD的标准分层结构:

右边传统分层,左边经过DIP改进型,两者有什么区别呢?

眼尖的人可以看出来,两者确实差了不少

线条1:application到infrastructure被反转了

线条2:这条线没有了,在MVC里面这线是常见的,applicaton与domain没分开,但DDD中这条线是不推荐的,就算在松散分层架构中也一般不使用,除非简单的CRUD项目

线条3:也被反转了,这其实类似CQRS中的Q部分


以上来源于群友的讨论,真的是世上无难事,只怕有心人;这点区别真没留意过

这图来源于阿里大牛殷浩之手,《阿里DDD四弹》中进行过总结,DTOAssembler放在了application层,有些不太合理

在《分层架构》中thrift的TService,为了不与controller重复,所以需要一个application service,此时thrift与controller可以有相同的业务请求

也就是说controller对外有多种输入,但对应用层来说都是同一个user case,如果放在应用层内转化,是不是应该层为了同一个use case要爆露多过方法

适配层做三件事:

  1. 协议适配(如采用controller,通过 @RequestMapping 注解和 JSON 序列化)
  2. 参数规则验证(如,不能为空、手机是数字并且11位、邮箱要有@之类简单验证)
  3. 为调用下层(应用层)转化输入(assembler)

如果说分4层的话:

  1. controller (assembler、转化)
  2. appliction
  3. domain
  4. repository(convertor、转化)

应用层是真正的业务入口,是很披的一层:

  1. 用来协调领域操作 这里一般看系统架构不一样会有所不同,主要再分为同步调用的方式,和步骤事件的方式。
  2. 关注点分离的操作(如日志、通知等)

application service编排业务,domain service编排领域

本文转载自: 掘金

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

⚡效率工具(第一期)⚡ - 推荐一款对象映射神器「MapSt

发表于 2021-08-03

前言

工作中常常出现的一种情况是,我们需要把Entity/PO/DTO/VO/QueryParam之间做转换,解决这类问题的工具有很多,如Orika、BeanUtils、Hutool工具包,为何对MapStrucet情有独钟,用来单独推荐呢?

简介

MapSturct 是一个生成类型安全, 高性能且无依赖的 JavaBean 映射代码的注解处理器

怎么理解呢,对于BeanUtils来说,映射主要是靠反射来实现,当有大量的拷贝时,意味着大量的使用了反射,效率相对低下,就连《阿里巴巴开发手册》中也明确提到,不准使用BeanUtils

众所周知,效率最快的当然是手写的get()、set(),当然开发效率也是最慢的,而MapStruct通过编译器编译生成常规的方法,我们通过写接口和注解就可以手动帮我们生成get()、set()代码,效率不知提高了多少倍

具体性能测试已经有人做过了,可以参考这篇文章:5种常见Bean映射工具的性能比对

示例

首先引入MapStruct的依赖

Maven:

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>

在Plugins加上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

现有两个实体类

1
2
3
4
5
6
7
java复制代码@Data
public class Student {
String name;
Integer age;
String idCard;
Date birthDay;
}
1
2
3
4
5
6
7
java复制代码@Data
public class StudentVO {
String name;
String birthDay;
String idCard;
Integer studentAge;
}

场景1:单个对象之间的映射 / 批量映射

按照需求,我们要把Student对象转换为StudentVO对象,其中student.age 要映射到studentVO.studentAge中,而 student.idCard在这次查询中无需显示,student.birthDay格式化成字符串再传入studentVO.bidthDay中

我们只需要创建一个接口:

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Component
@Mapper(componentModel = "spring")
public interface StudentConverter {

@Mapping(target = "studentAge", source = "age")
@Mapping(target = "idCard", ignore = true)
@Mapping(target = "birthDay", dateFormat = "yyyy-MM-dd HH:mm:ss")
StudentVO studentToStudentVO(Student student);

List<StudentVO> studentToStudentVO(List<Student> students);
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@RestController
public class TestController {

@Autowired
private StudentConverter studentConverter;

@GetMapping("/test")
public void beanConvertTest() {
Student student = new Student();
student.setName("name");
student.setAge(18);
student.setIdCard("123456");
student.setBirthDay(new Date());

StudentVO studentVO = studentConverter.studentToStudentVO(student);
System.out.println(studentVO);

List<Student> students = Collections.singletonList(student);
List<StudentVO> studentVOS = studentConverter.studentToStudentVO(students);
System.out.println(studentVOS);
}
}

输出结果:

可以看到出生日期被成功被格式化成我们想要的字符串,idCard我们不需要于是没有被映射

场景2:多个对象映射为一个

有时候会出现要将多个对象映射为一个对象的情况,而多个对象之间可能有重复的字段,这个根据@Mapping注解就能灵活解决

现在新增一个类Address.class,其中name字段和student.name是重复字段

1
2
3
4
5
java复制代码@Data
public class Address {
String name;
String address;
}

需求是在场景1的基础上,将address字段加到StudentVO.class中,但是name字段得用Student.class 的。

1
2
3
4
5
6
7
8
9
java复制代码@Data
public class StudentVO {
String name;
String birthDay;
String idCard;
Integer studentAge;
//新增字段
String address;
}

我们可以通过source参数来设置需要映射的字段来自于哪个实例,接口方法如下:

1
2
3
4
5
java复制代码@Mapping(target = "studentAge", source = "student.age")
@Mapping(target = "idCard", ignore = true)
@Mapping(target = "birthDay", source = "student.birthDay", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(target = "name", source = "student.name")
StudentVO studentAndAddressToVO(Student student, Address address);

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@GetMapping("/test")
public void beanConvertTest() {
Student student = new Student();
student.setName("name");
student.setAge(18);
student.setIdCard("123456");
student.setBirthDay(new Date());

Address address = new Address();
address.setName("addressName");
address.setAddress("address");

StudentVO studentVO = studentConverter.studentAndAddressToVO(student, address);
System.out.println(studentVO);
}

输出结果:

对于上面的例子,需要注意的是,多个对象映射为一个对象时,只有重复的字段,或者字段名不一样的字段进行映射,才需要用注解告诉MapStruct,到底要使用哪个来源的字段

更多更复杂的用法可以参考官方示例仓库,非常齐全:mapstruct/mapstruct-examples: Examples for using MapStruct (github.com)

其他注意事项

  • 如果项目中也同时使用到了 Lombok,一定要注意 Lombok的版本要等于或者高于1.18.10,否则会有编译不通过的情况发生
  • 如果接口的存放包名为mapper,可能与Mybatis冲突会导致项目起不来
  • 当两个对象属性不一致时,比如Student对象中某个字段不存在于StudentVO当中时,在编译时会有警告提示,可以在@Mapping中配置 ignore = true,当字段较多时,可以直接在@Mapper中设置unmappedTargetPolicy属性或者unmappedSourcePolicy属性为 ReportingPolicy.IGNORE即可

总结:应该如何选择对象映射工具

  1. 若是字段少,写起来也不麻烦,就没必要用框架了,手写get()/set() 就好了,技术不应该为用而用,应该为了解决对应的问题而用
  2. 若是字段多,转换不频繁,为省事就用BeanUtils吧,稍微复杂点的场景也可以使用 Hutool工具包的BeanUtils,提供的拷贝选项要多一些
  3. 字段多又转换频繁,从性能方面考虑,还是从高性能的工具中选一个用吧,比如本文推荐的MapStruct

Reference

常用开发库 - MapStruct工具库详解 | Java 全栈知识体系 (pdai.tech)

丢弃掉那些BeanUtils工具类吧,MapStruct真香!!! (juejin.cn)

本文转载自: 掘金

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

netty系列之 netty中的ByteBuf详解| 8月更

发表于 2021-08-03

简介

netty中用于进行信息承载和交流的类叫做ByteBuf,从名字可以看出这是Byte的缓存区,那么ByteBuf都有哪些特性呢?一起来看看。

ByteBuf详解

netty提供了一个io.netty.buffer的包,该包里面定义了各种类型的ByteBuf和其衍生的类型。

netty Buffer的基础是ByteBuf类,这是一个抽象类,其他的Buffer类基本上都是由该类衍生而得的,这个类也定义了netty整体Buffer的基调。

先来看下ByteBuf的定义:

1
kotlin复制代码public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {

ByteBuf实现了两个接口,分别是ReferenceCounted和Comparable。Comparable是JDK自带的接口,表示该类之间是可以进行比较的。而ReferenceCounted表示的是对象的引用统计。当一个ReferenceCounted被实例化之后,其引用count=1,每次调用retain() 方法,就会增加count,调用release() 方法又会减少count。当count减为0之后,对象将会被释放,如果试图访问被释放过后的对象,则会报访问异常。

如果一个对象实现了ReferenceCounted,并且这个对象里面包含的其他对象也实现了ReferenceCounted,那么当容器对象的count=0的时候,其内部的其他对象也会被调用release()方法进行释放。

综上,ByteBuf是一个可以比较的,可以计算引用次数的对象。他提供了序列或者随机的byte访问机制。

注意的是,虽然JDK中有自带的ByteBuffer类,但是netty中的 ByteBuf 算是对Byte Buffer的重新实现。他们没有关联关系。

创建一个Buff

ByteBuf是一个抽象类,并不能直接用来实例化,虽然可以使用ByteBuf的子类进行实例化操作,但是netty并不推荐。netty推荐使用io.netty.buffer.Unpooled来进行Buff的创建工作。Unpooled是一个工具类,可以为ByteBuf分配空间、拷贝或者封装操作。

下面是创建几个不同ByteBuf的例子:

1
2
3
4
5
6
ini复制代码   import static io.netty.buffer.Unpooled.*;

ByteBuf heapBuffer = buffer(128);
ByteBuf directBuffer = directBuffer(256);
ByteBuf wrappedBuffer = wrappedBuffer(new byte[128], new byte[256]);
ByteBuf copiedBuffer = copiedBuffer(ByteBuffer.allocate(128));

上面我们看到了4种不同的buff构建方式,普通的buff、directBuffer、wrappedBuffer和copiedBuffer。

普通的buff是固定大小的堆buff,而directBuffer是固定大小的direct buff。direct buff使用的是堆外内存,省去了数据到内核的拷贝,因此效率比普通的buff要高。

wrappedBuffer是对现有的byte arrays或者byte buffers的封装,可以看做是一个视图,当底层的数据发生变化的时候,Wrapped buffer中的数据也会发生变化。

Copied buffer是对现有的byte arrays、byte buffers 或者 string的深拷贝,所以它和wrappedBuffer是不同的,Copied buffer和原数据之间并不共享数据。

随机访问Buff

熟悉集合的朋友应该都知道,要想随机访问某个集合,一定是通过index来访问的,ByteBuf也一样,可以通过capacity或得其容量,然后通过getByte方法随机访问其中的byte,如下所示:

1
2
3
4
5
6
ini复制代码        //随机访问
ByteBuf buffer = heapBuffer;
for (int i = 0; i < buffer.capacity(); i ++) {
byte b = buffer.getByte(i);
System.out.println((char) b);
}

序列读写

读写要比访问复杂一点,ByteBuf 提供了两个index用来定位读和写的位置,分别是readerIndex 和 writerIndex ,两个index分别控制读和写的位置。

下图显示的一个buffer被分成了三部分,分别是可废弃的bytes、可读的bytes和可写的bytes。

1
2
3
4
5
6
python复制代码    +-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
| | (CONTENT) | |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity

上图还表明了readerIndex、writerIndex和capacity的大小关系。

其中readable bytes是真正的内容,可以通过调用read* 或者skip* 的方法来进行访问或者跳过,调用这些方法的时候,readerIndex会同步增加,如果超出了readable bytes的范围,则会抛出IndexOutOfBoundsException。默认情况下readerIndex=0。

下面是一个遍历readable bytes的例子:

1
2
3
4
csharp复制代码        //遍历readable bytes
while (directBuffer.isReadable()) {
System.out.println(directBuffer.readByte());
}

首先通过判断是否是readable来决定是否调用readByte方法。

Writable bytes是一个未确定的区域,等待被填充。可以通过调用write*方法对其操作,同时writerIndex 会同步更新,同样的,如果空间不够的话,也会抛出IndexOutOfBoundsException。默认情况下 新分配的writerIndex =0 ,而wrapped 或者copied buffer的writerIndex=buf的capacity。

下面是一个使用writable Byte的例子:

1
2
3
4
vbscript复制代码        //写入writable bytes
while (wrappedBuffer.maxWritableBytes() >= 4) {
wrappedBuffer.writeInt(new Random().nextInt());
}

Discardable bytes是已经被读取过的bytes,初始情况下它的值=0,每当readerIndex右移的时候,Discardable bytes的空间就会增加。如果想要完全删除或重置Discardable bytes,则可以调用discardReadBytes()方法,该方法会将Discardable bytes空间删除,将多余的空间放到writable bytes中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码调用 discardReadBytes() 之前:

+-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity


调用 discardReadBytes()之后:

+------------------+--------------------------------------+
| readable bytes | writable bytes (got more space) |
+------------------+--------------------------------------+
| | |

readerIndex (0) <= writerIndex (decreased) <= capacity

注意,虽然writable bytes变多了,但是其内容是不可控的,并不能保证里面的内容是空的或者不变。

调用clear()方法会将readerIndex 和 writerIndex 清零,注意clear方法只会设置readerIndex 和 writerIndex 的值,并不会清空content,看下面的示意图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码调用 clear()之前:

+-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity


调用 clear()之后:

+---------------------------------------------------------+
| writable bytes (got more space) |
+---------------------------------------------------------+
| |
0 = readerIndex = writerIndex <= capacity

搜索

ByteBuf提供了单个byte的搜索功能,如 indexOf(int, int, byte) 和 bytesBefore(int, int, byte)两个方法。

如果是要对ByteBuf遍历进行搜索处理的话,可以使用 forEachByte(int, int, ByteProcessor),这个方法接收一个ByteProcessor用于进行复杂的处理。

其他衍生buffer方法

ByteBuf还提供了很多方法用来创建衍生的buffer,如下所示:

1
2
3
4
5
6
7
8
scss复制代码duplicate()
slice()
slice(int, int)
readSlice(int)
retainedDuplicate()
retainedSlice()
retainedSlice(int, int)
readRetainedSlice(int)

要注意的是,这些buf是建立在现有buf基础上的衍生品,他们的底层内容是一样的,只有readerIndex, writerIndex 和做标记的index不一样。所以他们和原buf是有共享数据的。如果你希望的是新建一个全新的buffer,那么可以使用copy()方法或者前面提到的Unpooled.copiedBuffer。

在前面小节中,我们讲到ByteBuf是一个ReferenceCounted,这个特征在衍生buf中就用到了。我们知道调用retain() 方法的时候,引用count会增加,但是对于 duplicate(), slice(), slice(int, int) 和 readSlice(int) 这些方法来说,虽然他们也是引用,但是没有调用retain()方法,这样原始数据会在任意一个Buf调用release()方法之后被回收。

如果不想有上面的副作用,那么可以将方法替换成retainedDuplicate(), retainedSlice(), retainedSlice(int, int) 和 readRetainedSlice(int) ,这些方法会调用retain()方法以增加一个引用。

和现有JDK类型的转换

之前提到了ByteBuf 是对ByteBuffer的重写,他们是不同的实现。虽然这两个不同,但是不妨碍将ByteBuf转换ByteBuffer。

当然,最简单的转换是把ByteBuf转换成byte数组byte[]。要想转换成byte数组,可以先调用hasArray() 进行判断,然后再调用array()方法进行转换。

同样的ByteBuf还可以转换成为ByteBuffer ,可以先调用 nioBufferCount()判断能够转换成为 ByteBuffers的个数,再调用nioBuffer() 进行转换。

返回的ByteBuffer是对现有buf的共享或者复制,对返回之后buffer的position和limit修改不会影响到原buf。

最后,使用toString(Charset) 方法可以将ByteBuf转换成为String。

总结

ByteBuf是netty的底层基础,是传输数据的承载对象,深入理解ByteBuf就可以搞懂netty的设计思想,非常不错。

本文的例子可以参考:learn-netty4

本文已收录于 <www.flydean.com>

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

RestFul风格和控制器讲解 RestFul和控制器 Re

发表于 2021-08-03

RestFul和控制器

  • 控制器复杂提供访问应用程序的行为,通常通过接口定义或注解定义两种方法实现。
  • 控制器负责解析用户的请求并将其转换为一个模型。
  • 在Spring MVC中一个控制器类可以包含多个方法
  • 在Spring MVC中,对于Controller的配置方式有很多种

1.实现Controller接口

Controller是一个接口,在org.springframework.web.servlet.mvc包下,接口中只有一个方法;

1
2
3
4
5
java复制代码//实现该接口的类获得控制器功能
public interface Controller {
//处理请求且返回一个模型与视图对象
ModelAndView handleRequest(HttpServletRequest var1, HttpServletResponse var2) throws Exception;
}

测试

1.新建一个Moudle,springmvc-04-controller 。

添加web支持 新建lib 添加jar包!

在这里插入图片描述)在这里插入图片描述

2.配置web.xml

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">

<!--1.配置DispatcherServlet-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--关联一个springmvc的配置文件:【servlet-name】-servlet.xml-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc-servlet.xml</param-value>
</init-param>
<!--启动级别-1-->
<load-on-startup>1</load-on-startup>
</servlet>

<!--/ 匹配所有的请求;(不包括.jsp)-->
<!--/* 匹配所有的请求;(包括.jsp)-->
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

3.编写一个Controller类,ControllerTest1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码package com.cy.controller;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//定义控制器
//注意点:不要导错包,实现Controller接口,重写方法;
public class ControllerTest1 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
//返回一个模型视图对象
ModelAndView mv = new ModelAndView();

mv.addObject("msg","ControllerTest1");
mv.setViewName("test");

return mv;
}
}

4.添加Spring MVC配置文件

在resource目录下添加springmvc-servlet.xml配置文件,配置的形式与Spring容器配置基本类似,为了支持基于注解的IOC,设置了自动扫描包的功能,具体配置信息如下:

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
xml复制代码<?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">

<!-- 自动扫描包,让指定包下的注解生效,由IOC容器统一管理 -->
<context:component-scan base-package="com.cy.controller"/>
<!-- 让Spring MVC不处理静态资源 .css .js .html .mp3 .mp4-->
<mvc:default-servlet-handler />
<mvc:annotation-driven />

<!-- 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
id="internalResourceViewResolver">
<!-- 前缀 -->
<property name="prefix" value="/WEB-INF/jsp/" />
<!-- 后缀 -->
<property name="suffix" value=".jsp" />
</bean>
</beans>

5.写要跳转的jsp页面

1
2
3
4
5
6
7
8
9
java复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>SpringMVC</title>
</head>
<body>
${msg}
</body>
</html>

6.去Spring配置文件中注册请求的bean;

name对应请求路径,class对应处理请求的类

1
xml复制代码    <bean name="/t1" class="com.cy.controller.ControllerTest1"/>

7.配置Tomcat运行测试,

我这里没有项目发布名配置的就是一个 / ,所以请求不用加项目名,OK!
在这里插入图片描述



2.使用注解@Controller

  • @Controller注解类型用于声明Spring类的实例是一个控制器(在讲IOC时还提到了另外3个注解);
  • Spring可以使用扫描机制来找到应用程序中所有基于注解的控制器类,为了保证Spring能找到你的控制器,需要在配置文件中声明组件扫描。
1
2
xml复制代码<!-- 自动扫描指定的包,下面所有注解类交给IOC容器管理 -->
<context:component-scan base-package="com.cy.controller"/>
  • 增加一个ControllerTest2类,使用注解实现;
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码//@Controller注解的类会自动添加到Spring上下文中
@Controller //代表这个类会被spring接管,被这个注解的类,如果返回值是String 并且具体页面可以跳转,那么就会被视图解析器解析
public class ControllerTest2{

//映射访问路径
@RequestMapping("/t2")
public String index(Model model){
//Spring MVC会自动实例化一个Model对象用于向视图中传值
model.addAttribute("msg", "ControllerTest2");
//返回视图位置
return "test";//WEB-INF/jsp/test.jsp
}
}
  • 运行tomcat测试
    在这里插入图片描述
    可以发现,我们的两个请求都可以指向一个视图,但是页面结果的结果是不一样的,从这里可以看出视图是被复用的,而控制器与视图之间是弱偶合关系。

注解方式是平时使用的最多的方式!

3.RequestMapping

@RequestMapping

  • @RequestMapping注解用于映射url到控制器类或一个特定的处理程序方法。可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。
  • 为了测试结论更加准确,我们可以加上一个项目名测试 myweb
  • 只注解在方法上面
1
2
3
4
5
6
7
java复制代码@Controller
public class ControllerTest3{
@RequestMapping("/h1")
public String test(){
return "test";
}
}
  • 访问路径:http://localhost:8080 / 项目名 / h1
  • 同时注解类与方法
1
2
3
4
5
6
7
8
java复制代码@Controller
@RequestMapping("/admin")
public class ControllerTest4 {
@RequestMapping("/h1")
public String test(){
return "test";
}
}

访问路径:http://localhost:8080 / 项目名/ admin /h1 , 需要先指定类的路径再指定方法的路径;

RestFul 风格

概念:

Restful就是一个资源定位及资源操作的风格。不是标准也不是协议,只是一种风格。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

功能

  • 资源:互联网所有的事物都可以被抽象为资源
  • 资源操作:使用POST、DELETE、PUT、GET,使用不同方法对资源进行操作。
  • 分别对应 添加、 删除、修改、查询。

传统方式操作资源 :

通过不同的参数来实现不同的效果!方法单一,post 和 get

  • http://127.0.0.1/item/queryItem.action?id=1 查询,GET
  • http://127.0.0.1/item/saveItem.action 新增,POST
  • http://127.0.0.1/item/updateItem.action 更新,POST
  • http://127.0.0.1/item/deleteItem.action?id=1 删除,GET或POST

使用RESTful操作资源 :

可以通过不同的请求方式来实现不同的效果!如下:请求地址一样,但是功能可以不同!

  • http://127.0.0.1/item/1 查询,GET
  • http://127.0.0.1/item 新增,POST
  • http://127.0.0.1/item 更新,PUT
  • http://127.0.0.1/item/1 删除,DELETE

学习测试

传统方式写法:

1.在新建一个类 RestFulController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.cy.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class RestFulController {

@RequestMapping("/add")
public String test1(int a, int b, Model model){
int res = a+b;
model.addAttribute("msg","结果为"+res);
return "test";
}
}

测试请求查看下
http://localhost:8080/add?a=1&b=2
在这里插入图片描述


RestFul风格写法

2.在Spring MVC中可以使用 @PathVariable 注解,让方法参数的值对应绑定到一个URI模板变量上。

1
2
3
4
5
6
7
8
9
10
java复制代码@Controller
public class RestFulController {

@RequestMapping("/add/{a}/{b}")
public String test1(@PathVariable int a, @PathVariable int b, Model model){
int res = a+b;
model.addAttribute("msg","结果为"+res);
return "test";
}
}

测试请求查看下
http://localhost:8080/add/1/2
在这里插入图片描述

思考:使用路径变量的好处?

  • 使路径变得更加简洁;
  • 安全,防止注入
  • 获得参数更加方便,框架会自动进行类型转换。
通过路径变量的类型可以约束访问参数,如果类型不一样,则访问不到对应的请求方法,如这里访问是的路径是/commit/1/a,则路径与方法不匹配,而不会是参数转换失败。

3.我们来修改下对应的参数类型,再次测试

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Controller
public class RestFulController {
//映射访问路径
@RequestMapping("/add/{a}/{b}")
public String test1(@PathVariable int a, @PathVariable String b, Model model){

String res = a+b;
//Spring MVC会自动实例化一个Model对象用于向视图中传值
model.addAttribute("msg", "结果:"+res );
//返回视图位置
return "test";
}
}

我们在来测试请求查看下
http://localhost:8080/add/1/2
在这里插入图片描述


使用method属性指定请求类型

用于约束请求的类型,可以收窄请求范围。指定请求谓词的类型如GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE等

我们来测试一下:

  • 增加一个方法
1
2
3
4
5
6
java复制代码    //映射访问路径,必须是POST请求
@RequestMapping(value = "/hello",method = {RequestMethod.POST})
public String test2(Model model){
model.addAttribute("msg", "hello!");
return "test";
}

另一种写法:

1
2
3
4
5
java复制代码    @GetMapping("/hello")
public String test2(Model model){
model.addAttribute("msg", "hello!");
return "test";
}
  • 我们使用浏览器地址栏进行访问默认是Get请求,会报错405:
    在这里插入图片描述
    如果将POST修改为GET则正常了;
    在这里插入图片描述

创建一个表单a.jsp

放在WEB-INF目录下

1
2
3
4
5
6
7
8
9
10
11
html复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>SpringMVC</title>
</head>
<body>
<form action="/add/1/3" method="post">
<input type="submit">
</form>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Controller
public class RestFulController {

@PostMapping("/add/{a}/{b}")
public String test1(@PathVariable int a, @PathVariable int b, Model model){
int res = a+b;
model.addAttribute("msg","结果为"+res);
return "test";
}

@GetMapping("/add/{a}/{b}")
public String test2(@PathVariable int a, @PathVariable String b, Model model){
String res = a+b;
model.addAttribute("msg","结果为"+res);
return "test";
}
}

我们来测试一下:
http://localhost:8080/add/1/1
在这里插入图片描述
我们可以发现Post请求 正常执行,然后继续测试Get请求
http://localhost:8080/a.jsp
在这里插入图片描述
我们可以发现Get请求也执行了,
在这里插入图片描述

所以我们可以通过不同的请求方式来实现不同的效果!


小结:

Spring MVC 的 @RequestMapping 注解能够处理 HTTP 请求的方法, 比如 GET, PUT, POST, DELETE 以及 PATCH。

所有的地址栏请求默认都会是 HTTP GET 类型的。

方法级别的注解变体有如下几个:组合注解

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

@GetMapping 是一个组合注解,平时使用的会比较多!

它所扮演的是 @RequestMapping(method =RequestMethod.GET) 的一个快捷方式。

本文转载自: 掘金

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

吊炸天的可视化安全框架,轻松搭建自己的认证授权平台!

发表于 2021-08-03

SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…

摘要

之前我们在学习Oauth2的时候,需要通过写代码来实现认证授权服务。最近发现一款可视化的安全框架Keycloak,只需几个命令就可以快速搭建认证授权服务,无需自行开发。原生支持SpringBoot,使用起来非常简单,推荐给大家!

简介

Keycloak是一款开源的认证授权平台,在Github上已有9.4k+Star。Keycloak功能众多,可实现用户注册、社会化登录、单点登录、双重认证 、LDAP集成等功能。

安装

使用Docker搭建Keycloak服务非常简单,两个命令就完事了,我们将采用此种方式。

  • 首先下载Keycloak的Docker镜像,注意使用jboss的镜像,官方镜像不在DockerHub中;
1
bash复制代码docker pull jboss/keycloak:14.0.0
  • 使用如下命令运行Keycloak服务:
1
2
3
4
bash复制代码docker run -p 8080:8080 --name keycloak \
-e KEYCLOAK_USER=admin \
-e KEYCLOAK_PASSWORD=admin \
-d jboss/keycloak:14.0.0
  • 运行成功后可以通过如下地址访问Keycloak服务,点击圈出来的地方可以访问管理控制台,访问地址:http://192.168.7.142:8080

控制台使用

接下来我们来体验下Keycloak的管理控制台,看看这个可视化安全框架有什么神奇的地方。

  • 首先输入我们的账号密码admin:admin进行登录;

  • 登录成功后进入管理控制台,我们可以发现Keycloak是英文界面,良心的是它还支持多国语言(包括中文),只要将Themes->Default Locale改为zh-CN即可切换为中文;

  • 修改完成后保存并刷新页面,Keycloak控制台就变成中文界面了;

  • Keycloak非常良心的给很多属性都添加了解释,而且还是中文的,基本看下解释就可以知道如何使用了;

  • 在我们开始使用Keycloak保护应用安全之前,我们得先创建一个领域(realm),领域相当于租户的概念,不同租户之间数据相互隔离,这里我们创建一个macrozheng的领域;

  • 接下来我们可以在macrozheng领域中去创建用户,创建一个macro用户;

  • 之后我们编辑用户的信息,在凭据下设置密码;

  • 创建完用户之后,就可以登录了,用户和管理员的登录地址并不相同,我们可以在客户端页面中查看到地址;

  • 访问该地址后即可登录,访问地址:http://192.168.7.142:8080/auth/realms/macrozheng/account

  • 用户登录成功后即可查看并修改个人信息。

结合Oauth2使用

OAuth 2.0是用于授权的行业标准协议,在《Spring Cloud Security:Oauth2使用入门》 一文中我们详细介绍了Oauth2的使用,当然Keycloak也是支持的,下面我们通过调用接口的方式来体验下。

两种常用的授权模式

我们再回顾下两种常用的Oauth2授权模式。

授权码模式

  • (A)客户端将用户导向认证服务器;
  • (B)用户在认证服务器进行登录并授权;
  • (C)认证服务器返回授权码给客户端;
  • (D)客户端通过授权码和跳转地址向认证服务器获取访问令牌;
  • (E)认证服务器发放访问令牌(有需要带上刷新令牌)。

密码模式

  • (A)客户端从用户获取用户名和密码;
  • (B)客户端通过用户的用户名和密码访问认证服务器;
  • (C)认证服务器返回访问令牌(有需要带上刷新令牌)。

密码模式体验

  • 首先需要在Keycloak中创建客户端mall-tiny-keycloak;

  • 然后创建一个角色mall-tiny;

  • 然后将角色分配给macro用户;

  • 一切准备就绪,在Postman中使用Oauth2的方式调用接口就可以获取到Token了,获取token的地址:http://192.168.7.142:8080/auth/realms/macrozheng/protocol/openid-connect/token

结合SpringBoot使用

接下来我们体验下使用Keycloak保护SpringBoot应用的安全。由于Keycloak原生支持SpringBoot,所以使用起来还是很简单的。

  • 由于我们的SpringBoot应用将运行在localhost:8088上面,我们需要对Keycloak的客户端的有效的重定向URI进行配置;

  • 接下来我们需要修改应用的pom.xml,集成Keycloak;
1
2
3
4
5
6
xml复制代码<!--集成Keycloak-->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>14.0.0</version>
</dependency>
  • 再修改应用的配置文件application.yml,具体属性参考注释即可,需要注意的是给路径绑定好可以访问的角色;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yaml复制代码# Keycloak相关配置
keycloak:
# 设置客户端所在领域
realm: macrozheng
# 设置Keycloak认证服务访问路径
auth-server-url: http://192.168.7.142:8080/auth
# 设置客户端ID
resource: mall-tiny-keycloak
# 设置为公开客户端,不需要秘钥即可访问
public-client: true
# 配置角色与可访问路径的对应关系
security-constraints:
- auth-roles:
- mall-tiny
security-collections:
- patterns:
- '/brand/*'
- '/swagger-ui/*'
  • 接下来访问下应用的Swagger页面,访问的时候会跳转到Keycloak的控制台去登录,访问地址:http://localhost:8088/swagger-ui/

  • 登录成功后,即可访问被保护的Swagger页面和API接口,一个很标准的Oauth2的授权码模式,流程参考授权码模式的说明即可。

总结

Keycloak是一款非常不错的可视化安全框架,让我们无需搭建认证服务即可完成认证和授权功能。原生支持SpringBoot,基本无需修改代码即可集成,不愧为现代化的安全框架!

参考资料

  • Keycloak官方文档:www.keycloak.org/getting-sta…
  • 保护SpringBoot应用安全:www.keycloak.org/docs/latest…

项目源码地址

gitee.com/macrozheng/…

本文转载自: 掘金

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

MyBatis系列(九)- MyBatis的动态SQL| 8

发表于 2021-08-03

相关文章

MyBatis系列汇总:MyBatis系列


前言

  • 动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
  • 上面这句话是MaBatis官网说的!这篇文章很重要!在工作中必不可少!
  • 首先我们先建立一些测试的库。下面的示例都是建立在此表进行测试的!
+ ![image-20210730140037206.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/b096b52bd7af661e0318970b8bacda4305487bde5190fb4a72d6e46500329c47)
  • 实体类
+ 
1
2
3
4
5
6
7
8
9
10
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor
public class Blog {
private Integer id;
private String title;
private String autor;
private Date creat_time;
private Integer reads;
}
  • 使用动态标签,可以在xml中也就是sql中写一些基本的逻辑。十分方便好用!
  • 造点数据,方便测试。
+ ![image-20210730161726094.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/c4da5bb1c6147761f4da4dfc31293c3bd6c0c12555bd6ae173b61ad373d23c54)

一、if 、where 标签

  • xml
+ 
1
2
3
4
5
6
7
8
9
10
11
java复制代码    <select id="getBlogInfo" resultType="Blog" parameterType="map">
select
*
from
myblog
where
1 = 1
<if test="title!='' and title!=null">
and title like concat(concat('%',#{title}),'%')
</if>
</select>
  • mapper
+ 
1
java复制代码List<Blog> getBlogInfo(Map<String,Object> map);
+ 在实际工作开发中,我们传入的值一般使用map来。这样会更加方便和易扩展。 + 而返回的result,我们一般使用实体类来实现,因为Swgger,前后端联调无敌方便!!强烈推荐! + 如果有小伙伴想了解Swgger的话,欢迎留言,博主会根据需要的程度来决定是否单独开一篇来详解Swgger!!!
  • Junit Test
+ 
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码    @Test
public void getMyBlog(){
SqlSession session = MybatisUtils.getSession();
MyBlogMapper mapper = session.getMapper(MyBlogMapper.class);
Map<String,Object> map = new HashMap<>();
map.put("title","Mybatis");
List<Blog> myBlogMappers = mapper.getBlogInfo(map);
for (Blog myBlogMapper : myBlogMappers) {
System.out.println(myBlogMapper);
}
session.close();
}
  • 执行结果:
+ ![image-20210730162207384.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/ad21c2d2fd466aec566e4a0fa108c6d70521755a8986c301ebc91698600b6e1d)
+ 完美查出。
  • 可能有人会问,为什么要加这个标签呢?
+ 首先,if 标签可以让我们的sql更加灵活,这个 title 的值传的话就代表带条件查询,不传的话,即是无效代码,查询所有!
+ 然后,如果在service中实现这种效果也是可以的,但是十分繁琐。所以在实际工作中,if 标签是用的最多的!
+ 最后,同学们注意到上面sql中 `1=1`,为啥要写这个,在实际开发中,我们一般都是软删除(逻辑删除),所以一般都会有个删除标识来确定这条数据是否存在!加这个纯粹是为了模拟真实开发代码!
  • 那么where标签什么时候用呢?
+ xml


    - 
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    <select id="getBlogInfo1" resultType="Blog" parameterType="map">
select
*
from
myblog
where
<if test="title!='' and title!=null">
title like concat(concat('%',#{title}),'%')
</if>
<if test="id!='' and id!=null">
and id = #{id}
</if>
</select>
- 如果我们的语句是这样子的,那么如果 title 为空,sql语句是不是相当于where后面直接加上了and? - 演示: *
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码    @Test
public void getMyBlog1(){
SqlSession session = MybatisUtils.getSession();
MyBlogMapper mapper = session.getMapper(MyBlogMapper.class);
Map<String,Object> map = new HashMap<>();
map.put("id","2");
List<Blog> myBlogMappers = mapper.getBlogInfo1(map);
for (Blog myBlogMapper : myBlogMappers) {
System.out.println(myBlogMapper);
}
session.close();
}
* 报错信息如下 * ![image-20210730163358590.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/db124eb5c6db6f1048d8bee9623298575c39c92adb56923278ff292763d755b9) *
1
2
3
4
5
6
7
java复制代码org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'and id = '2'' at line 8
### The error may exist in com/dy/dynamic/mapper/MyBlogMapper.xml
### The error may involve com.dy.dynamic.mapper.MyBlogMapper.getBlogInfo1-Inline
### The error occurred while setting parameters
### SQL: select * from myblog where and id = ?
### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'and id = '2'' at line 8
  • 使用where标签
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    <select id="getBlogInfo1" resultType="Blog" parameterType="map">
select
*
from
myblog
<where>
<if test="title!='' and title!=null">
title like concat(concat('%',#{title}),'%')
</if>
<if test="id!='' and id!=null">
and id = #{id}
</if>
</where>
</select>
+ 执行看结果 + ![image-20210730163517626.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/7bdb5fc20fa1469b8d89607a5498c3bd27108f47270a128f9d193ddc8f7a8389) + 完美解决!
  • 如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样:
+ 
1
2
java复制代码SELECT * FROM BLOG
WHERE
  • 这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:
+ 
1
2
java复制代码SELECT * FROM BLOG
WHERE AND id = 2
+ 这个查询也会失败。这个问题不能简单地用条件元素来解决。
  • where标签的作用显而易见了!
+ 当条件有and时,它可以判断该条件是否是第一个条件,是的话自动去除。
+ *where* 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,*where* 元素也会将它们去除。
  • 如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:
+ 
1
2
3
java复制代码<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
+ 这个trim 下面再讲。

二、choose、when、otherwise 标签

  • 有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。
  • if 讲完了,记不记得还有一种判断条件的东东?
+ 没错就是 switch case (JAVA中)
+ 在动态sql中就是 choose
+ 下面来玩一玩这玩意
  • xml
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码    <select id="getBlogInfoWhoose" resultType="Blog" parameterType="map">
select
*
from
myblog
<where>
<choose>
<when test="title!='' and title!=null">
title like concat(concat('%',#{title}),'%')
</when>
<when test="id!='' and id!=null">
and id = #{id}
</when>
<otherwise>
AND `reads` > 10000
</otherwise>
</choose>
</where>
</select>
  • mapper
+ 
1
java复制代码List<Blog> getBlogInfoWhoose(Map<String,Object> map);
  • Junit Test
+ 
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码    @Test
public void getMyBlog1(){
SqlSession session = MybatisUtils.getSession();
MyBlogMapper mapper = session.getMapper(MyBlogMapper.class);
Map<String,Object> map = new HashMap<>();
map.put("title","Spring");
List<Blog> myBlogMappers = mapper.getBlogInfoWhoose(map);
for (Blog myBlogMapper : myBlogMappers) {
System.out.println(myBlogMapper);
}
session.close();
}
  • 执行结果
+ ![image-20210730165601475.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/d38a88bf93adae23d1f56776a54fc6153a7f2d5eebf2e1ca784b3589402fc838)
  • 当没有满足choose中的条件时
+ Junit Test


    - 
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Test
public void getMyBlog1(){
SqlSession session = MybatisUtils.getSession();
MyBlogMapper mapper = session.getMapper(MyBlogMapper.class);
Map<String,Object> map = new HashMap<>();
// map.put("title","Spring");
List<Blog> myBlogMappers = mapper.getBlogInfoWhoose(map);
for (Blog myBlogMapper : myBlogMappers) {
System.out.println(myBlogMapper);
}
session.close();
}
+ 执行结果 - ![image-20210730165825240.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/208a61c46747481b02e46d2897b6a99c3ce24397de1aaec8a9d2c14dc70dc558) - 这样只查出来readis(阅读量)大于10000的数据
  • 总结下这几个标签
+ 类似reads这种关键字,需要加飘号 `
+ 所有条件都不满足的时候就输出 otherwise 中的内容。
+ when元素表示当 when 中的条件满足的时候就输出其中的内容,跟 JAVA 中的 switch 效果差不多的是按照条件的顺序。

三、trim、set 标签

  • 上面说的这么多,都是在讲select(查询),那么现在来讲一下 update 中的标签 set
  • 基本的update语句我这里就不再重复写了,大家只需要关注这种动态语句即可。在实际工作中开发一般以这种居多,毕竟它比较灵活嘛!

①、set 标签

  • xml
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码    <update id="updateBlogName" parameterType="map">
update myblog
<set>
<if test="title!=null and title!=''">
title = #{title},
</if>
<if test="autor!=null and autor!=''">
autor = #{autor},
</if>
<if test="reads!=null and reads!=''">
`reads` = #{reads}
</if>
</set>
where
id = #{id}
</update>
  • mapper
+ 
1
java复制代码Integer updateBlogName(Map<String,Object> map);
  • Junit Test
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    @Test
public void updateBlogName(){
SqlSession session = MybatisUtils.getSession();
MyBlogMapper mapper = session.getMapper(MyBlogMapper.class);
Map<String,Object> map = new HashMap<>();
map.put("id","3");
map.put("title","富婆让我陪她逛街");
map.put("autor","大大大大鱼");
map.put("reads","100000");
Integer num = mapper.updateBlogName(map);
System.out.println("一共更新了:"+num+"条数据");
session.commit();//更新不要忘记提交事务哦
session.close();
}
  • 执行结果
+ ![image-20210731162506916.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/f68a8083bc07ec71ee7f61443dfed55ab10b350fcde72e9285f5af33e67a83a2)
+ ![image-20210731162711474.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/7f96f033667986cd05e83ce80f38024fbd55e517a1583613e5deb01a79164fc9)
  • 大家有没有发现,我们在sql中是将set语句后面的逗号写死的?
+ ![image-20210731163403210.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/e22d2c8bdbd5cedbfe0f700091b173a4c76e87e000bf457c54c28ab73f4415b8)
+ 那么,如果我们只传了`title` 和`autor`呢?
+ 理论上是不是语句变成这样?


    - 
1
2
3
4
java复制代码update myblog set
title = #{title},
autor = #{autor},
where id = #{id}
+ 我们执行一下试试 - ![image-20210731163729668.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/3c96d405939120306fdb365eecca1cccd1bfc5eb40838a8439e28d7998847903) - ![image-20210731163748574.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/765b99d9a85a7b15339714be82c8f8f501d08837549436aa4a6fadf13918c6e4) + 更新成功,原来`set`标签和`where`标签一样,这么智能呀! + `set`标签可以自动帮我识别语句结尾的逗号并进行一定的处理! + 这样我们写的sql语句是不是更加具有灵活性呢?十分方便好用!

②、trim 标签

  • 通过上面的例子我们知道了,where 、set 标签可以去除逗号 、and 、or 这种连接符。
  • trim 标签也是可以做到的!
  • xml
+ 
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复制代码    <insert id="insertBlogName" parameterType="map">
insert into myblog
(
<if test="title!=null and title!=''">
title,
</if>
<if test="autor!=null and autor!=''">
autor,
</if>
<if test="reads!=null and reads!=''">
`reads`,
</if>
<if test="creat_time!=null">
`creat_time`
</if>
)
values(
<if test="title!=null and title!=''">
#{title},
</if>
<if test="autor!=null and autor!=''">
#{autor},
</if>
<if test="reads!=null and reads!=''">
#{reads},
</if>
<if test="creat_time!=null">
#{creat_time}
</if>
)
</insert>
  • mapper
+ 
1
java复制代码Integer insertBlogName(Map<String,Object> map);
  • Junit Test
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    @Test
public void insetBlogInfo(){
SqlSession session = MybatisUtils.getSession();
MyBlogMapper mapper = session.getMapper(MyBlogMapper.class);
Map<String,Object> map = new HashMap<>();
map.put("title","如何榜上富婆?");
map.put("autor","大鱼");
map.put("reads","1000");
map.put("creat_time",new Date());
Integer num = mapper.insertBlogName(map);
System.out.println("一共新增了:"+num+"条数据");
session.commit();
session.close();
}
  • 执行结果
+ ![image-20210731194154983.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/3349016cc5b7a8b07113787ff8a3fde8260bff53c0dfc1b9fe1fcf3e83bcd5b3)
+ ![image-20210731194211831.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/b39833b038cacda0e52c74a33ec3e00d42e3fa8b1d5ae746712a1fa110ad2a57)
  • 记得session.commit(); 提交事务哦~
  • 这个sql如果我们不是传所有,是不是也会出现上面的问题?就是多个逗号,导致sql执行出错?
+ ![image-20210731194402341.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/bbba947b2015dbff94fa12998166480b576a8e35a52302b8f586184f5148c656)

image-20210731194402341.png

  • 我们可以使用trim标签来完成自动去除逗号等连接符
  • xml改造
+ 
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复制代码    <insert id="insertBlogName1" parameterType="map">
insert into myblog
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="title!=null and title!=''">
title,
</if>
<if test="autor!=null and autor!=''">
autor,
</if>
<if test="reads!=null and reads!=''">
`reads`,
</if>
<if test="creat_time!=null">
`creat_time`
</if>
</trim>
<trim prefix="values(" suffix=")" suffixOverrides=",">
<if test="title!=null and title!=''">
#{title},
</if>
<if test="autor!=null and autor!=''">
#{autor},
</if>
<if test="reads!=null and reads!=''">
#{reads},
</if>
<if test="creat_time!=null">
#{creat_time}
</if>
</trim>
</insert>
+ 执行结果 - ![image-20210731194922870.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/18e53b37af3d3e8889a2b1d58e664810da3c5c37c18ad906c7eb0588279229d4) - ![image-20210731194938661.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/790bc7e177ead7c890db89bba5e7616bdb979efc38b82497b979d3242704803a) + 完美解决问题! + 重点解析 - prefix:开头所需加的 - suffix:结束所需加的 - suffixOverrides:每行语句结束时需要加的东西 - prefixOverrides:每行语句开始时需要加的东西 + 注意点 - 当数据库中设置了时间为datetime时,我们`if`标签中不要判断不为空 - 只需要判断不为null即可 - ![image-20210731195553294.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/5157ef9f09859a70c6b70d7eb7afc674e8a5d56f5184c987cadb6e91cd38811d)

四、foreach 标签

  • 在sql中写for循环是种什么感受?
  • 来玩一个需求:查询id为1、2、4、5的博客数据。
  • 学到这里了,千万不要这样用and这种捞的写法。
+ 
1
2
3
4
5
6
7
	java复制代码select * from myblog where id = 1 or id = 2 or id = 4 or id = 5

```#### ①、foreach简单用法
* xml


+
java复制代码 <select id="getBlogInfos" parameterType="map" resultType="Blog"> select * from myblog <where> <foreach collection="ids" item="id" open="and (" close=")" separator="or"> id=#{id} </foreach> </where> </select>
1
2
3
4
* mapper


+
java复制代码List<Blog> getBlogInfos(Map<String,Object> map);
1
2
3
4
* Junit Test


+
java复制代码 @Test public void getBlogInfos(){ SqlSession session = MybatisUtils.getSession(); MyBlogMapper mapper = session.getMapper(MyBlogMapper.class); Map<String,Object> map = new HashMap<>(); List<String> ids = new ArrayList<>(); ids.add("1"); ids.add("2"); ids.add("4"); ids.add("5"); map.put("ids",ids); List<Blog> list = mapper.getBlogInfos(map); for (Blog blog : list) { System.out.println(blog); } session.close(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
* 执行结果


+ ![image-20210731221756853.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/543242261ad521fd0af091efc64b8f418ba7a22ebbd9a93e166f772093c2e18c)
+ foreach标签


- collection : 这个是我们在标签内所需遍历的集合,即放入map中的key
- item : 遍历出来的值我们所赋予的key
- open : 开始所加的参数
- close : 结束所加的参数
- separator : 每个值中间所加的参数#### ②、where in 配合foreach用法


+ xml


-
java复制代码 <select id="getBlogInfos1" parameterType="map" resultType="Blog"> select * from myblog <where> id in <foreach collection="ids" item="id" open="(" close=")" separator=","> #{id} </foreach> </where> </select>
1
2
3
4
5
+ 其他地方不需要变
+ 这个最终效果是


-
java复制代码select * from myblog WHERE id in ( ? , ? , ? , ? )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
		- ![image-20210731222613047.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/e8f651c243422e2bca8de17e68c07a13343acd90321c41324b0ff098a8bf8def)
+ 只列举这两个例子吧,具体怎么用还是得看具体的需求。
+ sql的长度是有限制的,这个ids里面的值太多会无法查询。
+ sql最大长度是:默认的SQL拼接长度最大值是2000个参数。


### 五、sql、include 标签


* 上面那么多标签玩下来,大家会发现一个问题,就是同一个业务中,可能会有很多重复的内容,比如每个查询都有`title`、`autor`等相同内容。
* 那么我们有没有类似于java中工具类的方法呢?
* 这时候就需要用到我们的sql、include 标签啦~
* xml


+
java复制代码 <sql id="if-key-info"> <if test="title!=null and title!=''"> title = #{title} </if> <if test="autor!=null and autor!=''"> and autor = #{autor} </if> <if test="reads!=null and reads!=''"> and `reads` = #{reads} </if> </sql> <select id="getBlogInfo" resultType="Blog" parameterType="map"> select * from myblog <where> <include refid="if-key-info"></include> </where> </select> ``` + 执行结果 - ![image-20210731223327323.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/bbd17b91316aa19ecd48929aa1278ef34f5bcdae26987ede4a9cf37b81fd9b10) + 有了这个,我们可以把相同的的放在一起,是不是方便了很多呢? + sql id= "" :这里面的命名是随意的,只要在当前mapper.xml是唯一的即可 + include : 引用上面的命名即可!

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

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

深入分析 Java IO (三)NIO

发表于 2021-08-03

前言

前两篇文章介绍了IO和BIO, 关于相关文章的链接如下:

深入分析 Java IO (一)概述

深入分析 Java IO (二)BIO

本篇文章我们就重点介绍NIO,下面进入正文,首先做个概述。

概述

NIO 中的 N 可以理解为 Non-blocking,一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入,对应的在java.nio包下。

NIO 新增了 Channel、Selector、Buffer 等抽象概念,支持面向缓冲、基于通道的 I/O 操作方法。

NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现。

NIO 这两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。

对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发效率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

正文

我们先看一下 NIO 涉及到的核心关联类图,如下:

img

上图中有三个关键类:Channel 、Selector 和 Buffer,它们是 NIO 中的核心概念。

  • Channel:可以理解为通道;
  • Selector:可以理解为选择器;
  • Buffer:可以理解为数据缓冲流;

NIO 引入了 Channel、Buffer 和 Selector 就是想把 IO 传输过程中涉及到的信息具体化,让程序员有机会去控制它们。

当我们进行传统的网络 IO 操作时,比如调用 write() 往 Socket 中的 SendQ 队列写数据时,当一次写的数据超过 SendQ 长度时,操作系统会按照 SendQ 的长度进行分割的,这个过程中需要将用户空间数据和内核地址空间进行切换,而这个切换不是程序员可以控制的,由底层操作系统来帮我们处理。

而在 Buffer 中,我们可以控制 Buffer 的 capacity(容量),并且是否扩容以及如何扩容都可以控制。

代码示例

实例图:

image-20210803083530560

客户端程序示例:

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
java复制代码package org.example.nio.example;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
* NIO客户端
*/
public class NIOClient {

// 通道管理器(Selector)
private static Selector selector;

public static void main(String[] args) throws IOException {
// 创建通道管理器(Selector)
selector = Selector.open();
// 创建通道SocketChannel
SocketChannel channel = SocketChannel.open();
// 将通道设置为非阻塞
channel.configureBlocking(false);
// 客户端连接服务器,其实方法执行并没有实现连接,需要在handleConnect方法中调channel.finishConnect()才能完成连接
channel.connect(new InetSocketAddress("127.0.0.1", 9090));
/**
* 将通道(Channel)注册到通道管理器(Selector),并为该通道注册selectionKey.OP_CONNECT
* 注册该事件后,当事件到达的时候,selector.select()会返回,
* 如果事件没有到达selector.select()会一直阻塞。
*/
channel.register(selector, SelectionKey.OP_CONNECT);
// 循环处理
while (true) {
/*
* 选择一组可以进行I/O操作的事件,放在selector中,客户端的该方法不会阻塞,
* selector的wakeup方法被调用,方法返回,而对于客户端来说,通道一直是被选中的
* 这里和服务端的方法不一样,查看api注释可以知道,当至少一个通道被选中时。
*/
selector.select();
// 获取监听事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 迭代处理
while (iterator.hasNext()) {
// 获取事件
SelectionKey key = iterator.next();
// 移除事件,避免重复处理
iterator.remove();
// 检查是否是一个就绪的已经连接服务端成功事件
if (key.isConnectable()) {
handleConnect(key);
} else if (key.isReadable()) {// 检查套接字是否已经准备好读数据
handleRead(key);
}
}
}
}

/**
* 处理客户端连接服务端成功事件
*/
private static void handleConnect(SelectionKey key) throws IOException {
// 获取与服务端建立连接的通道
SocketChannel channel = (SocketChannel) key.channel();
if (channel.isConnectionPending()) {
// channel.finishConnect()才能完成连接
channel.finishConnect();
}
channel.configureBlocking(false);
// 数据写入通道
String msg = "Hello Server!";
channel.write(ByteBuffer.wrap(msg.getBytes()));
// 通道注册到选择器,并且这个通道只对读事件感兴趣
channel.register(selector, SelectionKey.OP_READ);
}

/**
* 监听到读事件,读取客户端发送过来的消息
*/
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
// 从通道读取数据到缓冲区
ByteBuffer buffer = ByteBuffer.allocate(128);
channel.read(buffer);
// 输出服务端响应发送过来的消息
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务端发来的消息:" + msg);
}
}

服务端示例:

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
java复制代码package org.example.nio.example;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
* NIO服务端
*/
public class NIOServer {

// 通道管理器(Selector)
private static Selector selector;

public static void main(String[] args) throws IOException {
// 创建通道管理器(Selector)
selector = Selector.open();
// 创建通道ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 将通道设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 将ServerSocketChannel对应的ServerSocket绑定到指定端口(port)
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(9090));
/**
* 将通道(Channel)注册到通道管理器(Selector),并为该通道注册selectionKey.OP_ACCEPT事件
* 注册该事件后,当事件到达的时候,selector.select()会返回,
* 如果事件没有到达selector.select()会一直阻塞。
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 循环处理
while (true) {
// 当注册事件到达时,方法返回,否则该方法会一直阻塞
selector.select();
// 获取监听事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 迭代处理
while (iterator.hasNext()) {
// 获取事件
SelectionKey key = iterator.next();
// 移除事件,避免重复处理
iterator.remove();
// 检查是否是一个就绪的可以被接受的客户端请求连接
if (key.isAcceptable()) {
handleAccept(key);
} else if (key.isReadable()) {// 检查套接字是否已经准备好读数据
handleRead(key);
}
}
}
}

/**
* 处理客户端连接成功事件
*/
private static void handleAccept(SelectionKey key) throws IOException {
// 获取客户端连接通道
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
// 信息通过通道发送给客户端
String msg = "Hello Client!";
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
// 给通道设置读事件,客户端监听到读事件后,进行读取操作
socketChannel.register(selector, SelectionKey.OP_READ);
}


/**
* 监听到读事件,读取客户端发送过来的消息
*/
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
// 从通道读取数据到缓冲区
ByteBuffer buffer = ByteBuffer.allocate(128);
channel.read(buffer);
// 输出客户端发送过来的消息
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("server received msg from client:" + msg);
}
}

通道channel

NIO 的核心就是通道和缓存区,所以它们的工作模式是这样的:

image-20210802145658941

通道有点类似 IO 中的流,但不同的是,同一个通道既允许读也允许写,而任意一个流要么是读流要么是写流。
但是你要明白一点,通道和流一样都是需要基于物理文件的,而每个流或者通道都通过文件指针操作文件,这里说的通道是双向的也是有前提的,那就是通道基于随机访问文件RandomAccessFile的可读可写文件指针。

基本的通道类型有如下一些:

FileChannel 是基于文件的通道;

SocketChannel 和 ServerSocketChannel 用于网络 TCP 套接字数据报读写;

DatagramChannel 是用于网络 UDP 套接字数据报读写。

通道不能单独存在,它永远需要绑定一个缓存区,所有的数据只会存在于缓存区中,无论你是写或是读,必然是缓存区通过通道到达磁盘文件,或是磁盘文件通过通道到达缓存区。即缓存区是数据的起点,也是终点。

缓存区Buffer

缓冲区(Buffer):一个用于特定基本数据类型的容器。由java.nio包定义的,所有缓冲区都是Buffer抽象类的子类。

Java NIO 中的 Buffer 主要用于和 NIO 通道进行交互,数据是从通道读入到缓冲区的,然后从缓冲区中写入到通道中的。

Buffer 就像一个数组,可以保存多个相同类型的数据。根据数据类型的不同(boolean)除外,有以下 Buffer 常用子类:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。上述 Buffer 类他们都是通过相似的方法进行管理数据的,只是各自管理的数据类型不同而已。都是通过如下的方法获取一个 Buffer 对象:

1
arduino复制代码public static XxxBuffer allocate(int capacity) {}

缓冲区的基本属性

容量(capacity):表示 Buffer 最大数据容量,缓冲区容量不能为负,并且一旦创建不能更改。

限制(limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于容量。

位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制。

标记(mark)和重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中的一个特定的position,之后可以通过调用reset()方法恢复到这个position。

简而言之:0 <= mark <= position <= limit <= capacity。

Selector(选择器)

Selector 被称为选择器 ,当然你也可以翻译为多路复用器 。它是Java NIO 核心组件中的一个,用于检查一个或多个 Channel(通道)的状态是否处于连接就绪、接受就绪、可读就绪、可写就绪。

如此可以实现单线程管理多个 channels,也就是可以管理多个网络连接。

image-20210801232932867

使用 Selector 的好处在于: 相比传统方式使用多个线程来管理 IO,Selector 使用了更少的线程就可以处理通道了,并且实现网络高效传输!

创建一个选择器一般是通过 Selector 的工厂方法,Selector.open :

1
java复制代码Selector selector = Selector.open();

而一个通道想要注册到某个选择器中,必须调整模式为非阻塞模式,例如:

1
2
3
4
5
6
java复制代码//创建一个 TCP 套接字通道
SocketChannel channel = SocketChannel.open();
//调整通道为非阻塞模式
channel.configureBlocking(false);
//向选择器注册一个通道
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

以上代码是注册一个通道到选择器中的最简单版本,支持注册选择器的通道都有一个 register 方法,该方法就是用于注册当前实例通道到指定选择器的。

该方法的第一个参数就是目标选择器,第二个参数其实是一个二进制掩码,它指明当前选择器感兴趣当前通道的哪些事件。以枚举类型提供了以下几种取值:

  • int OP_READ = 1 << 0;
  • int OP_WRITE = 1 << 2;
  • int OP_CONNECT = 1 << 3;
  • int OP_ACCEPT = 1 << 4;

这种用二进制掩码来表示某些状态的机制,我们在讲述虚拟机类类文件结构的时候也遇到过,它就是用一个二进制位来描述一种状态。

register 方法会返回一个 SelectionKey 实例,该实例代表的就是选择器与通道的一个关联关系。你可以调用它的 selector 方法返回当前相关联的选择器实例,也可以调用它的 channel 方法返回当前关联关系中的通道实例。

除此之外,SelectionKey 的 readyOps 方法将返回当前选择感兴趣当前通道中事件中准备就绪的事件集合,依然返回的一个整型数值,也就是一个二进制掩码。

例如:

1
java复制代码int readySet = selectionKey.readyOps();

假如 readySet 的值为 13,二进制 「0000 1101」,从后向前数,第一位为 1,第三位为 1,第四位为 1,那么说明选择器关联的通道,读就绪、写就绪,连接就绪。

所以,当我们注册一个通道到选择器之后,就可以通过返回的 SelectionKey 实例监听该通道的各种事件。

当然,一旦某个选择器中注册了多个通道,我们不可能一个一个的记录它们注册时返回的 SelectionKey 实例来监听通道事件,选择器应当有方法返回所有注册成功的通道相关的 SelectionKey 实例。

1
java复制代码Set<SelectionKey> keys = selector.selectedKeys();

selectedKeys 方法会返回选择器中注册成功的所有通道的 SelectionKey 实例集合。我们通过这个集合的 SelectionKey 实例,可以得到所有通道的事件就绪情况并进行相应的处理操作。

总结

  • 优点
    • 1个线程就行就能处理所有连接,这个线程不停循环遍历就行了。
  • 缺点
    • 单线程不停循环发起系统调用,一样会耗尽 CPU 资源。
  • NIO 的瓶颈
    • 在于需要不停的调起系统调用,每个链接我们都要调系统调用询问是否有过来数据,我们要是明确的知道哪个连接有数据包过来呢,就不用挨个遍历寻找找答案了。

结尾

我是一个正在被打击还在努力前进的码农。如果文章对你有帮助,记得点赞、关注哟,谢谢!

本文转载自: 掘金

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

【Docker 系列】docker 学习 一,Docker的

发表于 2021-08-03

这是我 8月更文挑战 的第 3 天

Docker 学习 一

Docker 是什么

网址:hub.docker.com/

docker对进程进行封装隔离,属于 操作系统层面的虚拟化技术

由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器

docker 应用场景

  • 自动化测试和持续集成、发布
  • Web 应用的自动化打包和发布
  • 后台应用易部署

docker 的优势

  • 快速, 一致的交付应用程序
  • 可移植,可扩展
  • 轻巧,快速,经济,高效,压榨linux自身资源

Docker 能做什么?

先来说说 Docker 和虚拟机有啥不一样的

以前的虚拟机这样的,系统占用资源大,很多步骤是冗余的,并且启动还很慢,不能忍

现在的 Docker 是这个样子的,

容器之间互相隔离,互补干扰,一起运行在同一个操作系统上,最大化使用操作系统资源

​

Docker 技术和虚拟机技术的不同?

  • 每个容器间都是相互隔离的,他们有属于自己的文件系统,相互不会有影响
  • 容器没有自己的内核,没有自己的硬件,容器内的应用是直接运行在宿主机的内核中
  • 传统的虚拟机是虚拟出一个硬件,运行完成的操作系统,在其上面运行应用

那么 Docker 具体能做什么?

做 DevOps

做 DevOps 有如下几个提升点:

  • 应用可以更快捷的部署和交付

以前麻烦的安装步骤一去不复返,使用 Docker 容器化后,打包镜像发布测试,一键部署及运行

  • 可以更方便的升级和扩容

使用 Docker,将项目打包成镜像,升级方便,扩容方便

  • 开发,运维,测试都会更简单

再也不用担心开发环境,测试环境,运维环境不一致的情况了

  • 更高效的利用资源

Dcoker 是运行在宿主机的内核中,可以在这台物理主机上部署多个 Docker 实例

Docker 的组成

Docker 使用客户端-服务器 (C/S) 架构模式,使用远程API来管理和创建 Docker 容器

Docker 的三个基本概念:

图片来源于网络

  • 镜像

相当于是一个 root 文件系统,类似于一个模板,这是静态的

  • 容器

相当于从模板拉出来的一个实例,容器通过镜像来创建,我们可以对他做创建,启动,停止,暂停,删除等操作

  • 仓库

用来保存镜像的,可以看做是一个代码控制中心

Docker 的安装和使用

安装

网络上安装 Docker 的方式大致有如下几种:

  • 官方脚本自动安装
  • 使用 Docker 仓库安装
  • 使用 ==shell== 脚本安装

咱们以 ubuntu 的系统为例子,使用 Docker 仓库的方式进行安装,我的ubuntu 系统版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码# cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.5 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.5 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic

设置仓库

安装依赖包

1
2
3
4
5
6
7
shell复制代码sudo apt-get update
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common

添加 Docker 的官方 GPG 密钥:

1
shell复制代码curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg | sudo apt-key add -

验证秘钥,可以直接搜索 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88 后面的8个字符

1
shell复制代码sudo apt-key fingerprint 0EBFCD88

有如下输出为正确设置了秘钥

1
2
3
4
shell复制代码pub   rsa4096 2017-02-22 [SCEA]
9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88
uid [ unknown] Docker Release (CE deb) <docker@docker.com>
sub rsa4096 2017-02-22 [S]

设置稳定版仓库

1
2
3
4
shell复制代码sudo add-apt-repository \
"deb [arch=amd64] https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/ \
$(lsb_release -cs) \
stable"

安装 Docker

安装最新的 Docker 版本

1
2
shell复制代码sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

安装完成后,验证是否OK

可以通过 docker version 来查看 docker 的版本

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
yaml复制代码# docker version
Client: Docker Engine - Community
Version: 20.10.7
API version: 1.41
Go version: go1.13.15
Git commit: f0df350
Built: Wed Jun 2 11:56:40 2021
OS/Arch: linux/amd64
Context: default
Experimental: true

Server: Docker Engine - Community
Engine:
Version: 20.10.7
API version: 1.41 (minimum version 1.12)
Go version: go1.13.15
Git commit: b0f5bc3
Built: Wed Jun 2 11:54:48 2021
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.4.8
GitCommit: 7eba5930496d9bbe375fdf71603e610ad737d2b2
runc:
Version: 1.0.0
GitCommit: v1.0.0-0-g84113ee
docker-init:
Version: 0.19.0
GitCommit: de40ad0

运行一个 hello-world

1
shell复制代码sudo docker run hello-world

出现如下信息,为 docker 安装成功

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
shell复制代码Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:df5f5184104426b65967e016ff2ac0bfcd44ad7899ca3bbcf8e44e4461491a9e
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/

当然,你也可以选择不安装最新的,安装自己指定的版本也可

  1. 使用 ==apt-cache madison docker-ce== 查看仓库中可用的版本
  2. 使用 ==sudo apt-get install docker-ce=<VERSION_STRING> docker-ce-cli=<VERSION_STRING> containerd.io== 安装指定版本的 Docker
  3. 使用 ==sudo docker run hello-world== 验证是否安装成功
  • 查看 docker 镜像
1
2
3
arduino复制代码# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest d1165f221234 4 months ago 13.3kB
  • docker 镜像
1
2
3
4
5
6
7
shell复制代码卸载镜像
sudo apt-get purge docker-ce docker-ce-cli containerd.io

删除安装目录
/var/lib/docker 是docker 的默认安装路径
sudo rm -rf /var/lib/docker
sudo rm -rf /var/lib/containerd

镜像加速

如果是使用阿里云服务器的小伙伴可以看这一步

配置镜像加速,需要 docker 的安装版本在 1.10.0 以上,我们当前安装的 docker 版本为 1.41,完全符合

我们可以通过修改 daemon 配置文件 /etc/docker/daemon.json 来使用加速器,执行如下指令

1
2
3
4
5
6
7
8
shell复制代码sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://uhr5ub75.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

docker run 的流程

  • docker run 现在本地找对应的镜像,若有则直接运行
  • 若没有就去docker hub 上下载,若有就下载到本地后运行
  • 若没有就直接报错

Docker 的底层原理

Docker 是如何工作的?

docker 是一个C/S 模型,docker 的后台守护进行运行在主机上,客户端和服务端通过套接字 Socket 通信

docker 服务端收到 docker 客户端的指令时,则执行该指令

为什么 Docker 比 虚拟机快呢?

在网络上找了一张图,咱们对比一下就明确了

如图,Docker 比虚拟机快的原因如下:

  • docker 比虚拟机的抽象层更少
  • docker 利用的是宿主机的内核,而虚拟机是需要新建一个 OS

基于如上 2 点,虚拟机启动时,会加载操作系统,启动慢,时间基本上是分钟级的

docker 启动的时候,不需要加载操作系统内核,因此快,时间基本上是秒级的

参考资料:

docker docs

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

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

1…582583584…956

开发者博客

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