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

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


  • 首页

  • 归档

  • 搜索

为什么需要Java内存模型?

发表于 2021-10-14

面试官:今天想跟你聊聊Java内存模型,这块你了解过吗?

候选者:嗯,我简单说下我的理解吧。那我就从为什么要有Java内存模型开始讲起吧

面试官:开始你的表演吧。

候选者:那我先说下背景吧

候选者:1. 现有计算机往往是多核的,每个核心下会有高速缓存。高速缓存的诞生是由于「CPU与内存(主存)的速度存在差异」,L1和L2缓存一般是「每个核心独占」一份的。

候选者:2. 为了让CPU提高运算效率,处理器可能会对输入的代码进行「乱序执行」,也就是所谓的「指令重排序」

候选者:3. 一次对数值的修改操作往往是非原子性的(比如i++实际上在计算机执行时就会分成多个指令)

候选者:在永远单线程下,上面所讲的均不会存在什么问题,因为单线程意味着无并发。并且在单线程下,编译器/runtime/处理器都必须遵守as-if-serial语义,遵守as-if-serial意味着它们不会对「数据依赖关系的操作」做重排序。

候选者:CPU为了效率,有了高速缓存、有了指令重排序等等,整块架构都变得复杂了。我们写的程序肯定也想要「充分」利用CPU的资源啊!于是乎,我们使用起了多线程

候选者:多线程在意味着并发,并发就意味着我们需要考虑线程安全问题

候选者:1. 缓存数据不一致:多个线程同时修改「共享变量」,CPU核心下的高速缓存是「不共享」的,那多个cache与内存之间的数据同步该怎么做?

候选者:2. CPU指令重排序在多线程下会导致代码在非预期下执行,最终会导致结果存在错误的情况。

候选者:针对于「缓存不一致」问题,CPU也有其解决办法,常被大家所认识的有两种:

候选者:1.使用「总线锁」:某个核心在修改数据的过程中,其他核心均无法修改内存中的数据。(类似于独占内存的概念,只要有CPU在修改,那别的CPU就得等待当前CPU释放)

候选者:2.缓存一致性协议(MESI协议,其实协议有很多,只是举个大家都可能见过的)。MESI拆开英文是(Modified (修改状态)、Exclusive (独占状态)、Share(共享状态)、Invalid(无效状态))

候选者:缓存一致性协议我认为可以理解为「缓存锁」,它针对的是「缓存行」(Cache line) 进行”加锁”,所谓「缓存行」其实就是 高速缓存 存储的最小单位。

面试官:嗯…

候选者:MESI协议的原理大概就是:当每个CPU读取共享变量之前,会先识别数据的「对象状态」(是修改、还是共享、还是独占、还是无效)。

候选者:如果是独占,说明当前CPU将要得到的变量数据是最新的,没有被其他CPU所同时读取

候选者:如果是共享,说明当前CPU将要得到的变量数据还是最新的,有其他的CPU在同时读取,但还没被修改

候选者:如果是修改,说明当前CPU正在修改该变量的值,同时会向其他CPU发送该数据状态为invalid(无效)的通知,得到其他CPU响应后(其他CPU将数据状态从共享(share)变成invalid(无效)),会当前CPU将高速缓存的数据写到主存,并把自己的状态从modify(修改)变成exclusive(独占)

候选者:如果是无效,说明当前数据是被改过了,需要从主存重新读取最新的数据。

候选者:其实MESI协议做的就是判断「对象状态」,根据「对象状态」做不同的策略。关键就在于某个CPU在对数据进行修改时,需要「同步」通知其他CPU,表示这个数据被我修改了,你们不能用了。

候选者:比较于「总线锁」,MESI协议的”锁粒度”更小了,性能那肯定会更高咯

面试官:但据我了解,CPU还有优化,你还知道吗?

候选者:嗯,还是了解那么一点点的。

候选者:从前面讲到的,可以发现的是:当CPU修改数据时,需要「同步」告诉其他的CPU,等待其他CPU响应接收到invalid(无效)后,它才能将高速缓存数据写到主存。

候选者:同步,意味着等待,等待意味着什么都干不了。CPU肯定不乐意啊,所以又优化了一把。

候选者:优化思路就是从「同步」变成「异步」。

候选者:在修改时会「同步」告诉其他CPU,而现在则把最新修改的值写到「store buffer」中,并通知其他CPU记得要改状态,随后CPU就直接返回干其他事了。等到收到其它CPU发过来的响应消息,再将数据更新到高速缓存中。

候选者:其他CPU接收到invalid(无效)通知时,也会把接收到的消息放入「invalid queue」中,只要写到「invalid queue」就会直接返回告诉修改数据的CPU已经将状态置为「invalid」

候选者:而异步又会带来新问题:那我现在CPU修改完A值,写到「store buffer」了,CPU就可以干其他事了。那如果该CPU又接收指令需要修改A值,但上一次修改的值还在「store buffer」中呢,没修改至高速缓存呢。

候选者:所以CPU在读取的时候,需要去「store buffer」看看存不存在,存在则直接取,不存在才读主存的数据。【Store Forwarding】

