神奇的 (a == (Integer) 1 && a == (Integer) 2 && a == (Integer) 3) = true

前一段时间看了一篇文章 (a == (Integer) 1 && a == (Integer) 2 && a == (Integer) 3) 是否可以为 true,当时第一反应怎么可能,谁知道再往下看,作者竟然给出来如下代码,一运行神奇的事出现了,真的为 true,代码如下: package cn.bridgeli.demo; import java.lang.reflect.Field; public class Magic { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { Class cache = Integer.class.getDeclaredClasses()[0]; Field c = cache.getDeclaredField("cache"); c.setAccessible(true); Integer[] array = (Integer[]) c.get(cache); // array[129] is 1 array[130] = array[129]; // Set 2 to be 1 array[131] = array[129]; // Set 3 to be 1 Integer a = 1; if (a == (Integer) 1 && a == (Integer) 2 && a == (Integer) 3) { System.out.println(true); } else { System.out.println(false); } } } 因为作者没有给出解释,所以就研究了一番,发现需要基础非常扎实才能写出这段代码,这段代码之所以为 true,要理解如下几个问题: ...

October 31, 2021 · 2 min · 344 words · Bridge Li

GeoHash 算法的 Java 版实现

之前曾经做过一个类 LBS 的小需求,当时是用 redis 做的,就是这篇文章,其实 GeoHash 算法,我们也可以自己实现,具体如下: package cn.bridgeli.demo; import java.util.ArrayList; import java.util.BitSet; import java.util.HashMap; public class GeoHash { public static final double MINLAT = -90; public static final double MAXLAT = 90; public static final double MINLNG = -180; public static final double MAXLNG = 180; private static int numbits = 5 * 5; //经纬度单独编码长度 private static double minLat; private static double minLng; private final static char[] digits = {&#8216;0&#8217;, &#8216;1&#8217;, &#8216;2&#8217;, &#8216;3&#8217;, &#8216;4&#8217;, &#8216;5&#8217;, &#8216;6&#8217;, &#8216;7&#8217;, &#8216;8&#8217;, &#8216;9&#8217;, &#8216;b&#8217;, &#8216;c&#8217;, &#8216;d&#8217;, &#8216;e&#8217;, &#8216;f&#8217;, &#8216;g&#8217;, &#8216;h&#8217;, &#8216;j&#8217;, &#8216;k&#8217;, &#8216;m&#8217;, &#8216;n&#8217;, &#8216;p&#8217;, &#8216;q&#8217;, &#8216;r&#8217;, &#8216;s&#8217;, &#8216;t&#8217;, &#8216;u&#8217;, &#8216;v&#8217;, &#8216;w&#8217;, &#8216;x&#8217;, &#8216;y&#8217;, &#8216;z&#8217;}; //定义编码映射关系 final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>(); //初始化编码映射内容 static { int i = 0; for (char c : digits) { lookup.put(c, i++); } } public GeoHash() { setMinLatLng(); } public String encode(double lat, double lon) { BitSet latbits = getBits(lat, -90, 90); BitSet lonbits = getBits(lon, -180, 180); StringBuilder buffer = new StringBuilder(); for (int i = 0; i < numbits; i++) { buffer.append((lonbits.get(i)) ? &#8216;1&#8217; : &#8216;0&#8217;); buffer.append((latbits.get(i)) ? &#8216;1&#8217; : &#8216;0&#8217;); } String code = base32(Long.parseLong(buffer.toString(), 2)); //Log.i("okunu", "encode lat = " + lat + " lng = " + lon + " code = " + code); return code; } public ArrayList<String> getAroundGeoHash(double lat, double lon) { //Log.i("okunu", "getArroundGeoHash lat = " + lat + " lng = " + lon); ArrayList<String> list = new ArrayList<>(); double uplat = lat + minLat; double downLat = lat &#8211; minLat; double leftlng = lon &#8211; minLng; double rightLng = lon + minLng; String leftUp = encode(uplat, leftlng); list.add(leftUp); String leftMid = encode(lat, leftlng); list.add(leftMid); String leftDown = encode(downLat, leftlng); list.add(leftDown); String midUp = encode(uplat, lon); list.add(midUp); String midMid = encode(lat, lon); list.add(midMid); String midDown = encode(downLat, lon); list.add(midDown); String rightUp = encode(uplat, rightLng); list.add(rightUp); String rightMid = encode(lat, rightLng); list.add(rightMid); String rightDown = encode(downLat, rightLng); list.add(rightDown); //Log.i("okunu", "getArroundGeoHash list = " + list.toString()); return list; } //根据经纬度和范围,获取对应的二进制 private BitSet getBits(double lat, double floor, double ceiling) { BitSet buffer = new BitSet(numbits); for (int i = 0; i < numbits; i++) { double mid = (floor + ceiling) / 2; if (lat >= mid) { buffer.set(i); floor = mid; } else { ceiling = mid; } } return buffer; } //将经纬度合并后的二进制进行指定的32位编码 private String base32(long i) { char[] buf = new char[65]; int charPos = 64; boolean negative = (i < 0); if (!negative) { i = -i; } while (i <= -32) { buf[charPos&#8211;] = digits[(int) (-(i % 32))]; i /= 32; } buf[charPos] = digits[(int) (-i)]; if (negative) { buf[&#8211;charPos] = &#8216;-&#8216;; } return new String(buf, charPos, (65 &#8211; charPos)); } private void setMinLatLng() { minLat = MAXLAT &#8211; MINLAT; for (int i = 0; i < numbits; i++) { minLat /= 2.0; } minLng = MAXLNG &#8211; MINLNG; for (int i = 0; i < numbits; i++) { minLng /= 2.0; } } //根据二进制和范围解码 private double decode(BitSet bs, double floor, double ceiling) { double mid = 0; for (int i = 0; i < bs.length(); i++) { mid = (floor + ceiling) / 2; if (bs.get(i)) { floor = mid; } else { ceiling = mid; } } return mid; } //对编码后的字符串解码 public double[] decode(String geohash) { StringBuilder buffer = new StringBuilder(); for (char c : geohash.toCharArray()) { int i = lookup.get(c) + 32; buffer.append(Integer.toString(i, 2).substring(1)); } BitSet lonset = new BitSet(); BitSet latset = new BitSet(); //偶数位,经度 int j = 0; for (int i = 0; i < numbits * 2; i += 2) { boolean isSet = false; if (i < buffer.length()) { isSet = buffer.charAt(i) == &#8216;1&#8217;; } lonset.set(j++, isSet); } //奇数位,纬度 j = 0; for (int i = 1; i < numbits * 2; i += 2) { boolean isSet = false; if (i < buffer.length()) { isSet = buffer.charAt(i) == &#8216;1&#8217;; } latset.set(j++, isSet); } double lon = decode(lonset, -180, 180); double lat = decode(latset, -90, 90); return new double[]{lat, lon}; } public static void main(String[] args) { GeoHash geohash = new GeoHash(); String s = geohash.encode(39.923201, 116.390705); System.out.println("geohash:" + s); ArrayList<String> aroundGeoHash = geohash.getAroundGeoHash(39.923201, 116.390705); for (String s1 : aroundGeoHash) { System.out.println("aroundGeoHash:" + s1); } double[] geo = geohash.decode(s); System.out.println(geo[0] + " " + geo[1]); } } 参考:https://www.jianshu.com/p/2fd0cf12e5ba

September 25, 2021 · 4 min · 701 words · Bridge Li

Spring boot 自动装配实现的原理 – 文字简述版

当启动 Spring boot 应用程序的时候,会先创建 SpringApplication 的对象,在对象的构造方法中会进行某些参数的初始化工作,最主要的是判断当前应用程序的类型以及初始化器和监听器,在这个过程中会加载整个应用程序的 spring.factories 文件,将文件的内容放到缓存对象中,方便后续获取。 SpringApplication 对象创建完成之后,开始执行 run 方法,来完成整个启动,启动过程中最主要的有两个方法,第一个叫做 prepareContext,第二个叫做 refreshContext,在这两个关键步骤中完成了自动装配的核心功能,前面的处理逻辑包含了上下文对象的创建,banner 的打印,异常报告器的准备等各个准备工作,方便后续来进行调用。 在 prepareContext 方法中主要完成的是对上下文对象的初始化操作,包含了属性值的设置,比如环境对象,在整个过程中有一个非常重要的方法,叫做 load,load 主要完成一件事,将当前启动类作为一个 beanDefinition 注册到 registry 中,方便后续在进行 BeanFactoryPostProcessor 调用执行的时候,找到对应的主类,来完成 @SpringBootApplication、@EnableAutoConfiguration 等注解的解析工作。 在 refreshContext 方法中会进行整个容器的刷新过程,会调用 Spring 中的 refresh 方法,refresh 中有 13 个非常关键的方法,来完成整个 Spring 应用程序的启动,在自动装配过程中,会调用 invokeBeanFactoryPostProcessor 方法,在此方法中主要对 ConfigurationClassPostProcessor 类的处理,他是 BeanFactoryPostProcessor 的子类也是,BeanDefinitionRegistryPostProcessor 的子类,在调用的时候会先调用 BeanDefinitionRegistryPostProcessor 中的 postProcessBeanDefinitionRegistry 方法,然后调用 BeanFactoryPostProcessor 中的 postProcessBeanFactory 方法,在执行 postProcessBeanDefinitionRegistry 方法的时候会解析处理各种注解,包含 @PropertySource、@ComponentScan、@ComponentScans、@Bean、@Import 等注解,最主要的是 @Import 注解的解析。 在解析 @Import 注解的时候,会有一个 getImport 的方法,从主类开始递归解析注解,把所有包含 @Import 的注解都解析道,然后在 processImport 方法中对 Import 的类进行分类,此处最主要的是识别 AutoConfigurationImportSelect 归属于 ImportSelect 的子类,在后续过程中会调用 deferredImportSelectorHandler 中的 process 方法,来完善 EnableAutoConfiguration 的加载。 ...

August 29, 2021 · 1 min · 82 words · Bridge Li

好用的 IDEA 插件

好久没有好好的写过博客了,不过这一篇也没打算好好写。前一段时间换工作,所谓工欲善其事,必先利其器,所以常用的软件都需要重新配置,而作为一名 Java 程序员,最重要的就是 IDEA 了,所以这次就写一下,我个人认为比较好用的 IDEA 插件,以及他们的作用,后面如果发现更好用的插件了,也会在这篇文章里面更新。 IDE Eval Reset,开发者:zhile.io 大家众所周知的,IDEA 很好用,但是付费软件,还不便宜,但是在国内很多人都是找各种方法破解,人家也在做反破解,一直搞攻防战,但是 IDEA 有一个很人性的一点,可以试用 30 天,所以这个插件就是让大家无限试用。 安装完成之后,在 Help 菜单下面会多一个:Eval Rest 的子菜单,就可以重制 30 天的有效期,当然也可以选上右下角的:Auto reset before per restart,啥功能不用说了吧。 最后多说一句:请大家最好不要滥用此功能,最好还是支持正版。 Maven Helper,开发者:Vojtech Krasa 在我刚工作的时候的那个年代,我们 build 还用 Ant,现在可能很多人都没听说过了,后来才开始用 maven,但无论无论如何都还有一个巨大的问题,jar 包冲突,有时会出莫名其妙的问题,所以这个插件就是用来分析 maven 项目的 jar 冲突的。 装上这个插件之后,在 pom 文件左下方会多一个:Dependency Analyzer 的子菜单,点一下,就可以看到那些 jar 冲突了,然后选中,在右侧排除掉即可。 GsonFormatPlus,开发者:mars-men 在我们的工作中,使用第三方的接口,现在数据一般都是用 json,所以不可避免的要用对象和 json 的互转,而我们根据 json 写对象的时候,自己一行一行的写,不仅容易错,而且还没有效率,唯一的好处就是锻炼大家打字的说平,所以这个插件应运而生了,他可以很轻松的根据 json 数据,生成实体类。 安装完成后,我们只需要新建一个实体类,然后在该类中,摁下 option + s 键,然后把 json 数据 copy 到那个框里面,就可以直接生成相应的实体类了,巨方便快捷。 any-rule,开发者:any-rule 在我们工作中,自从有了正则表达式,我们的工作量可能减轻了很多,但很多时候写正则表达式也挺头疼的,最起码我是这样的,老是写不对。所以这个插件就是帮我们生成一些常见的正则表达式。 安装完成后,你只需要摁下 option + a,然后就可以看到一些常见的正则表达式,选择合适的即可。 ...

July 17, 2021 · 1 min · 156 words · Bridge Li

位图在 12306 中的应用

记得 12306 刚上线的时候,就在想 12306 是如何卖票,一趟车从北京到上海,中间经过了 N 个站,大家可以买其中的任意两站,而因为卖出了一个一张票,从北京到上海很多站的车票都会变动,当时就感觉这个算法太复杂了,一般人还真写不出来,由此虽然很多人都在吐槽 12306,但是我却一直任务 12306 特别牛,很多人吐槽的大学生水平肯定是做不出来的,前一段时间,听马士兵教育的周志磊老师讲课,提出 redis 中的位图解决,设计的很巧妙,突然感觉豁然开朗,如果你也有这个问题,不妨参考一下。至于什么是位图,就不多说了,如果不知道,可以简单搜索。 首先说问题,我们假设一趟车是从 A 站到 B 站,中间有 C、D、E、F、G 站,这趟车有 1、2、3、5、6 个座位,任何一个人从可以买任意一趟车的任一个座,当然被别人买过了就不行,我们都坐过车,所以规则就不多说了,直接上算法。 我们设置一个 key,例如 keyA 就代表,这趟车在 A 站的情况,所以默认情况下 keyA = 000000;同理其他站也是这个情况。假设此时有一个人甲买了从 A 站到 E 站的票 2 号靠,也就是 A、C、D 三站他是在 2 号座位上的,E 站就下车了,座位空出来了,所以此时 keyA、keyC、keyD 应该是 010000,其余还都是 000000,于此同时又来了一个人乙,他要买从 C 站到 F 站的票,那么他可以买哪些票呢?很明显除了 2 都可以买(被甲从 A 站到 E 站占了),我们怎么得到的这个结果呢?我们可以让 keyC、keyD、keyE,也就是乙要做的这三站的 key,按位做或运算,我们就可以得出结果:010000,第二位是 1,就代表座上有人我们不能买,其余的都可以买,假设他买了 4 号票,那么此时 keyA 是 010000,keyC、keyD 是 010100,keyE 是 000100,keyF、keyG、keyB 以为是 000000,所以假设此时来了一个人丙要买从 D 站到 B 站的票,我们只需要 keyD、keyE、keyF、keyG 按位或即可,得出的结果是 010100,也就是只有 2 号位(被甲在 D 到 E 的时候占了)和 4 号位(被乙从 D 站到 E 站的时候占了)不能买,其余的座位是可以随便买的,然后把相应的座位从出发站到终到站前面的一站标记成 1 即可,这样我们可以发现,我们只需要一个按位或运算,即可实现动态管理这趟车的车票,非常简单。而且位图八位才一个字节,一趟车一个车厢 118 座,按 16 个车厢算才 1888 位,236 个字节,占用内存非常少。而且 redis 原生支持位图或运算,速度也非常快。 ...

May 30, 2021 · 1 min · 100 words · Bridge Li

再谈 ThreadLocal

几年前我曾经写过两篇关于 ThreadLocal 的文章,分别是ThreadLocal类之简单理解和ThreadLocal类之简单应用示例,不过限于当时的水平,有些问题并没有说的很明白,所以今天再写一篇文章,重新说说这个类。 我们首先看一个例子: package cn.bridgeli.demo; /** * @author BridgeLi * @date 2021/4/21 11:02 */ public class User { String name = "Denny"; } 然后我们有一个操作: package cn.bridgeli.demo; import org.junit.Test; /** * @author BridgeLi * @date 2021/4/21 10:28 */ public class ThreadTest { private User user = new User(); @Test public void testThreadLocal() { new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(user.name); }).start(); new Thread(() -> user.name = "BridgeLi").start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } 这个时候我们就知道一定会有线程安全问题,所以我们怎么解决这个问题呢?就是 ThreadLocal,请看下面: ...

April 22, 2021 · 3 min · 478 words · Bridge Li

以 Java 为例简单说明常见 IO 模型

BIO 我们先看一个 Java 例子: package cn.bridgeli.demo; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; /** * @author bridgeli */ public class SocketBIO { public static void main(String[] args) throws Exception { ServerSocket server = new ServerSocket(9090, 20); System.out.println("step1: new ServerSocket(9090) "); while (true) { Socket client = server.accept(); System.out.println("step2:client: " + client.getPort()); new Thread(new Runnable() { @Override public void run() { InputStream inputStream = null; BufferedReader reader = null; try { inputStream = client.getInputStream(); reader = new BufferedReader(new InputStreamReader(inputStream)); while (true) { String dataLine = reader.readLine(); //阻塞2 if (null != dataLine) { System.out.println(dataLine); } else { client.close(); break; } } System.out.println("客户端断开"); } catch (IOException e) { e.printStackTrace(); } finally { if (null != reader) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } if (null!= inputStream) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }).start(); } } } BIO 是最初始的 IO 模型,该模型有两个大问题:1. accept 是阻塞的;2. read 也是阻塞的,也就是说我们的服务器起来之后,首先会在 accept 处阻塞,等待客户端连接,但有一个客户端连接的时候,我们可以从客户端处读取数据,这个时候也是阻塞的,所以我们的系统只能是单连接的,当有多个客户端连接的时候,只能一个一个的排着队连接,然后从客户端中读取数据,为了实现多连接,这就要求我们必须启用线程来解决,最开始等待客户端连接,然后有一个客户端连上了之后,启动一个线程读取客户端的数据,然后主线程继续等待客户端连接。 ...

March 30, 2021 · 3 min · 586 words · Bridge Li

Java 的引用类型和使用场景

每种编程语言都有自己操作内存中元素的方式,例如在 C 和 C++ 里是通过指针,而在 Java 中则是通过“引用”。在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱,今天这篇文章就简单介绍一下这四种类型,并简单说一下他们的使用场景。 1, 强引用(Strong Reference) 强引用类型,是我们最常讲的一个类型,我们先看一个例子: package cn.bridgeli.demo.reference; /** * @author BridgeLi * @date 2021/2/26 10:02 */ public class User { @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize"); } } package cn.bridgeli.demo.reference; import org.junit.Test; /** * @author BridgeLi * @date 2021/2/26 10:03 */ public class StrongReferenceTest { @Test public void testStrongReference() { User user = new User(); user = null; System.gc(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } 我们都知道当一个实例对象具有强引用时,垃圾回收器不会回收该对象,当内存不足时,宁愿 OOM,也就是抛出 OutOfMemeryError 异常也不会回收强引用的对象,因为 JVM 认为强引用的对象是用户正在使用的对象,它无法分辨出到底该回收哪个,强行回收有可能导致系统严重错误。但是当对象被赋值为 null 之后,会被回收,并且会执行对象的 finalize 函数,此时我们可以通过该函数拯救自己,但是有两点需要注意一个是只能拯救一次,当再次被垃圾回收的时候就不能拯救了,另一个就是有事没事千万不要重写次函数,本例只是为了说明问题重写了此函数,如果在工作中误重写了此函数,可能会导致垃圾不能回收,最终 OOM,另外有熟悉 GC 的同学没?猜一下我为什么要 sleep 一下? ...

February 28, 2021 · 3 min · 449 words · Bridge Li

用两个线程交替打印数字和字母

前一段时间听马士兵老师讲课,讲到某公司的一个面试,两个线程,其中一个线程输出ABC,另一个线程输出123,如何控制两个线程交叉输出1A2B3C,由于本人多线程掌握的一直不是很好,所以听完这道题,个人感觉收获良多,这是一个学习笔记。这道题有多种解法,不过有些属于纯炫技,所以只记录常见的三种解法。首先看第一种 park 和 unpark package cn.bridgeli.demo; import com.google.common.collect.Lists; import java.util.List; import java.util.concurrent.locks.LockSupport; /** * @author BridgeLi * @date 2021/2/6 16:14 */ public class Thread_Communication_Park_Unpark { static Thread t1 = null; static Thread t2 = null; public static void main(String[] args) { final List<Integer> integers = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7); final List<String> strings = Lists.newArrayList("A", "B", "C", "D", "E", "F", "G"); t1 = new Thread(() -> integers.forEach(item -> { System.out.print(item); LockSupport.unpark(t2); LockSupport.park(); }), "t1"); t2 = new Thread(() -> strings.forEach(item -> { LockSupport.park(); System.out.print(item); LockSupport.unpark(t1); }), "t2"); t1.start(); t2.start(); } } 这个是最简单的实现方法,LockSupport.park() 使当前线程阻塞,而 LockSupport.unpark() 则表示唤醒一个线程,所以他需要一个参数,表示你要唤醒哪个线程,很好理解,也比较简单。 ...

February 7, 2021 · 2 min · 413 words · Bridge Li

利用 DeferredResult 实现 http 轮询实时返回数据接口

博客有半年没更新了,不是我偷懒,而是之前服务器到期了,开博客这么多年,钱其实花了不少,但是没有一点收益,所以上了谷歌广告,如果文章对你稍稍有一点帮助,希望能花一秒钟帮忙点一下广告,谢谢。 今天这篇文章呢,不难,其实是解答我一直以来心里的一个疑问。是这样的,之前看五八技术委员会主席沈剑老师的公众号架构师之路的一篇文章:http 如何像 tcp 一样实时的收消息,里面其中的一个方案是用 http 短连接轮询的方式实现“伪长连接”。但是对于轮询,我们的第一反应肯定是有延时,但是标题不是说的是实时吗?当然我们可以把轮询的时长缩短一些,先不说这样大部分时间的轮询调用,可能都没消息返回,造成服务器资源浪费,轮询时间再短也是有延时啊,所以难道是伪实时?反正一般消息延时个三五秒,甚至十秒八秒一分钟,大家也不会在意,只会认为对方返回慢,对不起,这是我们程序员的锅,但是 http 真的不能实现实时吗?沈剑老师提出了一种方法:首选 webim 和 webserver 之间建立一条 http 连接,专门用作消息通道,这条连接叫 http 消息连接。然后会有如下处理: 没有消息到达的时候,这个 http 消息连接将被夯住,不返回,由于 http 是短连接,这个 http 消息连接最多被夯住 90 秒,就会被断开(这是浏览器或者 webserver 的行为); 在 1 的情况下,如果 http 消息连接被断开,立马再发起一个 http 消息连接; 此时在在 1 和 2 的配合下,浏览器与 webserver 之间将永远有一条消息连接在,然后还有一种情况 每次收到消息时,这个消息连接就能及时将消息带回浏览器页面,并且在返回后,会立马再发起一个 http 消息连接 这样就能做到使用 http 端连接轮询的方式实现了实时收消息。不过需要说明的是,其实还有一种情况:消息到达时,上一个 http 消息连接正在返回,也就是第二种情况的时候突然来了一个消息,此时没有 http 消息连接可用。虽然理论上 http 消息连接的返回是瞬时的,没有消息连接可用出现的概率极小,但是根据墨菲定律我们知道,这种情况肯定会出现,所以这种情况下我们可以将消息暂存入消息池中,下一个消息连接到达后,无需等待,直接去消息池中取消息,将将消息带回,然后立刻返回生成新的消息连接即可。 以上过程,可以参考沈剑老师的公众号,链接:https://mp.weixin.qq.com/s/6BCucq6QsH8lfDGLtQCl2A 不过以上都不是今天这篇文章的重点,和今天这篇文章的标题也没有任何关系。重点是当时看了沈剑老师的这篇文章后我一直有一个疑问:第一步的时候如何夯住?总不能 sleep 吧,这多不优雅啊,由于一直以为没有遇到过类似的需求,所以这么几年来我也没深究这个问题,但是心里确实一直记着,直到前一段时间,听马士兵教育的公开课,当时再讲类似的问题的时候提到了夯住 http 的连接(具体是哪个问题,还真不记得了),虽然当时上课的老师没提怎么实现,但是评论区我问了一下,如何夯住不返回?然后有一个同学回复说,用 DeferredResult,然后下课后搜了一下资料,果然可以,如下是实现的笔记,所以这才是重点,希望对有这个疑问的同学也有一点帮助。 消息返回实体类,大家可以根据实际情况,自己定义即可: package cn.bridgeli.deferredresulttest.entity; import lombok.Data; import lombok.Getter; /** * @author bridgeli */ @Data public class DeferredResultResponse { private Integer code; private String msg; public enum Msg { TIMEOUT("超时"), FAILED("失败"), SUCCESS("成功"); @Getter private String desc; Msg(String desc) { this.desc = desc; } } } controller 接口: package cn.bridgeli.deferredresulttest.controller; import cn.bridgeli.deferredresulttest.entity.DeferredResultResponse; import cn.bridgeli.deferredresulttest.service.DeferredResultService; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import javax.annotation.Resource; /** * @author bridgeli */ @RestController @RequestMapping(value = "/deferred-result") public class DeferredResultController { @Resource private DeferredResultService deferredResultService; /** * 为了方便测试,简单模拟一个 * 多个请求用同一个requestId会出问题 */ private final String requestId = "test"; @GetMapping(value = "/get") public DeferredResult<DeferredResultResponse> get(@RequestParam(value = "timeout", required = false, defaultValue = "10000") Long timeout) { DeferredResult<DeferredResultResponse> deferredResult = new DeferredResult<>(timeout); deferredResultService.process(requestId, deferredResult); return deferredResult; } /** * 设置DeferredResult对象的result属性,模拟异步操作 * * @param desired * @return */ @GetMapping(value = "/result") public String settingResult(@RequestParam(value = "desired", required = false, defaultValue = "成功") String desired) { DeferredResultResponse deferredResultResponse = new DeferredResultResponse(); if (DeferredResultResponse.Msg.SUCCESS.getDesc().equals(desired)) { deferredResultResponse.setCode(HttpStatus.OK.value()); deferredResultResponse.setMsg(desired); } else { deferredResultResponse.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); deferredResultResponse.setMsg(DeferredResultResponse.Msg.FAILED.getDesc()); } deferredResultService.settingResult(requestId, deferredResultResponse); return "Done"; } } 其中:/get 接口模拟沈剑老师说的消息连接,/result 接口模拟有一条新消息来了,然后 /get 接口会立即返回。主要注意的是 requestId,在实际项目中不能使用同一个,否则会出现问题,这个测一下就知道了,也很容易想到原因。 ...

January 9, 2021 · 2 min · 370 words · Bridge Li