候选者:好了,解决掉第一个异步带来的问题了。(相同的核心对数据进行读写,由于异步,很可能会导致第二次读取的还是旧值,所以首先读「store buffer」。

面试官:还有其他?

候选者:那当然啊,那「异步化」会导致相同核心读写共享变量有问题,那当然也会导致「不同」核心读写共享变量有问题啊

候选者:CPU1修改了A值,已把修改后值写到「store buffer」并通知CPU2对该值进行invalid(无效)操作,而CPU2可能还没收到invalid(无效)通知,就去做了其他的操作,导致CPU2读到的还是旧值。

候选者:即便CPU2收到了invalid(无效)通知,但CPU1的值还没写到主存,那CPU2再次向主存读取的时候,还是旧值…

候选者:变量之间很多时候是具有「相关性」(a=1;b=0;b=a),这对于CPU又是无感知的…

候选者:总体而言,由于CPU对「缓存一致性协议」进行的异步优化「store buffer」「invalid queue」,很可能导致后面的指令很可能查不到前面指令的执行结果(各个指令的执行顺序非代码执行顺序),这种现象很多时候被称作「CPU乱序执行」

候选者:为了解决乱序问题(也可以理解为可见性问题,修改完没有及时同步到其他的CPU),又引出了「内存屏障」的概念。

面试官:嗯…

候选者:「内存屏障」其实就是为了解决「异步优化」导致「CPU乱序执行」/「缓存不及时可见」的问题,那怎么解决的呢?嗯,就是把「异步优化」给”禁用“掉(:

候选者:内存屏障可以分为三种类型:写屏障,读屏障以及全能屏障(包含了读写屏障),屏障可以简单理解为:在操作数据的时候,往数据插入一条”特殊的指令”。只要遇到这条指令,那前面的操作都得「完成」。

候选者:那写屏障就可以这样理解:CPU当发现写屏障的指令时,会把该指令「之前」存在于「store Buffer」所有写指令刷入高速缓存。

候选者:通过这种方式就可以让CPU修改的数据可以马上暴露给其他CPU,达到「写操作」可见性的效果。

候选者:那读屏障也是类似的:CPU当发现读屏障的指令时,会把该指令「之前」存在于「invalid queue」所有的指令都处理掉

候选者:通过这种方式就可以确保当前CPU的缓存状态是准确的,达到「读操作」一定是读取最新的效果。

候选者:由于不同CPU架构的缓存体系不一样、缓存一致性协议不一样、重排序的策略不一样、所提供的内存屏障指令也有差异,为了简化Java开发人员的工作。Java封装了一套规范,这套规范就是「Java内存模型」

候选者:再详细地说,「Java内存模型」希望 屏蔽各种硬件和操作系统的访问差异,保证了Java程序在各种平台下对内存的访问都能得到一致效果。目的是解决多线程存在的原子性、可见性(缓存一致性)以及有序性问题。

面试官:那要不简单聊聊Java内存模型的规范和内容吧?

候选者:不了,怕一聊就是一个下午,下次吧?

本文总结:

  • 并发问题产生的三大根源是「可见性」「有序性」「原子性」
  • 可见性:CPU架构下存在高速缓存,每个核心下的L1/L2高速缓存不共享(不可见)
  • 有序性:主要有三方面可能导致打破
+ 编译器优化导致重排序(编译器可以在不改变单线程程序语义的情况下,可以对代码语句顺序进行调整重新排序)
+ 指令集并行重排序(CPU原生就有可能将指令进行重排)
+ 内存系统重排序(CPU架构下很可能有store buffer /invalid queue 缓冲区,这种「异步」很可能会导致指令重排)
  • 原子性:Java的一条语句往往需要多条 CPU 指令完成(i++),由于操作系统的线程切换很可能导致 i++ 操作未完成,其他线程“中途”操作了共享变量 i ,导致最终结果并非我们所期待的。
  • 在CPU层级下,为了解决「缓存一致性」问题,有相关的“锁”来保证,比如“总线锁”和“缓存锁”。
+ 总线锁是锁总线,对共享变量的修改在相同的时刻只允许一个CPU操作。
+ 缓存锁是锁缓存行(cache line),其中比较出名的是MESI协议,对缓存行标记状态,通过“同步通知”的方式,来实现(缓存行)数据的可见性和有序性
+ 但“同步通知”会影响性能,所以会有内存缓冲区(store buffer/invalid queue)来实现「异步」进而提高CPU的工作效率
+ 引入了内存缓冲区后,又会存在「可见性」和「有序性」的问题,平日大多数情况下是可以享受「异步」带来的好处的,但少数情况下,需要强「可见性」和「有序性」,只能”禁用”缓存的优化。
+ “禁用”缓存优化在CPU层面下有「内存屏障」,读屏障/写屏障/全能屏障,本质上是插入一条”屏障指令”,使得缓冲区(store buffer/invalid queue)在屏障指令之前的操作均已被处理,进而达到 读写 在CPU层面上是可见和有序的。
  • 不同的CPU实现的架构和优化均不一样,Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,保证了Java程序在各种平台下对内存的访问都能得到一致效果

欢迎关注我的微信公众号【Java3y】来聊聊Java面试,对线面试官系列持续更新中!

【对线面试官-移动端】系列 一周两篇持续更新中!

【对线面试官-电脑端】系列 一周两篇持续更新中!

原创不易!!求三连!!

本文转载自: 掘金

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

【JAVA】OpenFegin 实现接口参数签名认证(一)

发表于 2021-10-13

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

我们在日常开发中,常常会对接一些三方sdkapi,而对接这些sdk就必然会遇到参数签名认证的问题,下面我将会用两篇文章来讲解 什么是接口参数签名认证、以及 OpenFeign 如何实现接口参数签名认证。下面让我们开始吧

为啥接口要使用参数签名

有些同学可能还没有接触过参数签名校验,下面我们将会详讲一下出现的场景,以及为啥需要 😍。

我们在使用三方sdk的时候,一般是不需要进行登录获取认证信息的。为啥呢?如果使用登录的话,过程繁琐,而且存在维护认证信息困难的问题。所以我们可以发现,一般的三方 开放sdkapi 接口,都会提供给我们一个 appkey(有的叫 appid,反正是同一种东西) 和一个 appsecret 来代替登录。有的小伙伴就会有疑问了,这和参数签名校验有啥关系呢?🤪 别慌,我们慢慢道来。

image.png

首先,我们思考一下,只有一个 appkey 和 appsecret 我们怎么实现接口身份认证呢?同学们可以先闭目思考一下。是直接把 appkey 和 appsecret 直接当成参数明文传过去吗?🤓,嘻嘻,应该没有同学这么想的吧。。。

image.png

我们明确一点,appkey 表明你是谁, appsecret 表明你就是真的你,这两个值 appkey 和 appsecret 是必然需要包含到请求信息里面的。要让别人找到你,appkey 肯定是需要直接给到对方的(ps: 可以明文,也可以通过加密的方式,只要协商好,啥都不是事儿),appsecret 则需要用某种 “加密” 的方式隐藏在参数里面,在认证的时候,比对身份信息是否匹配,匹配 ok 通过,匹配 错误 ,那就拜拜。这时候我们欠缺的就是某种 “加密” 的方式了。咱也不卖关子,就直接说说这个带引号的 “加密” 通常是怎么做的吧。

20180129234948_UPGeXa.gif

  1. 一般的三方开放sdkapi都会要求带上至少三个参数,appkey、sign、timestamp,这三个参数呢,可能各有各的叫法,首先第一个 appkey ,有的三方sdk叫 appid,这个没有什么好讲的,这个是用来表明身份的。接着是第三个参数 timestamp (这里仅仅是个代称,代指除 appkey、sign 以外必要的参数) ,这个就比较花里胡哨了,可能每个三方sdk的名字都不一样,普遍一点,可能叫 timestamp 、nonce 、salt 、rand 等等,也可能是几个的组合,它的作用主要是给第二个参数 sign 的生成做夹杂参数的,作用我们和第二个参数 sign 一起讲,现在我们就来讲讲参数签名最核心的参数 sign。
  2. sign 一般由哈希算法生成,原串一般由 appkey + appsecret + 请求参数或者请求体 + timestamp 拼接而成(这个拼接规则就相当于上段中所说的 “加密” 方式),经过我们常见的哈希算法 MD5 或者 SHA-1 处理之后,得到我们最终的 sign 。聪明的小伙伴可能已经明白为啥要这样做了,当我们在后端接收参数时,通过 appkey 获取对应用户的 appsecret,如果没有找到appsecret,肯定直接认证失败再按照约定好的 “加密” 方式拼接好各个参数,再以相同的哈希算法生成一个 sign,最终和参数上带的参数 sign 比对,如果一致,就认证通过。这里还需要注意的一个点 请求参数或者请求体 必须是有序的,保证接口文档声明的一致,这样才能保证后端接收到的 请求参数或者请求体 一致,这样生成的 sign 才不会出现意料之外的差错。
  3. 有的同学可能会有这样的疑问?上一段落中的第三个参数 timestamp 有啥用,如果去掉这个参数,后端肯定跑的一样好。有这个想法很不错,是个机智的boy,但是这样会有一个问题,如果这个请求地址被其他人抓到,其他人就可以不断的访问这个 请求地址 获得资源,后端对其的校验结果,永远是通过的,所以第三个参数 timestamp 必不可少。
  4. 对于不同的sdkapi提供商,每个的实现方式和安全强度也不一致,如果安全强度低,可能第三个参数 timestamp 仅仅需要一个唯一的随机值 nonce 即可,后端仅仅需要判断一下这个 nonce 有没有被使用过,如果被使用过则判定不通过,以此来保证每次请求都是唯一的。而有的安全强度高,第三个参数 timestamp 则可能由两个参数或者更多组成。咋们来个比较常见的,第三个参数 timestamp 由一个 timestamp 时间戳,一个 nonce 随机值组成,后端在校验时,不仅仅会判断 nonce 有没有被使用过,同时还会校验这个 timestamp 与服务器时间相差多少,如果相差大于 某个值 ,则也会判定不通过。比如后端这个值设定为 5秒 ,那么每一个请求的时间戳与服务器时间相比较不能超过 5秒 ,间隔超过 5秒 的请求会被认定为重放攻击或者错误的请求,这样极大的保证了接口的安全性。懂了这个原理的话,这第三个参数 timestamp 就有了 N种 组合的可能。

带点后端的思考,对于我这个干了几年的 后端菜菜鸟 来说,这第三个参数 timestamp 的 N种 组合中,带时间戳的肯定是比较好的,如果要保证请求的唯一性,那么就需要有个地方来保存这些已经请求过的第三个参数 timestamp ,如果请求量大时间长,维护这个东西肯定就开始伤脑筋了。而带时间戳的,就有了过期时间,如果超过设定的值,直接丢掉即可,减少维护和存储的成本😜

image.png

总结一下

我们总结一下上面三个参数的作用

  1. 保证每次请求唯一
  2. 防止参数被篡改
  3. 保证请求合法,来自认证用户

这也就是我们为啥要参数签名校验的原因咯😵

本篇文章到这儿就结束了,碍于时间问题,第二篇 OpenFeign 如何实现接口参数签名认证 咋们明天继续👻,谢谢大家的支持与鼓励。


如果文章对您有帮助的话,欢迎 点赞 、 评论 、 关注 、 收藏 、 分享 ,您的支持是我码字的动力,万分感谢!!!🌈

如果文章内容出现错误的地方,欢迎指正,交流,谢谢😘

参考链接

  • OpenFeign

本文转载自: 掘金

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

【评论抽掘金限定周边】 Java8的Optional你真的

发表于 2021-10-13

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

不管是一名小白程序员还是有三五年工作的CRUD BOY,或者是资深的高级Java工程师,在我们日常开发中,由于null的存在,经常会遇到NullPointerException,而我们为了避免该异常的产生,往往需要对于使用到的对象进行一些非空判断,最开始我们都会使用if…else来进行处理,比如下面的代码:

1
2
3
4
5
6
7
8
9
10
java复制代码Person person = getPersonById("123");
if (person != null) {
Address add = person.getAddress();
if (add != null) {
String city = add.getCity();
if (city != null) {
System.out.println(city);
}
}
}

这种if判断的方式导致代码的阅读性和维护性都变差,并且很容易会忘记,导致出现BUG。

Java 8中推出了Optional<T>,专门来解决这一问题。我们先来看看官方文档的介绍。

A container object which may or may not contain a non-null value. If a value is present, isPresent() will return true and get() will return the value.

Additional methods that depend on the presence or absence of a contained value are provided, such as orElse() (return a default value if value not present) and ifPresent() (execute a block of code if the value is present).

This is a value-based class; use of identity-sensitive operations (including reference equality (==), identity hash code, or synchronization) on instances of Optional may have unpredictable results and should be avoided.

通过官方文档我们可以看出,Optional<T>是一个容器对象,容器中存放着一个值,这个值可能是空也有可能不是空,并且还提供了一系列依赖于值是否为空的方法;Optional<T>是一个value-based类,也可以理解为基于值的类,所以像==,hash,synchronization这些操作应该避免,因为这些操作一般都是基于对象的空间地址而并非值。

方法

接下来我们看一下Optional<T>都提供哪些方法。

创建Optional

Optional.empty()

返回一个空的 Optional实例。

1
2
3
4
5
java复制代码public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}

Optional.of(T value)

返回具有 Optional的当前非空值的Optional

1
2
3
4
5
6
java复制代码public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}

该方法传入对象必须不能为空,如果为空则会抛出空指针异常。

Optional.ofNullable(T value)

返回一个 Optional指定值的Optional,如果非空,则返回一个空的 Optional 。

1
2
3
java复制代码public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}

ofNullable(T value)和 of(T value)唯一的区别是可以传入空对象,如果为空则同等于empty()。

其他相关方法

isPresent

如果存在值返回 true,否则为 false 。

1
2
3
java复制代码public boolean isPresent() {
return value != null;
}

ifPresent

如果存在值,则使用该值调用指定的消费者,否则不执行任何操作。

1
2
3
4
java复制代码public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}

注意和isPresent()方法区分。

orElse

如果值存在返回值,否则返回 other 。

1
2
3
java复制代码public T orElse(T other) {
return value != null ? value : other;
}

orElseGet

返回值(如果存在),否则调用 other并返回该调用的结果。

1
2
3
java复制代码public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}

orElseThrow

返回包含的值(如果存在),否则抛出由提供的exceptionSupplier创建的异常。

1
2
3
4
5
6
7
java复制代码public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}

map

将原Optional<T>转换为Optional<U>,如果值为空,则返回的也是空的Optional<U>。

1
2
3
4
5
6
7
8
9
java复制代码public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
// map方法中将apply的结果包装为Optional
return Optional.ofNullable(mapper.apply(value));
}
}

比如以下代码的目的是取出pOptional中person的address,如果person为空,则返回的的是Optional.empty(),反之则是Optional.ofNullable(person.getAddress()):

1
2
3
4
5
6
7
8
java复制代码Optional<Person> pOptional = Optional.ofNullable(getPersonById(123));
Optional<Address> address = pOptional.map(new Function<Person, Address>() {
@Override
public Address apply(Person person) {
// 返回的是值
return person.getAddress();
}
});

以上代码可简化为:

1
java复制代码Optional<Address> add = pOptional.map(Person::getAddress);

flatMap

从结果上看和map方法一样,都是将原Optional<T>转换为Optional<U>,就别在于方法参数中的mapper,map()方法的mapper要求apply()方法返回的是具体的值,而flatMap()的mapper参数的apply()方法返回的就是一个Optional<U>对象。

1
2
3
4
5
6
7
8
java复制代码public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}

上述map()中的案例使用flatMap()实现如下:

1
2
3
4
5
6
7
java复制代码Optional<Address> address1 = pOptional.flatMap(new Function<Person, Optional<Address>>() {
@Override
public Optional<Address> apply(Person person) {
// 需要自己包装成Optional
return Optional.of(person.getAddress());
}
});

同样可以简写为:

1
java复制代码pOptional.flatMap(person1 -> Optional.of(person1.getAddress()));

filter

对值进行过滤,如果值存在并且在predicate中返回为true,返回Optional本身,否则返回empty()。

1
2
3
4
5
6
7
java复制代码public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}

比如以下案例:

1
2
3
4
5
6
7
8
java复制代码Optional<Person> pOptional = Optional.ofNullable(getPersonById(123));
Optional<String> nameOpt = pOptional.filter(new Predicate<Person>() {
@Override
public boolean test(Person person) {
return person.getAge() > 10;
}
}).map(Person::getName);
System.out.println(nameOpt.orElse("Unknown"));

简写为:

1
2
java复制代码Optional<String> nameOpt = pOptional.filter(p -> p.getAge() > 10).map(Person::getName);
System.out.println(nameOpt.orElse("Unknown"));

那么通过上面的方法你是不是想立马在代码中对于你的if…else进行改造了呢?回顾我们最开始的代码:

1
2
3
4
5
6
7
8
9
10
java复制代码Person person = getPersonById("123");
if (person != null) {
Address add = person.getAddress();
if (add != null) {
String city = add.getCity();
if (city != null) {
System.out.println(city);
}
}
}

我们来用Optional改造一下看看:

1
2
3
4
5
java复制代码Person person = getPersonById("123");
Optional<String> cityOption = Optional.ofNullable(person)
.map(Person::getAddress)
.map(Address::getCity);
System.out.println("city is :"+cityOption.orElse("Unknown city."));

是不是感觉代码看着好像“高大上”了,但是这样是正确的吗?在这个过程中其实创建了很多的Optional对象出来,起码在空间上是更浪费的。

Optional正确的使用方式应该是什么呢?

Optional使用原则

Java语言的架构师Brian Goetz明确定义:

Optional is intended to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result,” and using null for such was overwhelmingly likely to cause errors.

Optional 旨在为库方法返回类型提供一种有限的机制;

其中需要一种明确的方式来表示“无结果”;

而对这种情况使用null极有可能导致错误。

Optional的设计初衷是为了作为方法的返回值,明确表示方法没有返回结果,而不是用null来表示,避免调用方因为返回null导致的错误。

那么有了这个设计意图,那么我们结合意图来看一下使用Optional需要遵循哪些原则。

不要过度使用Optional

有时候我们学会了一个东西之后就像立马使用,看什么代码都想套进去,有个例子说:有了一把锤子后,看啥都像钉子。

总想去敲两下。比如以下代码:

避免:

1
2
3
4
java复制代码public String fetchStatus() {
String status = ... ;
return Optional.ofNullable(status).orElse("PENDING");
}

而应该:

1
2
3
4
java复制代码public String fetchStatus() {
String status = ... ;
return status == null ? "PENDING" : status;
}

通过三目运算很简单的功能,非要用Optional,不光可读性变差,还额外构建了一个Optional对象。

不要将任何域对象声明为Optional

不要将任何方法的参数(尤其是setter)和构造方法的参数设置为Optional。

因为Optional不能被序列化,所以Optional设计时就没打算被用作对象的属性。

如果想通过Optional来保证对象的属性不被设置为null值,可以通过如下方式实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// PREFER
public class Customer {
private final String name; // 不能为空
private final String postcode; // 可以为空
public Cart(String name, String postcode) {
this.name = Objects.requireNonNull(name, () -> "Name cannot be null");
this.postcode = postcode;
}

public Optional<String> getPostcode() {
return Optional.ofNullable(postcode);
}
...
}

可以看到这里getPostcode()方法返回了一个Optional,但是不要在你的代码里将所有getter都改成这样,尤其是返回集合或数组的情况,返回空集合和数组就可以了,因为集合本身就有empty()方法可以判断是否为空。

在方法参数中使用Optionalin是另一个常见错误。这将导致代码的复杂度变高。应该在方法内部对参数进行检查,而不是强制调用方使用Optional。

不要使用Optional替代集合返回值

通过返回空集合的形式表示方法没有返回结果,通过Collections.emptyList()、emptyMap()和emptySet()构建空返回。

Optional值的比较使用equals

因为Optional类是一个value-based类,它的equals方法本身就是比较值,所以如果你需要对两个Optional中的值进行比较,则可以直接使用equals方法。

1
2
3
4
5
6
7
8
9
10
java复制代码public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Optional)) {
return false;
}
Optional<?> other = (Optional<?>) obj;
return Objects.equals(value, other.value);
}

不要将Null值赋值为Optional变量

Optional.empty()会初始化一个Optional代替null值,Optional是一个容器,被定义为null就没有意义。

避免以下代码:

1
2
3
4
java复制代码public Optional<Cart> fetchCart() {
Optional<Cart> emptyCart = null;
...
}

而应该:

1
2
3
4
java复制代码public Optional<Cart> fetchCart() {
Optional<Cart> emptyCart = Optional.empty();
...
}

在调用 get() 之前确保 Optional 具有值

如果你需要调用get()方法获取Optional值时,一定要确保在调用之前检查过Optional中有值;通常都会使用isPresent()-get()搭配的方式使用,但是这种方式并不优雅;但是你如果一定要选择使用get()则不要忘记isPresent()。

避免以下代码:

1
2
3
4
java复制代码Optional<Cart> cart = ... ; // 可能返回一个空的Optional
...
// 如果cart是空的会抛出java.util.NoSuchElementException
Cart myCart = cart.get();

而应该:

1
2
3
4
5
6
java复制代码if (cart.isPresent()) {
Cart myCart = cart.get();
...
} else {
...
}

值为空时,通过 orElse() 方法返回默认对象

这种方式比isPresent()-get()方式更加优雅。但是这里有点小问题,就是即使Optional有值时,也会执行orElse()方法,并且需要构建一个对象,所以性能上有一点小小的损耗。

避免以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public static final String USER_STATUS = "UNKNOWN";
...
public String findUserStatus(long id) {

Optional<String> status = ... ; // 可能返回一个空的Optional

if (status.isPresent()) {
return status.get();
} else {
return USER_STATUS;
}
}

而应该:

1
2
3
4
5
6
7
8
java复制代码public static final String USER_STATUS = "UNKNOWN";

public String findUserStatus(long id) {

Optional<String> status = ... ; // 可能返回一个空的Optional

return status.orElse(USER_STATUS);
}

值为空时,通过 orElseGet() 方法返回默认对象

orElseGet()是对isPresent()-get()的另一种优雅替代方案,同时比orElse() 来说,要更加高效,因为orElseGet()方法的参数是Java8中的Supplier,只有Optional中的值为空时,才会执行Supplier的get()方法,相对`orElse()没有性能损失。

避免以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public String computeStatus() {
...
}

public String findUserStatus(long id) {

Optional<String> status = ... ;

if (status.isPresent()) {
return status.get();
} else {
return computeStatus();
}
}

同样避免:

1
2
3
4
5
6
7
8
9
java复制代码public String computeStatus() {
...
}

public String findUserStatus(long id) {
Optional<String> status = ... ;
// computeStatus() 在status不为空时也会调用
return status.orElse(computeStatus());
}

而应该:

1
2
3
4
5
6
7
8
9
java复制代码public String computeStatus() {
... // some code used to compute status
}

public String findUserStatus(long id) {
Optional<String> status = ... ;
// computeStatus()只有在status为空时调用
return status.orElseGet(this::computeStatus);
}

总结

乍一看Optional好像挺简单的,但是要想正确使用并不容易,OptionalAPI主要用于返回类型,清晰表达返回值中没有结果的可能性。

  • 不要过度使用Optional
  • Optional尽量作为方法的返回值使用
  • 获取之前先判断是否有值
  • 适当使用Optional API

一些不恰当的使用可能导致在某些情况下出现BUG,建议收藏本文,使用Optional时进行参考。

抽奖说明

  • 本活动由掘金官方支持 详情可见掘力星计划。
  • 欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章。
  • 与文章内容相关的评论、建议、讨论等,「踩踩」「学习了」等泛泛类的评论无法获奖

讨论话题都给大家准备好了,思考一下,下图中的这几个人有什么问题,以及最后提交的代码是否有BUG,可以在留言区讨论。

本文转载自: 掘金

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

原来服务端的退出姿势也可以这么优雅

发表于 2021-10-13

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

相信写过 golang 的 xdm 都写过 http 服务器吧,用 golang 写服务器是不是很爽呀

最简单的 http 服务端

咱们来写一个简单的 http 服务器

1
2
3
4
5
6
7
8
9
10
11
go复制代码func main() {
srvMux := http.NewServeMux()
srvMux.HandleFunc("/getinfo", getinfo)
http.ListenAndServe(":9090", srvMux)

}

func getinfo(w http.ResponseWriter, r *http.Request) {
fmt.Println("i am xiaomotong!!!")
w.Write([]byte("you are access right!!\n"))
}

这个功能非常简单,就是监听了本地的 9090 端口,并且其中有一个 url 是会处理请求的,/getinfo ,咱们可以通过如下指令来请求一下看看效果

1
2
shell复制代码# curl localhost:9090/getinfo
you are access right!!

明确是可以正常访问的,且也会拿到我们对应的信息,服务器的日志也是正常的

咱们思考一下,这个时候如果遇到了意外,程序崩溃了,panic 了,或者我们认为的 kill 掉了,我们如何判断服务端是如何退出的呢?

加入 信号的 服务端

我们写 C/C++ 的时候对于信号应该不陌生吧,在 golang 里面,我们也加入信号来识别是否是认为 kill 程序的

linux 里面可以通过 man kill 查看 kill 指令的详细说明

这里我们可以看到一个kill -9 是对应的 SIGKILL 信号 ,我们还知道 SIGINT 信号是 Ctrl-C 的时候会发出这个信号,也是一个中断信号,如果对于这点不清楚话,可以网络上搜索一下 linux 信号列表

1
2
3
4
5
6
7
8
9
10
go复制代码func main() {
sig := make(chan os.Signal)
signal.Notify(sig, syscall.SIGINT, syscall.SIGKILL)

srvMux := http.NewServeMux()
srvMux.HandleFunc("/getinfo", getinfo)
go http.ListenAndServe(":9090", srvMux)

fmt.Println(<-sig)
}

http.ListenAndServe 是阻塞的,则此处咱们监听 9090 的服务是开了一个单独处理

验证一下

1
2
shell复制代码# go run main.go
^Cinterrupt

这个时候,我们的 http 服务器,已经能够区分信号了,知道自己是如何退出的了

咱们的需求有慢慢的增加,实际工作中,肯定不能做的这么 cuo

优雅的退出

工作中,我们带有 http 的服务端,肯定还有别的处理逻辑,例如读写文件,GRPC 通信,或者是使用数据库,那么我们程序关闭情况,还是要根据情况来处理,要遵循原子性

有如下 2 种情况:

  • 对于数据没有严格的质量要求,程序 panic 也无所谓,那么这个时候不用优雅关闭也没有啥问题
  • 对于上述说到的会操作数据库,读写文件等等会修改数据的,这里可不期望操作数据的过程中被中断,我们要遵循原子性,咱们的程序需要提供一个缓冲的时间,来优雅的退出

正常工作中退出必须是优雅的

如何实现优雅退出呢?

例如上面的例子,当主协程收到了中断信号后,就会马上退出程序,子协程也会相应退出

如果需要主协程等待子协程处理完当前手里的活再退出,那么我们是不是需要让主协程和子协程相互通信,才有可能实现呢?

使用 2 个 channel 来实现优雅关闭

这个方法比较容易想到

实现大体分为 2 步走:

  • 主协程收到中断信号后,通知子协程优雅关闭 ,这里命名为 stopCh
  • 子协程收到通知后,处理完手头的通知主协程关闭程序,这里命名为 closeCh
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
go复制代码func main() {
sig := make(chan os.Signal)
signal.Notify(sig, syscall.SIGINT, syscall.SIGKILL)

stopCh := make(chan int)
closeCh := make(chan int)

srvMux := http.NewServeMux()
srvMux.HandleFunc("/getinfo", getinfo)
go http.ListenAndServe(":19999", srvMux)

go func(stopCh, closeCh chan int) {
for {
select {
case <-stopCh:
fmt.Println(" processing tasks")
// 模拟正在处理数据
time.Sleep(time.Second*time.Duration(2))
fmt.Println("process over ")
closeCh <- 1
}
}
}(stopCh, closeCh)

<-sig
stopCh <- 1
<-closeCh
fmt.Println("close server ")
}

此处我们可以看出使用了 2 个通道来让主协程和子协程相互通信

开辟一个协程,执行匿名函数来监听 stopCh 通道是否有数据,若有数据,说明主协程收到了信号,并且通知子协程要优雅关闭了

这个时候,子协程做完自己的事情,就在 closeCh 写入数据,通知主协程可以正常关闭程序了

使用嵌套的 channel 来实现

使用 嵌套的 channel 来实现优雅关闭,可能一下子还想不到,不过官网有给我们一些方向

实现思路是:

  • 使用一个通道 stopCh,通道 stopCh 里面的元素是另外一个通道 tmpCh
  • 当主协程收到退出信号时,在 stopCh 中写入数据 tmpCh,并开始监听 tmpCh 是否有数据
  • 子协程从 stopCh 读取到数据 tmpCh 时,便知道自己需要优雅关闭了,处理完自己的事情之后,子协程往 tmpCh 写入数据
  • 主协程监听到 tmpCh 有数据,则退出程序
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
shell复制代码func main() {
sig := make(chan os.Signal)
signal.Notify(sig, syscall.SIGINT, syscall.SIGKILL)

stopCh := make(chan chan struct{})

srvMux := http.NewServeMux()
srvMux.HandleFunc("/getinfo", getinfo)
go http.ListenAndServe(":19999", srvMux)

go func(stopCh chan chan struct{}) {
for {
select {
case tmpCh:=<-stopCh:
fmt.Println(" processing tasks")
time.Sleep(time.Second*time.Duration(2))
fmt.Println("process over ")
tmpCh <- struct{}{}
}
}
}(stopCh)

tmpCh := make(chan struct{})

<-sig
stopCh <- tmpCh
<-tmpCh
fmt.Println("close server ")
}

上面 2 种方法都比较类似,都是使用通道来实现优雅关闭的功能,通道是 golang 天生的数据结构,咱们要用起来

使用 golang 标准解法 context

使用 golang 的 context ,能够更好的实现优雅关闭的问题

别以为 context 只会拿来传递数据,context 也是可以控制 子协程的生命周期的

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
go复制代码func main() {
sig := make(chan os.Signal)
signal.Notify(sig, syscall.SIGINT, syscall.SIGKILL)

stopCh := make(chan struct{})
// 创建一个上下文
ctx,cancle := context.WithCancel(context.Background())

srvMux := http.NewServeMux()
srvMux.HandleFunc("/getinfo", getinfo)
go http.ListenAndServe(":19999", srvMux)

go func(ctx context.Context,stopCh chan struct{}) {
for {
select {
case <-ctx.Done():
fmt.Println(" processing tasks")
time.Sleep(time.Second*time.Duration(2))
fmt.Println("process over ")
stopCh <- struct{}{}
}
}
}(ctx,stopCh)

<-sig
cancle()
<-stopCh
fmt.Println("close server ")
}

此处我们使用 context 的方式,当主协程关闭上下文的时候,子协程就会从通道到读取到数据,进而进行优雅关闭,我们可以看到源码,ctx.Done() 的返回值也是一个通道

主协程等待所有子协程优雅关闭实现方法

上面我们说到的都是主协程等待 1 个子协程优雅关闭后,自己关闭程序

那么实际工作中肯定是不止一个协程的,咱们要做的优雅,那就优雅到底 ,此处我们的处理方式是 golang 中 context + sync.WaitGroup 的方式来实现

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
go复制代码func main() {
sig := make(chan os.Signal)
signal.Notify(sig, syscall.SIGINT, syscall.SIGKILL)
ctx, cancle := context.WithCancel(context.Background())

mywg := sync.WaitGroup{}
// 控制 5 个子协程的声明周期
mywg.Add(5)

for i := 0; i < 5; i++ {
go func(ctx context.Context) {
defer mywg.Done()
for {
select {
case <-ctx.Done():
fmt.Println(" processing tasks")
time.Sleep(time.Second * time.Duration(1))
fmt.Println("process over ")
return

default:
time.Sleep(time.Second * time.Duration(1))
}
}
}(ctx)
}

<-sig
cancle()
// 等待所有的子协程都优雅关闭
mywg.Wait()
fmt.Println("close server ")
}

上述代码中,我们使用 sync.WaitGroup 控制 5 个子协程的生命周期,当主协程收到中断信号后,cancle() 掉 ctx

每一个子协程都能从 ctx.Done() 读取到数据,自行处理完毕手中事情后

最终 defer mywg.Done() ,主协程 mywg.Wait() 等待所有协程都优雅关闭后,自己也关闭了自己的程序

验证效果

1
2
3
4
5
6
7
8
9
10
11
12
shell复制代码# go run main.go
^C processing tasks
processing tasks
processing tasks
processing tasks
processing tasks
process over
process over
process over
process over
process over
close server

以上就是从一个不会优雅关闭到学会常用优雅关闭方法的简单路径,希望对你有用哦

欢迎点赞,关注,收藏

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

好了,本次就到这里

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

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

本文转载自: 掘金

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

成为女神同桌,我只用一行代码

发表于 2021-10-13

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

前言

我来自一个普普通通的软件开发班级,但是我们班与众不同,因为我们班级里面有5个女生,是的,5个,我骄傲,而且班级里面这5个女生还有我们的系花,我更自豪,为了让她们,哦不,让她注意到我,我要向他证明我的技术。
在会技术的女孩子面前表现自己技术高超,肯定能吸引到她。

故事起源于我走过她的身边,我看到她竟然在看B站视频,而且听到她说,“网好卡~”。

我机会来了,表现的机会啊,我如果能将B站视频下载到本地,一定能让她用崇拜的眼神多看我一眼,想到这里,我甚至都把结婚地点选择好了……

快跑10步,回到自己的电脑前,一顿操作,机械掉色的键盘被我敲的噼啪乱响,我要在最短的时间内把她刚才看的视频下载到本地,没错,最短的时间,因为刚才走过她身边的不止我自己。

开机,打开浏览器,打开百度,输入 如何将B站视频下载到本地(不要见笑,太真实了,我也不会啊),emmm…

python  you-get

额,打开方式不对,说好了,高手呢,好吧,加上一个 python,至于为什么要加上一个Python,那是因为,我们班是学Python的,其他语言女神不会,接下来,我在打开的网页中捕获到一个关键词 you-get,我想这应该就是我接下来攻克的方向了

python  you-get

编码环节

经过多年打游戏环节的练习,我已经及其熟练在抓取到电脑上的关键字了,没错,这个you-get就是今天的主角了

先检索一下you-get的使用说明书

找到它的官网 https://github.com/soimort/you-get/ 非常好,开源的,代码我虽然写不出来,但是复制,粘贴,修改,运行 这套动作我非常熟悉

python  you-get

这个Dumb 貌似有蠢的意思呢?难道是网页傻瓜下载器,这个名字… … 非常符合我的气质,哦不,要求

在继续翻阅一下手册

You-Get is a tiny command-line utility to download media contents (videos, audios, images) from the Web, in case there is no other handy way to do it.
Here’s how you use you-get to download a video from YouTube:

大概意思就是:我可以在控制台运行,我能下载视频,音频,图片,下面我举一个我在Youtube下载的例子,没错,我的英语非常好

翻看中看到这个表情包,让我坚定的认为这个开发大佬是个自己人

python  you-get

you-get 插件的环境要求

  • Python 3.2 以上
  • FFmpeg 1.0 or above

安装方式

对于咱们来说,建议你切换到国内的源,例如

  • 阿里云:mirrors.aliyun.com/pypi/simple…
  • 中国科技大学:pypi.mirrors.ustc.edu.cn/simple/
  • 清华大学:pypi.tuna.tsinghua.edu.cn/simple/

安装命令(啥?不知道在哪里输入这行代码)

1
py复制代码pip3 install you-get

使用方式

果然一行代码就可以搞定,安装完毕,打开控制台,这里我选择是B站的一个任意视频

1
python复制代码you-get 'https://www.bilibili.com/video/BV17f4y127YZ'

回车等了那么一小会,就下载下来了,果然==我的代码==写的就是好(ლ(′◉❥◉`ლ))

在这里插入图片描述

λ you-get “www.bilibili.com/video/BV17f…“
site: Bilibili
title: 所有人!请把膝盖准备好–翻译界女神张璐的古诗词汉英口译课来了!(记者会现场口译音频混剪)
stream:

  • format: flv
    container: flv
    quality: 高清 1080P
    size: 11.6 MiB (12156919 bytes)

    download-with: you-get –format=flv [URL]

    Downloading 所有人!请把膝盖准备好–翻译界女神张璐的古诗词汉英口译课来了!(记者会现场口译音频混剪).flv …

34.5% ( 4.0/ 11.6MB) ├██████████████──────────────────────────┤[1/1] 82 kB/s
D:\python100\venv\Scripts

翻看文件夹发现有两个文件 ,一个我知道是视频,另一个XML文件是啥,赶紧打开看看
在这里插入图片描述

在这里插入图片描述

哦,我太厉害了,弹幕,弹幕唉,我把弹幕也爬取下来了。

炫技环节

打开班级群,找到女神头像,双击,发送问候语,接下来,我就能做到女神旁边,给她慢慢去解说这一行代码的故事呢。
python  you-get

在这里插入图片描述

尾声

该插件涉及的网站非常多,具体可以在github页看到,我下面有一个简单的截图,你都可以去尝试一下,you-get作为一个出色的视频下载模块,必须给点99个赞。

python  you-get

很多时候,我们无法解决技术问题,不是因为我们技术不行,而是因为我们不知道怎么才行

本篇文章希望你从今天可以听说竟然有一款工具叫做you-get。
博主ID:梦想橡皮擦,==希望大家点赞、评论、收藏==

听说评论、点赞、收藏博主的人,都拿到大公司的OFFER了呢

本文转载自: 掘金

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

MySQL 主从复制 复制的基本原则 一主一从常见配置 参考

发表于 2021-10-13

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

复制的基本原则

slave 会从 master 读取 binlog 来进行数据同步

MySQL复制过程分为三步:

  1. master 将改变记录到二进制日志 (binary log). 这些记录过程叫做 二进制日志时间,
  2. slave 将 master 的 binary log events 拷贝到它的中继日志(relay log);
  3. slave 重做中继日志中的时间,将改变应用到自己的数据库中。MySQL 复制是一步的且串行化的。

复制的基本原则

每个 slave 只有一个 master
每个 slave 只能有一个唯一的服务器ID
每个 master 可以有多个 slave

复制的最大问题

延迟

一主一从常见配置

mysql 版本一致且后台以服务运行
主从都配置在 [mysqld] 节点下,都是小写

主库修改 my.ini 配置文件

  1. [必须] 主机服务器唯一 ID
  • server-id=1
  1. [必须] 启用二进制日志
  • log-bin = 自己的本地的路径/mysqlbin
  • log-bin=D:/devsoft/mysql5.7/data/mysqlbin

3 [可选] 启用错误日志

  • log-err = 自己的本地的路径/mysqlerr
  • log-err=D:/devsoft/mysql5.7/data/mysqlerr
  1. [可选] 根目录
  • basedir = “自己本地路径”
  • basedir = “D:/devsoft/mysql5.7/“
  1. [可选] 临时目录
  • tempdir = “自己本地路径”
  • tempdir = “D:/devsoft/mysql5.7/“
  1. [可选] 数据目录
  • datadir = “自己本地路径/data”
  • datadir = “D:/devsoft/mysql5.7/data”
  1. read-only = 0
  • 主机读写都可以
  1. [可选] 设置不要复制的数据库
  • binlog-ignore-db=mysq
  1. [可选]设置需要复制的数据库
  • binlog-do-db=需要复制的主数据库名字

从库修改 my.conf 配置文件

  1. [必须] 主机服务器唯一 ID
  2. [可选] 启用二进制日志

因修改过配置文件,请主机 + 从机都重启后台 mysql 服务
主机从机关闭防火墙
windows 手动关闭

1
shell复制代码liunx  service iptables stop

在 Windows 及其上建立账户并授权 slave

1
2
shell复制代码grant replication slave on *.* to 'zhangsan'@'从库IP' identified by '123456';
flush privileges;

查询 master 状态
show master status;

记录 File 和 Postition 值

执行完此步骤后再操作数主服务器 MySQL , 防止主服务器状态值变化

在 LIunx 上配置需要复制的主机

1
2
3
4
shell复制代码CHANGE MASTER  TO  MASTER_HOST='你的 IP'
MASTER_USER=‘zhangsan’
MASTER_PASSWORD='123456'
MASTER_LOG_FILE='mysqlbin.具体数字', MASTER_LOG_POS=具体值;


启动从服务其复制功能

1
shell复制代码start slave

show slave status\G

  • Slave_IO_Running: Yes
  • Slave_IO_Running: NO
  • 如果上面的两个参数都是 : Yes 说明配置成功!!

主机新建库、新建表、insert 记录、从机复制
如何重启从服务器的复制

1
shell复制代码stop slave

参考资料

  • mysql.com
  • www.processon.com/view/link/5…
  • www.cnblogs.com/gl-develope…
  • [www.cnblogs.com/dw3306/p/13…] (www.cnblogs.com/dw3306/p/13…)
  • blog.51cto.com/369369/7909…
  • [blog.csdn.net/weixin_2916…] (blog.csdn.net/weixin_2916…)

「欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章」。

本文转载自: 掘金

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

MyBatis自定义TypeHandler MyBatis自

发表于 2021-10-13

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

MyBatis自定义TypeHandler

1 什么是TypeHandler

TypeHandler根据字面意思即为类型处理器

引用官方文档的描述: MyBatis在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型

MyBatis存在一些默认的类型处理器, 可参考官方文档

2 为什么要使用TypeHandler

在开发过程中, 当默认的TypeHandler无法满足需求时, 例如遇到MyBatis不支持的数据类型或需要特殊处理的类型转换, 便需要自己定制对应的TypeHandler

笔者会在下面的代码实现中完成如下几种情况的TypeHandler:

  1. 逗号分隔保存在数据库中的数据, 在对应的Java类中为数组
  2. 自定义枚举

3 如何自定义TypeHandler

MyBatis提供了接口org.apache.ibatis.type.TypeHandler和类org.apache.ibatis.type.BaseTypeHandler

官方文档给出的示例为继承BaseTypeHandler, 笔者在这里也使用这种方式

先来观察一下官方的StringTypeHandler:

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 StringTypeHandler extends BaseTypeHandler<String> {
​
 @Override
 public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
     throws SQLException {
   ps.setString(i, parameter);
}
​
 @Override
 public String getNullableResult(ResultSet rs, String columnName)
     throws SQLException {
   return rs.getString(columnName);
}
​
 @Override
 public String getNullableResult(ResultSet rs, int columnIndex)
     throws SQLException {
   return rs.getString(columnIndex);
}
​
 @Override
 public String getNullableResult(CallableStatement cs, int columnIndex)
     throws SQLException {
   return cs.getString(columnIndex);
}
   
}

方法名称和代码都简洁明了, 观察可知, 只需要完成四个方法的覆盖, 即可实现自定义TypeHandler

3.1 逗号分隔字符串转数组

假设用户表t_user设计如下:

id username tags
1 admin admin, user

对应的Java类为:

1
2
3
4
5
6
7
8
java复制代码@Data
@NoArgsConstructor
@SuperBuilder(toBuilder = true)
public class User {
   private String id;
   private String username;
   private String[] tags;
}

tags属性在数据库中用逗号分隔的字符串保存, 但User类对应的属性为String数组

可以创建StringArrayTypeHandler来解决类型转换的问题:

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
java复制代码public class StringArrayTypeHandler extends BaseTypeHandler<String[]> {
​
 @Override
 public void setNonNullParameter(PreparedStatement preparedStatement, int i, String[] strings, JdbcType jdbcType)
   throws SQLException {
   preparedStatement.setString(i, StringUtils.join(strings, ","));
}
​
 @Override
 public String[] getNullableResult(ResultSet resultSet, String s) throws SQLException {
   return convert(resultSet.getString(s));
}
​
 @Override
 public String[] getNullableResult(ResultSet resultSet, int i) throws SQLException {
   return convert(resultSet.getString(i));
}
​
 @Override
 public String[] getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
   return convert(callableStatement.getString(i));
}
​
 /**
  * 将查询值转换为数组
  *
  * @param value 查询值, String
  * @return 转换结果, String[]
  */
 private String[] convert(String value) {
   return StringUtils.isEmpty(value) ? new String[0] : value.split(",");
}
​
}

3.2 自定义枚举

如何创建包含中文名称的枚举, 可以参考MyBatis中使用Java类与枚举

先创建工具类用于根据code获取枚举实体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class ValueNameEnumUtils {
​
 private ValueNameEnumUtils() {
}
​
 public static <E extends ValueNameEnum> E valueOf(Class<E> enumClass, int value) {
     E[] enumConstants = enumClass.getEnumConstants();
     for (E e : enumConstants) {
       if (e.getValue() == value) {
         return e;
      }
    }
     return null;
}
 
}

和3.1中的情况不同, 枚举的具体类型是不确定, 所以我们要使用泛型的方式处理TypeHandler

创建ValueNameEnumTypeHandler:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class ValueNameEnumTypeHandler<E extends ValueNameEnum> extends BaseTypeHandler<ValueNameEnum> {
​
 private final Class<E> type;
​
 public ValueNameEnumTypeHandler(Class<E> type) {
   if (type == null) {
     throw new IllegalArgumentException("Type argument cannot be null");
  }
   this.type = type;
}
 
}

泛型虽然名之为泛, 但在编译过程中实际会发生类型擦除

关于类型擦除可以阅读面试官:说说什么是泛型的类型擦除?

总之, 对于泛型TypeHandler, 我们需要声明一个用来标识具体类型的属性private final Class<E> type和创建对应的构造函数public ValueNameEnumTypeHandler(Class<E> type)

接下来和3.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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码public class ValueNameEnumTypeHandler<E extends ValueNameEnum> extends BaseTypeHandler<ValueNameEnum> {
​
 private final Class<E> type;
​
 public ValueNameEnumTypeHandler(Class<E> type) {
   if (type == null) {
     throw new IllegalArgumentException("Type argument cannot be null");
  }
   this.type = type;
}
​
 @Override
 public void setNonNullParameter(PreparedStatement ps, int i, ValueNameEnum parameter, JdbcType jdbcType)
   throws SQLException {
   ps.setInt(i, parameter.getValue());
}
​
 @Override
 public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
   int code = rs.getInt(columnName);
   return rs.wasNull() ? null : valueOf(code);
}
​
 @Override
 public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
   int code = rs.getInt(columnIndex);
   return rs.wasNull() ? null : valueOf(code);
}
​
 @Override
 public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
   int code = cs.getInt(columnIndex);
   return cs.wasNull() ? null : valueOf(code);
}
​
 /**
  * 根据枚举值返回枚举示例
  *
  * @param code 枚举值
  * @return 枚举实例
  */
 private E valueOf(int code) {
   try {
     return ValueNameEnumUtils.valueOf(type, code);
  } catch (Exception ex) {
     throw new IllegalArgumentException(
       "Cannot convert " + code + " to " + type.getSimpleName() + " by code value.", ex);
  }
}
   
}

完成上述代码直接启动, 会抛出异常: Unable to find a usable constructor for class cn.houtaroy.springboot.common.mybatis.handler.ValueNameEnumTypeHandler

产生异常的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
   // 未指定JavaType, 此处为false
   if (javaTypeClass != null) {
     try {
       Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
       return (TypeHandler<T>) c.newInstance(javaTypeClass);
    } catch (NoSuchMethodException ignored) {
       // ignored
    } catch (Exception e) {
       throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
    }
  }
   try {
     // 此处抛出异常
     Constructor<?> c = typeHandlerClass.getConstructor();
     return (TypeHandler<T>) c.newInstance();
  } catch (Exception e) {
     throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
  }
}

报错的原因直白, 没有找到ValueNameEnumTypeHandler的构造函数

首先我们要了解下Java类构造函数的机制: 如果定义了构造函数, 则使用定义, 否则默认生成空构造函数

在3.1中的StringArrayTypeHandler, 我们没有定义构造函数, 自动生成空构造函数, typeHandlerClass.getConstructor()不会抛出异常

但ValueNameEnumTypeHandler定义了一个构造函数ValueNameEnumTypeHandler(Class<E> type), 且没有指定JavaType, typeHandlerClass.getConstructor()自然抛出异常

解决方法有两种:

  1. 创造空的构造函数
  2. 指定JavaType

笔者推荐第二种, 因为第一种方式枚举类属性type会产生NPE(空指针异常), MyBatis官方也我们提供了注解@MappedTypes用于指定JavaType:

1
2
3
4
java复制代码@MappedTypes(ValueNameEnum.class)
public class ValueNameEnumTypeHandler<E extends ValueNameEnum> extends BaseTypeHandler<ValueNameEnum> {
   //...
}

4 如何使用TypeHandler

在上一章节中, 我们完成了编码实现自定义TypeHandler, 但完成的TypeHandler还没办法进行使用, 需要手动进行配置

有两种配置方式: 局部使用和全局使用

以StringArrayTypeHandler举例:

4.1 局部使用

在ResultMap中使用:

1
2
3
4
xml复制代码<resultMap id="UserResultMap" type="cn.houtaroy.springboot.common.system.model.User">
   <id column="id" property="id"/>
<result column="tags" property="tags" typeHandler="cn.houtaroy.springboot.extension.mybatis.handler.StringArrayTypeHandler"/>
</resultMap>

在语句中使用:

1
sql复制代码update t_user set tags = #{tags, typeHandler=cn.houtaroy.springboot.extension.mybatis.handler.StringArrayTypeHandler}

4.2 全局使用

使用配置文件指定handler包名:

1
2
yaml复制代码mybatis:
type-handlers-package: cn.houtaroy.springboot.extension.mybatis.handler

注意, 此配置类型为String, 只能配置一个包, 推荐使用下面的方式

手写配置类:

1
2
3
4
5
6
7
8
9
java复制代码@Configuration
public class MybatisTypeHandlerConfiguration {
 
 @Bean
 ConfigurationCustomizer typeHandlerRegistry() {
   return configuration -> configuration.getTypeHandlerRegistry().register(ValueNameEnumTypeHandler.class);
}
 
}

StringArrayTypeHandler不适合全局配置, 它会在全部JavaType为String[]的属性上使用

5 拓展阅读

  • MyBatis3官方文档中TypeHandler内容: mybatis – MyBatis 3 | 配置
  • 网上搜索的在Spring Bean声明周期中进行全局配置: Mybatis自定义全局TypeHander_chuobenggu7592的博客-CSDN博客

本文转载自: 掘金

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

快速排序的实现 引

发表于 2021-10-13

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

引

说到快速排序,可能大家都不会陌生。今天的算法课程中,学到了快速排序的两种中间实现步骤,给大家分享一下。

1.快速排序的思想

首先还是过一遍快速排序的基本思想。

用我自己的话总结一下,就是:

  1. 在比较序列中取关键值key
  2. 将序列中比key值大的放在key的一侧,比key值小的放在另一则
  3. 分别对左右两侧重复进行1、2操作

从步骤中我们能够清晰地明白这是一种分而治之的思想,因此在代码中的体现就是使用递归。

2.对于步骤2的解析

作为算法的主要部分,我们需要知道如何实现步骤2,即将大的放在一边,小的放在另一边。

从字面上来看,这个步骤似乎实现起来并不难,但我们的算法依然有许多考究之处。

v2-d4e5d0a778dba725091d8317e6bac939_b.gif

[图片来源](【算法】排序算法之快速排序 - 知乎 (zhihu.com))

2.1

左右分开找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码int partition(int arr[], int low, int high){
int key;
key = arr[low];
while(low<high){
while(low <high && arr[high]>= key )
high--;
if(low<high)
arr[low++] = arr[high];
while( low<high && arr[low]<=key )
low++;
if(low<high)
arr[high--] = arr[low];
}
arr[low] = key;
return low;
}

具体思路参考下图

20190320151833851.gif
图片来源

2.2

从一端“过滤”

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
js复制代码

int partition(int arr[], int low, int high){
int i=high;
int m=high;
int key =arr[high];

while(i>=low){
if(arr[i]>key){
int temp=arr[m];
arr[i]=arr[m];
arr[m]=temp;
m--;
}
i--;
}
int temp=arr[high];
arr[high]=arr[m+1];
arr[m+1]=temp;
printf("\n%d",m);
return m+1;
}

本文转载自: 掘金

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

Serverless 工程实践 零基础上手 Knativ

发表于 2021-10-13

简介: Knative 是一款基于 Kubernetes 的 Serverless 框架。其目标是制定云原生、跨平台的 Serverless 编排标准。

Knative 介绍

Knative 通过整合容器构建(或者函数)、工作负载管理(动态扩缩)以及事件模型这三者实现其 Serverless 标准。

在 Knative 体系架构下,各角色的协作关系如下图所示。

  • 开发者是指 Serverless 服务的开发人员可以直接使用原生 Kubernetes API 基于 Knative 部署 Serverless 服务。
  • 贡献者主要是指社区的贡献者。
  • Knative 可以被集成到支持的环境中,例如云厂商或者企业内部。目前,Knative 是基于Kubernetes来实现的,所以可以认为有 Kubernetes 的地方就可以部署 Knative。
  • 用户指终端用户,其通过Istio网关访问服务或者事件系统触发 Knative 中的 Serverless 服务。
  • 作为一个通用的 Serverless 框架,Knative 由 3 个核心组件组成。
  • Tekton:提供从源码到镜像的通用构建能力。Tekton 组件主要负责从代码仓库获取源码并编译成镜像,推送到镜像仓库。所有这些操作都是在 Kubernetes Pod 中进行的。
  • Eventing:提供事件的接入、触发等一整套事件管理能力。Eventing 组件针对 Serverless 事件驱动模式做了一套完整的设计,包括外部事件源的接入、事件注册、订阅以及事件过滤等功能。事件模型可以有效地解耦生产者和消费者的依赖关系。生产者可以在消费者启动之前生成事件,消费者也可以在生产者启动之前监听事件。

在 Knative 体系架构下各角色的协作关系

  • Serving:管理 Serverless 工作负载,可以和事件很好地结合,并且提供了基于请求驱动的自动伸缩能力,而且在没有服务需要处理的时候可以缩容到零。Serving 组件的职责是管理工作负载以对外提供服务。Serving 组件最重要的特性就是自动伸缩的能力。目前,其伸缩边界无限制。Serving 还具有灰度发布能力。

Knative 部署

本文将会以在阿里云部署 Kantive 服务为例,详细说明如何部署 Knative 相关服务。首先,登录到容器服务管理控制台,如图所示。

阿里云容器服务管理控制台

如没有集群,可以先选择创建集群,如下图所示。

配置与创建集群

创建集群比较缓慢,耐心等待集群创建完成,成功之后如图所示。

集群创建成功示意图

进入集群之后,选择左侧的“应用”,找到 “Knative” 并点击“一键部署”,如图所示。

创建 Knative 应用

稍等片刻,Knative 安装完成之后,可以看到核心组件已经处于“已部署”状态,如图所示。

Knative 应用部署完成

至此,我们完成了 Knative 的部署。

体验测试

首先需要创建一个 EIP,并将其绑定到 API Server 服务上,如下图所示。

图为 API Server 绑定 EIP

完成之后,进行 Serverless 应用的测试。选择应用中的 “Kantive 应用”,并且在服务管理中选择“使用模板创建”,如图所示。

快速创建示例应用

创建完成之后,可以看到控制台已经出现一个 Serverless 应用,如图所示。

示例应用创建成功

此时,我们可以点击应用名称查看该应用的详情,如下图所示。

查看示例应用详情

为了便于测试,可以在本地设置 Host:

101.200.87.158 helloworld-go.default.example.com

设置完成之后,在浏览器中打开系统分配的域名,可以看到已经输出预期的结果,如图所示。

浏览器测试示例应用

至此,我们完成了一个基于 Knative 的 Serverless 应用的部署和测试。

此时,我们还可以通过 CloudShell 进行集群的管理等。在集群列表页面,选择通过 CloudShell 进行管理,如图所示。

集群管理列表

通过 CloudShell 管理已创建的集群,如图所示。

CloudShell 窗口

执行指令:

kubectl get knative

可以看到,刚部署的 Knative 应用,如图所示。

CloudShell 查看 Knative 应用

关于作者:刘宇(江昱)国防科技大学电子信息专业在读博士,阿里云 Serverless 产品经理,阿里云 Serverless 云布道师,CIO 学院特聘讲师。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

Java中对象的比较让你的Coding更上一层楼 一:问题引

发表于 2021-10-13

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。 @[TOC]

一:问题引出

在使用优先级队列(堆)的时候,优先级队列对插入的元素要求必须是可以互相进行比较的元素,并且不能是null。
那么怎么比较自定义类的对象呢?


@TOC

二:Java中比较的几种方式


在这里插入图片描述

2.1 在Java中基本类型可以直接进行比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class TestCompare {
public static void main(String[] args) {
int a = 10;
int b = 20;
System.out.println(a > b);
System.out.println(a < b);
System.out.println(a == b);
char c1 = 'A';
char c2 = 'B';
System.out.println(c1 > c2);
System.out.println(c1 < c2);
System.out.println(c1 == c2);
boolean b1 = true;
boolean b2 = false;
System.out.println(b1 == b2);
System.out.println(b1 != b2);
}
}

输出结果:
在这里插入图片描述

对于基本类型,可以直接用’=’,’>’,‘<’等其他符号比较,返回true,或者是false。

2.2 对象的比较

1.引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
}
public class TestPriorityQueue {
public static void main(String[] args) {
Card c1 = new Card(1, "♠");
Card c2 = new Card(2, "♠");
Card c3 = c1;
//System.out.println(c1 > c2); // 编译报错
System.out.println(c1 == c2); // 编译成功 ----> 打印false,因为c1和c2指向的是不 同对象
//System.out.println(c1 < c2); // 编译报错
System.out.println(c1 == c3); // 编译成功 ----> 打印true,因为c1和c3指向的是同一个对象
}
}

从结果中可以看出Java中引用类型的比较不能直接按照‘<’,’>’比较。编译器会报错,那为什么‘==’可以呢?因为对于自定义的类型,都默认的继承了Object类,而Object类中提供了equal方法,而‘==’默认情况下调用的就是equal方法,equal方法比较的并不是两个变量的值的大小,而是直接比较两个引用变量的地址。

三:对象的比较

3.1 覆写基类的equal方法

在2.2中自定义类型equal方法比较的是两个变量的地址而不是大小,那要是硬要比较大小应该怎么做呢?我们可以覆写基类的equal方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Override
public boolean equals(Object o) {
// 自己和自己比较
if (this == o) {
return true;
}
// o如果是null对象,或者o不是Card的子类
if (o == null || !(o instanceof Card)) {
return false;
} /
/ 注意基本类型可以直接比较,但引用类型最好调用其equal方法
Card c = (Card)o;
return rank == c.rank
&& suit.equals(c.suit);
}
}

覆写的格式大同小异
1. 如果两个变量都指向同一个变量那么返回true;
2. 如果有传入的变量为null,那么返回false;
3. 如果传入的类型不同,(如上面的例子不是“card“),也返回false;
4. 按照类的实现目标完成比较,例如这里只要花色和数值一样,就认为是相同的牌
总结:覆写基类equal的方式虽然可以比较,但缺陷是:equal只能按照相等进行比较,不能按照大于、小于的方式进行
比较。


3.2 基于Comparble接口类的比较

对于自定义类型比较大小的话,在定义类的时候实现它的Comparble接口,然后在类中重写CompareTo方法
Comparble是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
31
32
java复制代码public interface Comparable<E> {
// 返回值:
// < 0: 表示 this 指向的对象小于 o 指向的对象
// == 0: 表示 this 指向的对象等于 o 指向的对象
// > 0: 表示 this 指向的对象等于 o 指向的对象
int compareTo(E o);
}
public class Card implements Comparable<Card> {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
} /
/ 根据数值比较,不管花色
// 这里我们认为 null 是最小的
@Override
public int compareTo(Card o) {
if (o == null) {
return 1;
} r
eturn rank - o.rank;
} p
ublic static void main(String[] args){
Card p = new Card(1, "♠");
Card q = new Card(2, "♠");
Card o = new Card(1, "♠");
System.out.println(p.compareTo(o)); // == 0,表示牌相等
System.out.println(p.compareTo(q));// < 0,表示 p 比较小
System.out.println(q.compareTo(p));// > 0,表示 q 比较大
}
}

3.3基于比较器的比较

具体步骤如下:
1:自定义比较器类,实现Comparator接口

1
2
3
4
5
6
7
java复制代码public interface Comparator<T> {
// 返回值:
// < 0: 表示 o1 指向的对象小于 o2 指向的对象
// == 0: 表示 o1 指向的对象等于 o2 指向的对象
// > 0: 表示 o1 指向的对象等于 o2 指向的对象
int compare(T o1, T o2);
}

2:覆写Comparator中的compare方法`

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
java复制代码import java.util.Comparator;
class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String
this.rank = rank;
this.suit = suit;
}
} c
lass CardComparator implements C
// 根据数值比较,不管花色
// 这里我们认为 null 是最小的
@Override
public int compare(Card o1, Card o2) {
if (o1 == o2) {
return 0;
} i
f (o1 == null) {
return -1;
} i
f (o2 == null) {
return 1;
} r
eturn o1.rank - o2.rank;
} p
ublic static void main(String[] args){
Card p = new Card(1, "♠");
Card q = new Card(2, "♠");
Card o = new Card(1, "♠");
// 定义比较器对象
CardComparator cmptor = new CardComparator();
// 使用比较器对象进行比较
System.out.println(cmptor.compare(p, o)); // == 0,表示牌相等
System.out.println(cmptor.compare(p, q)); // < 0,表示 p 比较小
System.out.println(cmptor.compare(q, p)); // > 0,表示 q 比较大
}
}

3.4 三种比较方式的对比
在这里插入图片描述

四:结尾

上面的就是java中常用的比较变量的方法,由于侵入性不同,需要跟需求选择。

本文转载自: 掘金

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

1…492493494…956

开发者博客

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