-
什么是动态代理首先,动态代理是代理模式的一种实现方式,代理模式除了动态代理还有 静态代理,只不过静态代理能够在编译时期确定类的执行对象,而动态代理只有在运行时才能够确定执行对象是谁。代理可以看作是对最终调用目标的一个封装,能够通过操作代理对象来调用目标类,这样就可以实现调用者和目标对象的解耦合。动态代理的应用场景有很多,最常见的就是 AOP 的实现、RPC 远程调用、Java 注解对象获取、日志框架、全局性异常处理、事务处理等。动态代理的实现有很多,但是 JDK 动态代理是很重要的一种,下面就 JDK 动态代理来深入理解一波。 JDK 动态代理首先先来看一下动态代理的执行过程在 JDK 动态代理中,实现了 InvocationHandler 的类可以看作是 代理类(因为类也是一种对象,所以上面为了描述关系,把代理类形容成了代理对象)。JDK 动态代理就是围绕实现了 InvocationHandler 的代理类进行的,比如下面就是一个 InvocationHandler 的实现类,同时它也是一个代理类。public class UserHandler implements InvocationHandler { private UserDao userDao; public UserHandler(UserDao userDao){ this.userDao = userDao; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { saveUserStart(); Object obj = method.invoke(userDao, args); saveUserDone(); return obj; } public void saveUserStart(){ System.out.println("---- 开始插入 ----"); } public void saveUserDone(){ System.out.println("---- 插入完成 ----"); }}一键获取完整项目代码代理类一个最最最重要的方法就是 invoke 方法,它有三个参数Object proxy: 动态代理对象,关于这个方法后面会说。Method method: 表示最终要执行的方法,method.invoke 用于执行被代理的方法,也就是真正的目标方法Object[] args: 这个参数就是向目标方法传递的参数。这里构造好了代理类,现在就要使用它来实现对目标对象的调用,那么如何操作呢?请看下面代码public static void dynamicProxy(){ UserDao userDao = new UserDaoImpl(); InvocationHandler handler = new UserHandler(userDao); ClassLoader loader = userDao.getClass().getClassLoader(); Class<?>[] interfaces = userDao.getClass().getInterfaces(); UserDao proxy = (UserDao)Proxy.newProxyInstance(loader, interfaces, handler); proxy.saveUser();}一键获取完整项目代码如果要用 JDK 动态代理的话,就需要知道目标对象的类加载器、目标对象的接口,当然还要知道目标对象是谁。构造完成后,就可以调用 Proxy.newProxyInstance方法,然后把类加载器、目标对象的接口、目标对象绑定上去就完事儿了。这里需要注意一下 Proxy 类,它就是动态代理实现所用到的代理类。Proxy 位于java.lang.reflect 包下,这同时也旁敲侧击的表明动态代理的本质就是反射。下面就围绕 JDK 动态代理,来深入理解一下它的原理,以及搞懂为什么动态代理的本质就是反射。 动态代理的实现原理在了解动态代理的实现原理之前,先来了解一下 InvocationHandler 接口 InvocationHandler 接口JavaDoc 告诉我们,InvocationHandler 是一个接口,实现这个接口的类就表示该类是一个代理实现类,也就是代理类。InvocationHandler 接口中只有一个 invoke 方法。动态代理的优势在于能够很方便的对代理类中方法进行集中处理,而不用修改每个被代理的方法。因为所有被代理的方法(真正执行的方法)都是通过在 InvocationHandler 中的 invoke 方法调用的。所以只需要对 invoke 方法进行集中处理。invoke 方法只有三个参数public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;一键获取完整项目代码proxy:代理对象method: 代理对象调用的方法args:调用方法中的参数。动态代理的整个代理过程不像静态代理那样一目了然,清晰易懂,因为在动态代理的过程中,没有看到代理类的真正代理过程,也不明白其具体操作,所以要分析动态代理的实现原理,必须借助源码。那么问题来了,首先第一步应该从哪分析?如果不知道如何分析的话,干脆就使用倒推法,从后往前找,直接先从 _Proxy.newProxyInstance_入手,看看是否能略知一二。 Proxy.newInstance 方法分析Proxy 提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。Proxy.newProxyInstance 源码(java.lang.reflect.Proxy)public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { Objects.requireNonNull(h); final Class<?>[] intfs = interfaces.clone(); final SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkProxyAccess(Reflection.getCallerClass(), loader, intfs); } Class<?> cl = getProxyClass0(loader, intfs); try { if (sm != null) { checkNewProxyPermission(Reflection.getCallerClass(), cl); } final Constructor<?> cons = cl.getConstructor(constructorParams); final InvocationHandler ih = h; if (!Modifier.isPublic(cl.getModifiers())) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { cons.setAccessible(true); return null; } }); } return cons.newInstance(new Object[]{h}); } catch (Exception e) { ...}一键获取完整项目代码乍一看起来有点麻烦,其实源码都是这样,看起来非常复杂,但是慢慢分析、厘清条理过后就好,最重要的是分析源码不能着急。上面这个 Proxy.newProxyInstsance 其实就做了下面几件事,这里画了一个流程图作为参考。从上图中也可以看出,newProxyInstsance 方法最重要的几个环节就是获得代理类、获得构造器,然后构造新实例。对反射有一些了解的同学,应该会知道获得构造器和构造新实例是怎么回事。所以重点就放在了获得代理类,这是最关键的一步,对应源码中的 _Class<?> cl = getProxyClass0(loader, intfs);_ 进入这个方法一探究竟private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) { if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); } return proxyClassCache.get(loader, interfaces);}一键获取完整项目代码这个方法比较简单,首先会直接判断接口长度是否大于 65535(刚开始看到这里是有点不明白的,这个判断是要判断什么?interfaces 这不是一个 class 类型吗,从 length 点进去也看不到这个属性,细看一下才明白,这居然是可变参数,Class … 中的 … 就是可变参数,所以这个判断应该是判断接口数量是否大于 65535。)然后会直接从 proxyClassCache 中根据 loader 和 interfaces 获取代理对象实例。如果能够根据 loader 和 interfaces 找到代理对象,将会返回缓存中的对象副本;否则,它将通过 ProxyClassFactory 创建代理类。proxyClassCache.get 就是一系列从缓存中的查询操作,注意这里的 proxyClassCache 其实是一个 WeakCache,WeakCahe 也是位于 java.lang.reflect 包下的一个缓存映射 map,它的主要特点是一个弱引用的 map,但是它内部有一个 SubKey ,这个子键却是强引用的。这里不用去追究这个 proxyClassCache 是如何进行缓存的,只需要知道它的缓存时机就可以了:即在类加载的时候进行缓存。如果无法找到代理对象,就会通过 ProxyClassFactory 创建代理,ProxyClassFactory 继承于 BiFunctionprivate static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>> {...}一键获取完整项目代码ProxyClassFactory 里面有两个属性一个方法。proxyClassNamePrefix:这个属性表明使用 ProxyClassFactory 创建出来的代理实例的命名是以 “$Proxy” 为前缀的。nextUniqueNumber:这个属性表明 ProxyClassFactory 的后缀是使用 AtomicLong 生成的数字所以代理实例的命名一般是 Proxy1这种。这个 apply 方法是一个根据接口和类加载器进行代理实例创建的工厂方法,下面是这段代码的核心。@Overridepublic Class<?> apply(ClassLoader loader, Class<?>[] interfaces) { ... long num = nextUniqueNumber.getAndIncrement(); String proxyName = proxyPkg + proxyClassNamePrefix + num; byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags); try { return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); } catch (ClassFormatError e) { throw new IllegalArgumentException(e.toString()); }}一键获取完整项目代码可以看到,代理实例的命名就是上面所描述的那种命名方式,只不过它这里加上了 proxyPkg 包名的路径。然后下面就是生成代理实例的关键代码。ProxyGenerator.generateProxyClass 跟进去是只能看到 .class 文件的,class 文件是虚拟机编译之后的结果,所以要看一下 .java 文件源码。.java 源码位于 OpenJDK中的 sun.misc 包中的 ProxyGenerator 下。此类的 generateProxyClass() 静态方法的核心内容就是去调用 generateClassFile() 实例方法来生成 Class 文件。方法太长了就不贴了,这里就大致解释以下其作用:第一步:收集所有要生成的代理方法,将其包装成 ProxyMethod 对象并注册到 Map 集合中。第二步:收集所有要为 Class 文件生成的字段信息和方法信息。第三步:完成了上面的工作后,开始组装 Class 文件。而 defineClass0 这个方法点进去是 native ,底层是 C/C++ 实现的,于是去看了一下 C/C++ 源码,路径在点开之后的 C/C++ 源码还是挺让人绝望的。不过再回头看一下这个 defineClass0 方法,它实际上就是根据上面生成的 proxyClassFile 字节数组来生成对应的实例罢了,所以不必再深究 C/C++ 对于代理对象的合成过程了。所以总结一下可以看出,JDK 生成了一个叫 $Proxy0 的代理类,这个类文件放在内存中的,在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。所以最开始的 dynamicProxy 方法反编译后的代码就是这样的public final class $Proxy0 extends java.lang.reflect.Proxy implements com.cxuan.dynamic.UserDao { public $Proxy0(java.lang.reflect.InvocationHandler) throws ; Code: 0: aload_0 1: aload_1 2: invokespecial #8 // Method java/lang/reflect/Proxy."<init>":(Ljava/lang/reflect/InvocationHandler;)V 5: return一键获取完整项目代码可以看到代理类继承了 Proxy 类,所以也就决定了 Java 动态代理只能对接口进行代理。 于是,上面这个图应该就可以看懂了。 invoke 方法中第一个参数 proxy 的作用细心的小伙伴们可能都发现了,invoke 方法中第一个 proxy 的作用是啥?代码里面好像 proxy 也没用到,这个参数的意义是啥呢?它运行时的类型是啥啊?为什么不使用 this 代替呢?Stackoverflow 给出了一个回答 https://stackoverflow.com/questions/22930195/understanding-proxy-arguments-of-the-invoke-method-of-java-lang-reflect-invoca什么意思呢?就是说这个 proxy ,它是真正的代理对象,invoke 方法可以返回调用代理对象方法的返回结果,也可以返回对象的真实代理对象,也就是 $Proxy0,这也是它运行时的类型。至于为什么不用 this 来代替 proxy,因为实现了 InvocationHandler 的对象中的 this ,指代的还是 InvocationHandler 接口实现类本身,而不是真实的代理对象————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/152166279
-
HTTP协议HTTP 理论和实践同样重要。如果我们未来写web开发(写网站)。HTTP就是我们工作中最常用到的东西。什么是HTTP?HTTP是应用层的协议,HTTP现在大规模使用的版本是 HTTP/1.1使用HTTP协议的场景:1.浏览器打开网站(基本上)2.手机APP访问对应的服务器(大概率)报文格式HTTP最重要的就是报文格式了HTTP的报文格式和 TCP/IP/UDP 这些不同,HTTP 的报文格式,要分两个部分来看待请求和响应HTTP协议,是一种 一问一答 结构模型的协议,请求和响应的协议格式,是有所差别的这里也不只有一问一答的模型结构:一问一答(访问网站)多问一答(上传文件,把请求拆分成多份传给服务器)一问多答(下载文件,把响应拆成多份进行下载)多问多答(串流 / 远程桌面,远程桌面就是这边操作了,那边就会有响应)如何查看到 HTTP 请求和响应的格式呢?抓包工具:把网卡上经过的数据,获取到,并显示出来(这个是我们必备的技能,分析和调试程序的重要手段)Fiddler抓包工具:wireshark:功能非常强大,可以抓各种各样的协议,使用起来比较复杂,但是用这个抓 HTTP 是不太方便的Fiddler工具:是专门用来抓HTTP的,使用起来简单,企业中也经常用到在官网下载Fiddler Classic 版本的就可以FIddler的请求和响应,还要下载证书,否则就只抓包http了,不能抓https了安装证书:出现这个点cancel就行4. Fiddler 本质上是一个 代理,可能会和其他的代理软件冲突举个栗子:除了 fiddler 之外,有的程序也是代理(1) 加速器(2) vpn…这些代理程序之间可能会产生冲突如果你的 fiddler 不能抓包了,一定要检查关闭之前的开启的代理软件(也可能是一个浏览器插件),如果还是不行,还可以尝试换一个浏览器,有的时候 edge浏览器 可能会不行,比如出现下面这样的情况postman 是构造请求的软件fiddler 是抓取/显示已有的请求的软件ctrl + a:选中所有的请求,delete删除这里所有的请求,方便后续观察想要观察的httpHTTP 协议是文本格式的协议(协议里面都是字符串)TCP,UDP,IP…都是二进制格式的协议**HTTP 响应也是文本的。直接查看,往往能看到二进制的数据(压缩后的),HTTP 响应经常会被压缩。因为压缩之后,体积更小,传输的时候,节省网络带宽。**但是,压缩和解压缩,是需要消耗额外的 cpu 和 时间 的。其实也没关系,一台服务器,最贵的硬件资源就是网络带宽,这样压缩和解压缩就是用便宜的换贵的解压缩之后,可以看到,响应的数据其实是 html,浏览器上显示的网页,就是html,往往都是浏览器先请求对应的服务器,从服务器这边拿到的 网页 数据(html),每次访问网页,都会下载对应的网页数据,会消耗一定的带宽(不像我们下载APP一样,只需要下载一次) 请求:键值对这里是 标准规定 的,所以不能自己胡编乱造响应:URLURL 是计算机中的非常重要的概念,不仅仅是在 HTTP 中涉及到jdbc设置数据源setUrl(“jdbc:mysql://127.0.0.1:3306/java1?characterEncoding=utf8&useSSL=false”);setUsersetPasswordURL,描述了某个资源在网络上的所属位置。数据库也是一种资源URL的具体信息:1.协议方案名(协议名):例如,https:// ,jdbc:mysql://2.登录信息(认证):这个东西现在几乎不会用到了(尤其是针对用户的产品)3.服务器地址:可以 IP地址,也可以是 域名4.服务器端口号:5.带层次的文件路径:6.查询字符串(query string)关于以上 url 的几个部分(query string),举个例子:对于 query string 来说,如果 value 部分要包含一些特殊符号的话,往往需要进行 urlencode 操作比如:搜索C++这个词后面使用 url 的时候,要记得针对 query string 的内容进行好 urlencode 工作。如果不处理好,有些浏览器就可能会解析失败,导致请求无法正常工作中文汉字也需要转义,如果不转义的话,汉字也出现特殊的符号的话,不就bug了7.片段标识符:比如说这个网址就有 [vue技术文档],片段标识符实现页面内部的跳转(https://cn.vuejs.org/guide/introduction.html#the-progressive-framework)请求方法方法既有 GET 还有POST,出现最多的还是GET,POST出现的比较少POST请求POST(1) 登录(2) 上传文件习惯用法(不是硬性规定,也可以不遵守的)这个不遵守的话,客户端和服务器都要一起不遵守的,不然会出现问题GET 请求,通常会把要传给服务器的数据,加到 url 的 query string 中。POST 请求,通常把要传给服务器的数据,加到 body 中登录和上传头像的请求 出现蓝色的字体(在刚开始加载网页的时候出现的,后续Fiddler删除这些包,就不会出现了),是获取到网页(得到 html 的响应)对于浏览器缓存的理解:刚才最开始没有抓到这里的返回的请求,是因为命中了浏览器的缓存存在网络缓存的原因:浏览器显示的网页,其实是从服务去这边下载的 html。html 内容可能会比较多,体积可能比较大,通过网络加载,消耗的时间就可能会比较多。浏览器一般都会自己带有缓存,就会把之前加载过的页面,保存在本地硬盘上。下次访问直接读取本地硬盘的数据即可。缓存可能出现的bug:如果缓存的是之前未修改的网页,缓存之后又修改了网页,就可能会出现bug上传图像的body本身是比较长的方法的种类:这些HTTP请求最初的初心是为了表示不同的 语义(不同的含义),现在在实际的使用过程中,初心,已经被遗忘了。HTTP的各种请求,目前来说已经不一定完全遵守自己的初心了,实际上程序员如何使用,更加随意了比如:还可以用GET来上传文件,用POST获取文件,也是可行的还有 POST和 PUT 目前来说,可以理解成 没有任何的区别!!!(任何使用 POST 的场景,换成 PUT 完全可以,反之亦然)GET 和 POST 的区别经典的面试题:GET 和 POST 的区别GET 和 POST 之间的差别,网络上的有些说法,需要大家来注意!(都是错误的说法)GET 请求能传递的数据量有上限,POST 传递的数据量没有上限GET 请求传递的数据不安全,POST 请求传递的数据更安全安全关键在于是否加密了GET 只能给服务器传输 文本数据。POST 可以给服务器传输文本 和 二进制数据(1) GET 也不是不能使用body(body 中是可以直接放二进制数的)(2) GET 也可以把 二进制的数据进行 base64 转码(转码成字符串的文本文件),放到 url 的query string 中,之后再解码,不也是二进制的数据吗下面说法不够准确,但是也不是完全错的1.GET 请求是幂等的。POST 请求不是幂等的。幂等:数学概念,输入相同的内容,输出是稳定的。举个栗子:吃进去的是草,挤出来的是奶。如果任何时候吃草,挤出来的都是奶,就是幂等的。如果吃草之后,不同时候挤出来的东西不一样,就不是幂等的。GET 和 POST 具体是否是幂等的,取决于代码的实现。GET 是否幂等,也不绝对。只不过 RFC 标准文档上建议 GET 请求实现成幂等的。再举个例子:不同的时间,广告的顺序都可能会不同 Header有哪些常见的Header,理解Header的含义HOSTHOST:www.sogou.comHOST后这个信息在 url 中也是存在的比如,在使用代理的情况下,Host 的内容是可能和 url 中的内容不同的Content-Length:body 中数据的长度Content-Type:body 中数据的格式注意:请求中有 body,才会有这两个属性。通常情况下 GET 请求没有 body,POST 请求有 bodybody 中的格式,可以选择的方式是非常多的body中数据的格式:请求:1.json2.form 表单的格式:相当于是把 GET 的query string 给搬到 body 中。(后面写个代码来构造一个)上传图片:3.form-data 的格式:上传文件的时候,会涉及到(也不一定就是 form-data,也可能是 form 表单)响应:后续给服务器提交请求,不同的 Content-Type(body),服务器处理数据的逻辑是不同的。服务器返回数据给浏览器,也需要设置合适的 Content-Type,浏览器也会根据不同的Content-Type 做出不同的处理User-Agent(简称UA)UA是用来区分不同的设备的,上古时期的UA是用来区分浏览器的兼容问题的,可以让旧的浏览器支持文字,让新的浏览器支持图片,这样也就区分开了现在 UA 使用来区分是PC端还是 移动端的Referer:描述了当前页面是从哪个页面跳转而来的如果是直接在地址栏输入 url(或者是点击收藏夹中的按钮)都是没有 RefererReferer的用途:这个也可以用来算钱的搜索引擎,每点击一次,它都会赚钱通过域名来区分是百度投放的广告,还是搜狗投放的广告 5. CookieCookie可以认为是浏览器在本地存储数据的一种机制对于安全性的考虑,所以引入了Cookie,存储简单的字符串也是够用的Cookie是怎么进行存储的(重点)浏览器中的 CookieCookie的作用:Servlet / Spring 中会结合代码更深入地理解Cookie响应状态码状态码响应 状态码表示了这次请求对应的响应,是啥样的状态(成功,失败,其他的情况,对应的原因是什么)这里的状态码,种类非常多。咱们不需要全都记住成功2xx 都表示成功,200是最常见的重定向3xx 表示重定向,(可以理解为爱情转移)请求中访问的是 A 这样的地址。响应返回了一个重定向报文,告诉你应该要访问B地址举个例子:比如你请教一个老师,老师说这边还没有空,叫你去找另一个老师,如果还不行的话,再来找我301和302301 Moved Permanently是永久重定向,是有缓存的,比如你地址就在新的网站了,以后就不要访问旧的网站了302 Move tmporarily 是临时重定向,是没有缓存的,指不定后面又要更改,比如说你地址在新网站了,但是可能后面又会改回旧的网站重定向的响应报文中,会带有Location字段,描述出当前要跳转到哪个新的地址(后面会在代码中具体演示)请求错误客户端,也就是浏览器(客户端)会构造一个http请求,不符合服务器的一些要求404 Not Found请求中访问的资源,在服务器上不存在比如:HTTP/1.1 404 Not Found404这个状态码表示的是自愿不存在,同时在 body 中也是可以返回一个指定的错误页面的,很多网站会把这个错误页面做的很丰富多彩比如:bilibili403 Forbidden表示访问的资源没有权限服务器错误5xx 表示服务器出错了看到这个说明服务器挂了比较容易出现的是500,后面我们自己写服务器的时候,还是比较容易写出500这样的问题的(一般就是你的代码有bug)一个经典的面试题:说一下,HTTP的状态码有哪些常见的通过不同的数字来表示状态,这种做法其实是非常常见的(在C语言中也有,比如 errno,表示打开文件失败的原因, strerror 把errno翻译成字符串)可能我们面试中也会考察C语言的 如何让客户端构造一个HTTP请求如何让服务器处理一个HTTP请求,这是非常重要的内容(Servlet/Spring中都会涉及到这一部分)浏览器:直接在浏览器地址栏输入url,此时构造了一个GET请求html中,一些特殊的html标签,可能会触发GET请求,比如像 img,a,link,script…通过form表单来触发GET/POST请求(我们需要了解一下),form本质也是一个HTML标签写一个简单的html代码,来编写逻辑(解释form的逻辑)form表达如何编写?使用form标签设置提交按钮:构造GET请求:构造POST请求:4. ajax的方式1.引入jquery库(第三方库,是需要额外下载引入的)前端引入第三方库非常容易的,只要代码中写一个库的地址即可jquery cdn中有一些资源和库2.编写代码回调函数:js中定义变量:ajax的get请求写法:ajax的post请求写法:构造请求还有更简单,更方便的方式,比如使用第三方工具,可以实现这里的效果。form能够构造get和post请求,ajax也能构造get和post请求。上面的知道这些就可以了,等学了前端来看才能够懂!!!一种更简单的构造HTTP请求的方式直接通过第三方工具,图形化的界面来构造。我们这里使用postman先创建一个Workspaces,然后创建标签页请求的设定,最后发送请求postman 也是我们常用的构造请求的方式,一般是用于测试阶段不会写ajax也没事,postman都会给你自动生成代码,就在右上角的code按钮HTTPS引入HTTPS的原因:使用HTTPS进行加密,也是为了防止运营商劫持,还有比如在商场你连商场的wifi,也可能会被黑客截获数据,黑客把wifi伪装成商场的wifi一样的名字————————————————原文链接:https://blog.csdn.net/2301_79722622/article/details/151930571
-
1.什么是闭包:闭包是JavaScript中最强大且独特的特性之一,它是函数与其词法环境的组合。闭包使得函数能够访问其外部作用域的变量,即使外部函数已经执行完毕。这种机制让JavaScript具备了许多其他语言需要复杂语法才能实现的功能。// 最简单的闭包示例function outer() { const message = "Hello from outer!"; // 外部变量 function inner() { console.log(message); // inner函数"记住"了message } return inner; // 返回inner函数}const myClosure = outer(); myClosure(); // "Hello from outer!"一键获取完整项目代码javascript2. 核心概念与前置知识:2.1 执行上下文 (Execution Context):执行上下文是 JavaScript 代码执行时的环境,每当代码执行时都会创建对应的执行上下文。执行上下文栈 (Call Stack)全局执行上下文Global EC函数执行上下文 1Function EC 1函数执行上下文 2Function EC 2函数执行上下文 3Function EC 3执行上下文的组成部分:// 伪代码:执行上下文的内部结构ExecutionContext = { // 1. 词法环境 (用于let/const和函数声明) LexicalEnvironment: { EnvironmentRecord: {}, // 环境记录 outer: null // 外部环境引用 }, // 2. 变量环境 (用于var声明) VariableEnvironment: { EnvironmentRecord: {}, outer: null }, // 3. this绑定 ThisBinding: undefined}一键获取完整项目代码javascript2.2 词法环境 (Lexical Environment):词法环境是存储标识符-变量映射的结构,由环境记录和外部环境引用组成。环境记录类型词法环境结构声明式环境记录Declarative对象环境记录Object全局环境记录Global词法环境Lexical Environment环境记录Environment Record外部环境引用Outer Reference词法环境的创建过程:function createLexicalEnvironment() { // 示例:词法环境的创建 function outer(x) { let a = 10; const b = 20; function inner(y) { let c = 30; console.log(a + b + c + x + y); // 访问多个作用域的变量 } return inner; } return outer;}// 执行过程中的词法环境变化/*1. 全局词法环境:{ EnvironmentRecord: { createLexicalEnvironment: <function>, outer: <function> }, outer: null}2. outer函数的词法环境:{ EnvironmentRecord: { x: 参数值, a: 10, b: 20, inner: <function> }, outer: <全局词法环境的引用>}3. inner函数的词法环境:{ EnvironmentRecord: { y: 参数值, c: 30 }, outer: <outer函数词法环境的引用>}*/一键获取完整项目代码javascript2.3 作用域链 (Scope Chain)作用域链是通过词法环境的外部引用形成的链条,用于标识符解析。当前词法环境父级词法环境全局词法环境null作用域链查找算法// 作用域链查找示例function demonstrateScopeChain() { const globalVar = "全局变量"; function level1() { const level1Var = "第一层变量"; function level2() { const level2Var = "第二层变量"; function level3() { const level3Var = "第三层变量"; // 变量查找顺序演示 console.log(level3Var); // 1. 在当前环境找到 console.log(level2Var); // 2. 向上一层查找 console.log(level1Var); // 3. 继续向上查找 console.log(globalVar); // 4. 查找到全局环境 // console.log(nonExistent); // 5. 找不到则报错 } return level3; } return level2; } return level1;}// 调用过程中的作用域链const fn = demonstrateScopeChain()()();fn(); // 执行时会沿着作用域链查找变量一键获取完整项目代码javascript3. 代码示例与分析:3.1 经典的闭包示例function makeCounter() { let count = 0; // 外部函数的局部变量 return function() { // 返回的内部函数形成闭包 count++; // 访问外部函数的变量 return count; };}const counter = makeCounter(); // 调用外部函数console.log(counter()); // 1 - 调用闭包函数console.log(counter()); // 2 - count 变量被保持console.log(counter()); // 3一键获取完整项目代码javascript3.2 逐行执行过程分析全局执行上下文makeCounter执行上下文闭包函数1. 创建全局执行上下文2. 调用makeCounter(),创建新执行上下文3. 创建count变量,值为04. 创建匿名函数,形成闭包5. 返回函数,makeCounter执行完毕6. makeCounter执行上下文出栈7. 但闭包仍保持对count的引用8. 调用counter()9. 访问并修改count变量10. 返回结果全局执行上下文makeCounter执行上下文闭包函数详细步骤解析:// 步骤 1-2: 全局执行上下文创建,调用makeCounterfunction makeCounter() { // 步骤 3: 在makeCounter的词法环境中创建count变量 let count = 0; // 步骤 4: 创建匿名函数,该函数的[[Environment]]属性 // 指向makeCounter的词法环境,形成闭包 return function() { // 步骤 9: 通过作用域链找到外部的count变量 count++; return count; }; // 步骤 5-6: makeCounter执行完毕,执行上下文出栈 // 但count变量因为被闭包引用而不会被垃圾回收}// 步骤 7: counter变量保存了闭包函数的引用const counter = makeCounter();// 步骤 8-10: 每次调用counter()都会访问保存的count变量console.log(counter()); // 1一键获取完整项目代码javascript3.3 V8 引擎的优化V8 引擎对闭包进行了以下优化:变量提升优化:只保留被闭包实际使用的外部变量内存管理:未被引用的外部变量会被垃圾回收作用域分析:在编译时分析变量使用情况function optimizationExample() { let used = "被闭包使用的变量"; let unused = "未被使用的变量"; // V8会优化掉这个变量 let alsoUnused = "同样未被使用"; return function() { console.log(used); // 只有这个变量会被保留在闭包中 };}一键获取完整项目代码javascript注意:在调试时,由于V8的优化,某些未使用的变量可能在调试器中显示为 “undefined”。4. 经典应用场景与最佳实践:4.1 模块化封装const Calculator = (function() { let result = 0; // 私有变量 return { add: function(x) { result += x; return this; }, multiply: function(x) { result *= x; return this; }, getResult: function() { return result; }, reset: function() { result = 0; return this; } };})();// 使用Calculator.add(5).multiply(2).getResult(); // 10一键获取完整项目代码javascript4.2 事件处理与回调function createButtonHandler(name) { return function(event) { console.log(`按钮 ${name} 被点击了`); // name变量被闭包保存 };}document.getElementById('btn1').onclick = createButtonHandler('按钮1');document.getElementById('btn2').onclick = createButtonHandler('按钮2');一键获取完整项目代码javascript4.3 防抖和节流// 防抖函数function debounce(func, delay) { let timeoutId; // 被闭包保存的变量 return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { func.apply(this, args); }, delay); };}// 节流函数function throttle(func, delay) { let lastCall = 0; return function(...args) { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; func.apply(this, args); } };}一键获取完整项目代码javascript5. 常见陷阱与解决方案:5.1 循环中的闭包陷阱问题代码:// 经典错误示例for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // 输出三次 3 }, 100);}一键获取完整项目代码javascript解决方案:// 方案1:使用IIFE创建新的作用域for (var i = 0; i < 3; i++) { (function(j) { setTimeout(function() { console.log(j); // 输出 0, 1, 2 }, 100); })(i);}// 方案2:使用let块级作用域for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // 输出 0, 1, 2 }, 100);}// 方案3:使用bindfor (var i = 0; i < 3; i++) { setTimeout(function(j) { console.log(j); // 输出 0, 1, 2 }.bind(null, i), 100);}一键获取完整项目代码javascript5.2 内存泄漏风险// 可能导致内存泄漏的代码function createHandler() { const largeData = new Array(1000000).fill('data'); // 大量数据 return function() { // 即使不使用largeData,它也会被闭包保留 console.log('handler called'); };}// 解决方案:显式释放不需要的引用function createHandler() { const largeData = new Array(1000000).fill('data'); const needed = largeData.slice(0, 10); // 只保留需要的部分 return function() { console.log(needed.length); // largeData会被垃圾回收 };}————————————————原文链接:https://blog.csdn.net/2301_81253185/article/details/151932979
-
如果你用过 Java 集合(如List、Map),一定见过List<String>、Map<Integer, String>这样的写法 —— 这就是泛型。泛型是 Java 5 引入的核心特性,它解决了 “容器存储元素类型不明确” 的问题,让代码更安全、更简洁。今天我们就从泛型的作用讲起,深入剖析泛型类、泛型方法、泛型接口的定义与使用,配合直观图解,让你彻底搞懂泛型!一、为什么需要泛型?—— 从两个痛点说起 在没有泛型的 Java 早期版本中,集合(如ArrayList)只能存储Object类型的元素。这会导致两个严重问题:类型不安全和频繁强制转型。痛点 1:类型不安全(运行时错误) 假设我们创建一个ArrayList,本意是存字符串,却不小心存入了整数。编译器不会报错,但运行时调用字符串方法会抛ClassCastException:// Java 5之前的代码(无泛型)List list = new ArrayList();list.add("hello");list.add(123); // 存入整数,编译器不报错 // 取出元素时,假设都是字符串String str = (String) list.get(1); // 运行时报错:Integer cannot be cast to String一键获取完整项目代码java痛点 2:频繁强制转型(代码冗余)即使存入的元素类型一致,取出时也必须手动转型,代码繁琐且易出错:List list = new ArrayList();list.add("apple");list.add("banana"); // 每次取元素都要转型String s1 = (String) list.get(0);String s2 = (String) list.get(1);一键获取完整项目代码java泛型如何解决这些问题? 泛型的核心思想是:在定义类 / 接口 / 方法时,不指定具体类型,而是留出 “类型参数”,在使用时再指定具体类型。用泛型改写上面的例子:// 定义时指定类型参数<String>,表示只能存字符串List<String> list = new ArrayList<>();list.add("hello");// list.add(123); // 编译时直接报错,类型不匹配 // 取出时无需转型,编译器已知是String类型String str = list.get(0);一键获取完整项目代码java泛型的两大核心作用:类型安全:编译时检查元素类型,避免存入错误类型的元素(将运行时错误提前到编译时);避免强制转型:编译器自动推断元素类型,取出时无需手动转型,简化代码。泛型作用图解二、泛型类:让类支持 “类型参数化” 泛型类是指在类定义时声明类型参数,使得类中的字段、方法参数或返回值可以使用这些类型参数。当创建类的实例时,指定具体类型,从而实现 “一个类适配多种数据类型”。1. 泛型类的定义语法public class 类名<类型参数1, 类型参数2, ...> { // 类型参数可以作为字段类型 private 类型参数1 变量名; // 可以作为方法参数或返回值类型 public 类型参数1 方法名(类型参数2 参数) { // ... }}一键获取完整项目代码java类型参数命名规范(约定俗成,增强可读性):T:Type(表示任意类型);E:Element(表示集合中的元素类型);K:Key(表示键类型);V:Value(表示值类型);若有多个参数,可用T1、T2或K、V等组合。2. 泛型类示例:自定义容器类假设我们需要一个 “容器” 类,既能存整数,也能存字符串,还能存自定义对象。用泛型类实现:// 定义泛型类,类型参数为T(表示容器中元素的类型)public class Container<T> { private T item; // 用T作为字段类型 // 构造方法,参数类型为T public Container(T item) { this.item = item; } // 方法返回值类型为T public T getItem() { return item; } // 方法参数类型为T public void setItem(T item) { this.item = item; }}一键获取完整项目代码java使用泛型类时,指定具体类型:// 创建存字符串的容器Container<String> strContainer = new Container<>("hello");String str = strContainer.getItem(); // 无需转型 // 创建存整数的容器Container<Integer> intContainer = new Container<>(123);int num = intContainer.getItem(); // 无需转型 // 创建存自定义对象的容器(如User类)Container<User> userContainer = new Container<>(new User("张三"));User user = userContainer.getItem();一键获取完整项目代码java注意:类型参数不能是基本类型(如int、double),必须用包装类(Integer、Double)。3. 多类型参数的泛型类如果需要多个类型参数(如键值对),可以声明多个类型参数:// 键值对泛型类,K表示键类型,V表示值类型public class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } // getter和setter public K getKey() { return key; } public V getValue() { return value; }}一键获取完整项目代码java使用时分别指定键和值的类型:Pair<String, Integer> pair = new Pair<>("age", 20);String key = pair.getKey(); // "age"Integer value = pair.getValue(); // 20一键获取完整项目代码java泛型类结构图解三、泛型方法:单个方法的 “类型参数化” 泛型方法是指在方法声明时单独声明类型参数的方法,它可以定义在泛型类中,也可以定义在普通类中。泛型方法的核心优势是:方法的类型参数独立于类的类型参数,灵活性更高。1. 泛型方法的定义语法// 修饰符 <类型参数> 返回值类型 方法名(参数列表) { ... }public <T> T 方法名(T 参数) { // ...}一键获取完整项目代码java关键:泛型方法必须在返回值前声明类型参数(如<T>),这是区分泛型方法与普通方法的标志。2. 泛型方法示例:通用打印方法实现一个方法,能打印任意类型的数组元素:public class GenericMethodDemo { // 泛型方法:打印任意类型的数组 public static <T> void printArray(T[] array) { for (T element : array) { System.out.print(element + " "); } System.out.println(); } public static void main(String[] args) { // 打印字符串数组 String[] strArray = {"a", "b", "c"}; printArray(strArray); // 输出:a b c // 打印整数数组 Integer[] intArray = {1, 2, 3}; printArray(intArray); // 输出:1 2 3 // 打印自定义对象数组(如User) User[] userArray = {new User("张三"), new User("李四")}; printArray(userArray); // 输出:User{name='张三'} User{name='李四'} }}一键获取完整项目代码java 为什么不用泛型类?如果用泛型类,每次打印不同类型的数组都要创建不同的类实例,而泛型方法可以直接通过静态方法调用,更简洁。3. 泛型方法与泛型类的区别对比项 泛型类 泛型方法类型参数声明 在类名后(如class A<T>) 在方法返回值前(如<T> void f())作用范围 整个类 仅当前方法灵活性 依赖类的实例化类型 调用时可独立指定类型泛型方法调用图解四、泛型接口:让接口支持 “类型参数化” 泛型接口与泛型类类似,在接口定义时声明类型参数,实现类可以指定具体类型或继续保留类型参数。1. 泛型接口的定义语法public interface 接口名<类型参数> { // 类型参数可以作为方法参数或返回值 类型参数 方法名(); void 方法名(类型参数 参数);}一键获取完整项目代码java2. 泛型接口示例:自定义比较器接口JDK 中的Comparable接口就是典型的泛型接口,我们模仿它定义一个简单的泛型接口:// 泛型接口:支持比较任意类型的对象public interface MyComparable<T> { // 比较当前对象与另一个对象,返回正数/负数/0表示大于/小于/等于 int compareTo(T other);}一键获取完整项目代码java实现方式 1:实现类指定具体类型// 让User类实现MyComparable,指定T为User(比较两个User对象)public class User implements MyComparable<User> { private String name; private int age; // 实现compareTo方法,按年龄比较 @Override public int compareTo(User other) { return this.age - other.age; } // 构造器和getter省略}一键获取完整项目代码java使用时:User u1 = new User("张三", 20);User u2 = new User("李四", 25);System.out.println(u1.compareTo(u2)); // -5(u1年龄小于u2)一键获取完整项目代码java实现方式 2:实现类继续保留泛型参数// 实现类不指定具体类型,继续使用泛型Tpublic class PairComparator<T> implements MyComparable<Pair<T, T>> { @Override public int compareTo(Pair<T, T> other) { // 假设Pair的比较逻辑(此处简化) return 0; }}一键获取完整项目代码java3. JDK 中的泛型接口:Comparable与ComparatorComparable<T>:类自身实现,定义 “自然排序”(如String按字典序排序);Comparator<T>:外部定义排序规则,更灵活(如按自定义规则排序)。这两个接口广泛用于Collections.sort()等方法中,是泛型接口的经典应用。泛型接口实现图解五、泛型的底层:类型擦除(简单了解) Java 泛型是 “编译时特性”,在运行时会擦除类型参数信息(称为 “类型擦除”)。也就是说,JVM 在运行时看不到List<String>和List<Integer>的区别,它们都会被擦除为List。类型擦除的规则:若类型参数有上限(如<T extends Number>),擦除为上限类型(Number);若没有上限,擦除为Object。例如:// 编译前List<String> list = new ArrayList<>();list.add("a"); // 编译后(类型擦除)List list = new ArrayList();list.add("a"); // 隐含String类型检查String s = (String) list.get(0); // 编译器自动添加转型一键获取完整项目代码java 为什么需要了解类型擦除?避免踩坑:例如不能用instanceof判断泛型类型(因为运行时已擦除):// 错误:编译不通过(无法判断List的泛型类型)if (list instanceof List<String>) { ... }一键获取完整项目代码java六、总结泛型是 Java 中简化代码、提升安全性的核心特性,核心要点:泛型的作用:类型安全:编译时检查元素类型,避免运行时类型转换错误;避免转型:编译器自动推断类型,减少手动转型代码。泛型类:在类定义时声明类型参数(如class Container<T>),实例化时指定具体类型,适用于类整体需要适配多种类型的场景。泛型方法:在方法返回值前声明类型参数(如<T> void print(T t)),可独立于类的泛型使用,灵活性更高。泛型接口:类似泛型类,实现类可指定具体类型或保留泛型(如Comparable<T>)。 掌握泛型是理解 Java 集合框架、实现通用组件的基础。实际开发中,合理使用泛型能让代码更简洁、更安全、更易维护。下一篇我们将深入泛型的高级特性(通配符、上限下限等),敬请期待!————————————————原文链接:https://blog.csdn.net/qq_40303030/article/details/152209932
-
1,JUC(java.util.concurrent)的常见类1)Callable 接口我们之前学过Runnable接口,它是一个任务,我们可以在创建线程的时候把任务丢给线程使用匿名内部类等方法来完成创建对象,现在我们有了一个新的方法来创建任务,并且执行这个任务,就是我们的Callable接口,Runnable的run方法是没有返回值的,但是Callable提供了返回值,支持泛型,我们就能获取到我们想要的参数,我们来看看是怎么用的; Callable<Integer> callable = new Callable<Integer>() { @Override public Integer call() throws Exception { return null; } };一键获取完整项目代码java我们使用匿名内部类的方法创建一个Callable对象,并且重写call方法,就相当与重写Runnable的run方法, 我们是不能把这个对象直接放到线程的构造方法中的,因为Thread没有提供传入Callable的版本,我们要使用另一个类FutureTask来拿到结果,在把创建的futureTask对象放到线程创建时的构造方法中去; FutureTask<Integer> task = new FutureTask<>(callable); Thread t1 = new Thread(task); t1.start(); System.out.println(task.get());一键获取完整项目代码java这里的task.get方法会阻塞main线程结束,直到t1线程正确计算出结果; 2)ReentrantLock这个是上古时期的锁,现在有更智能,更好的替代synchronized,那我们还学它干嘛呢,它还活着就一定是有原因的,1,synchronized是关键字,是由JVM内部通过C++实现的,而ReentrantLock是一个类;2,synchronized是通过进出代码块来实现的,ReentrantLock需要Lock和UnLock方法来辅助; ReentrantLock reentrantLock = new ReentrantLock(); reentrantLock.lock(); a++; reentrantLock.unlock();一键获取完整项目代码java3,ReentrantLock除了提供Lock和unLock之外还提供了一个不会造成阻塞的tryLock()它会根据是否加锁成功返回true或者false;4,synchronized是非公平锁,而ReentrantLock是默认是非公平锁,但是也提供了公平锁的实现; 5,ReentrantLock的等待通知机制是Condition类,比synchronized的wait和notify功能更强3)线程池博主博主,咱们之前不是讲过线程池了吗,怎么又来一遍呀,确实嗷,上次虽然给大家详细讲过了,但是我们还没有用呀,哈哈哈哈哈,我直接上代码;我们先来简单的版的;public class Demo2 { public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(1111); } } }; ExecutorService executorService = Executors.newFixedThreadPool(1);//创建固定数目的线程池 //ExecutorService executorService1 = Executors.newSingleThreadExecutor(); 创建单线程池 //ExecutorService executorService2 = Executors.newCachedThreadPool(); 创建线程动态增长的线程池 //ScheduledExecutorService service = Executors.newScheduledThreadPool(1); 创建定时线程池 //executorService.submit(runnable); executorService.shutdown(); }}一键获取完整项目代码java我们还可以通过execute来提交任务executorService.execute(runnable);一键获取完整项目代码java都是官方给提供的现成的,我们这会来自己创建;ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,20, 10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());一键获取完整项目代码java这就是我们自己创建的线程池,我们要把所有的参数都填上;1. 任务队列类型队列类型 特点ArrayBlockingQueue 有界队列,需指定容量LinkedBlockingQueue 无界队列(默认使用,可能 OOM)SynchronousQueue 不存储任务,直接提交给线程PriorityBlockingQueue 支持优先级排序2. 拒绝策略策略类 行为AbortPolicy(默认) 抛出 RejectedExecutionExceptionCallerRunsPolicy 由提交任务的线程直接执行任务DiscardPolicy 静默丢弃新任务DiscardOldestPolicy 丢弃队列中最旧的任务,然后重试提交工厂模式那个也是官方给提供的现成的哈哈哈哈,太懒了我; 4)信号量 Semaphore一种计数器,可以表示可用资源的个数;信号量的P操作,申请资源,计数器加一;信号量的V操作,释放资源,计数器减一;如果此时计数器为零,再尝试申请资源就会进入阻塞等待;有一点点像锁;我们使用acquire来申请资源,使用release来释放资源,我们来试试写代码;public class Demo4 { public static void main(String[] args) throws InterruptedException { Semaphore semaphore = new Semaphore(3);//3个可用资源 Runnable runnable = new Runnable() { @Override public void run() { try { System.out.println("申请资源"); semaphore.acquire(); System.out.println("获取到了资源"); Thread.sleep(10000); semaphore.release(); System.out.println("释放资源"); } catch (InterruptedException e) { throw new RuntimeException(e); } } }; Thread t1= new Thread(runnable); Thread t2= new Thread(runnable); Thread t3= new Thread(runnable); t1.start(); t2.start(); t3.start(); Thread t4= new Thread(runnable); t4.start(); t4.join(); }}一键获取完整项目代码java我通过运行这个代码可以看到t1, t2,t3线程获取申请资源之后不释放,t4申请资源就要等着,直到10s之后,t4线程才开始工作;5)CountDownLatch也类似一个计数器,我们传入构造方法的参数就是需要完成的任务个数,完成一个任务就调用countDown()方法,主线程中使用await方法,等待所有任务完成主线程才结束;public class Demo5 { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(10); Runnable runnable = new Runnable() { @Override public void run() { System.out.println(111); countDownLatch.countDown(); } }; for (int i = 0; i < 10; i++) { Thread t = new Thread(runnable); t.start(); } countDownLatch.await(); }}一键获取完整项目代码java2,线程安全的集合类我们之前学习的数据结构大部分是不安全的,我们还想使用之前的数据结构就要做相应的修改;1)多线程环境使用ArrayList1,使用ArrayList的第一种方式就是自己加锁,使用synchronized或者ReentrantLock,来对容易引发线程安全的地方来加以限制;2,就是套壳collections.synchronized(new ArrayList)对于public的方法都加上synchronized;3,使用CopyOnWriteArrayList这个方法是不去加锁的,我们知道,读操作是不影响线程安全的,那么我们在使用ArrayList的时候,我们修改了,我们就再复制一个数组,我们读取的时候只能读到旧的数据或者是已经修改完成的数据,不存在读取修改一半的情况,但是,如果我们的数据很大很大呢,难道我们要一下复制所有的元素吗,是的,就是这么难受,并且多个线程修改数据的时候也可能会发生问题,那我们干嘛要用它,这个是存在特定的使用场景的,服务器如果修改配置了的话是需要重新启动的,我们玩游戏的时候,如果我们要修改设置,比如打开声音,或者设置按键等,难道我们还要关掉游戏吗,我们这时候就是我们给出指令,根据新的设置,服务器就会创建新的哈希数组,来代替旧的数组,完成配置文件的修改,而不是服务器的重启;2)多线程环境下使用队列1. ArrayBlockingQueue 基于数组实现的阻塞队列2. LinkedBlockingQueue 基于链表实现的阻塞队列3. PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列4. TransferQueue 最多只包含⼀个元素的阻塞队列3)多线程环境下使用哈希表哈希表,查找时间复杂度O(1)啊,这必选得拿到多线程中,我们之前讲过,Hashtable是线程安全的,但它只是对HashMap的所有方法加锁,效率肯定是不高的,我们有一个完美的替代品就是ConcurrentHashMap;ConcurrentHashMap是对桶级别加锁,和HashTable不一样,更高效; 大家还记不记得的哈希表是咋样的了, 我们要解决哈希冲突,我们通常是在每个下标中构建链表或者是红黑树;如果链表太长了,我们还涉及到扩容操作; ConcurrentHashMap是对每个下标都加锁的,锁对象就使用表头,当两个线程在不同的下标是,就不会发生锁竞争,当两个线程修改同一个下标时,就存在线程安全性问题了,因为有表头锁的存在就会发生竞争,成功避免了线程安全问题;另外,记录的元素个数size怎么办呢,两个线程同时增加数据,size也会有线程安全问题,还有加锁吗,忘了我们的AtomicIngter了吗,这个原子类也是很好用的呀,大家不要忘了;还有最后一个哈希扩容问题,如果发生扩容就意味着和CopyOnWriteArrayLIst一样了,我们要把原来的数距全部复制过来,那肯定需要很多的时间,所以我们不会一次就把所有元素复制过去,我们会把每次put一些数据的过程中偷偷复制一些数据到新哈希表,就意味着我们把100%的任务分三开,每次执行别的操作都完成一点点的任务,直到扩容完全完毕;————————————————原文链接:https://blog.csdn.net/2301_79083481/article/details/146140421
-
1.前置知识1.1 Tomcat定义:Tomcat是一个开源的轻量级Web(Http)服务器和Servlet容器。它实现了Java Servlet等Java EE规范的核心功能,常用于部署和运行Java Web应用程序 。换言之,Tomcat就是一个严格遵循Servlet规范开发出来的、可以独立安装和运行的Java Web服务器/Servlet容器核心功能:Servlet容器:支持Servlet的执行,处理HTTP请求和响应Web服务器:提供静态资源(如HTML)的访问能力,支持基本的HTTP服务安装与版本对应:tomcat官网:Apache Tomcat®目录结构:bin:存放可执行文件,如startup.batconf:存放配置文件lib:存放Tomcat运行所需的jar文件logs:存储日志文件temp:存放临时文件,如上传的文件或缓存数据webapps:默认web应用部署目录work:服务器的工作目录,存放运行时生成的临时文件(编译文件)1.2 Servlet1.2.1 定义Servlet是Java语言编写的、运行在服务器端的程序,它遵循一套标准的API规范(Tomcat是这套规范的一个具体实现/容器,并提供了让Servlet与前端交互的运行时环境)1.2.2 API示范创建项目/配置文件:(1)在IEDA中创建Maven项目(2)在pom.xml文件中添加servlet依赖(置于< project >< /project >标签下)<dependencies> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <!--servlet依赖版本应与jdk和tomcat的版本相匹配--> <version>6.1.0</version> <scope>provided</scope> </dependency></dependencies>运行本项目xml(3)在main路径下创建webapp/Web-INF/web.xml,在xml文件中添加以下内容<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" ><web-app> <display-name>Archetype Created Web Application</display-name></web-app>运行本项目xml(4)下载插件:Smart Tomcat(为了方便启动项目)API示例:import jakarta.servlet.annotation.WebServlet;import jakarta.servlet.http.HttpServlet;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;//设置访问路径(url)@WebServlet("/method")//继承HttpServlet并重写doGet和doPost方法public class MethodServlet extends HttpServlet { //接收method=post的请求 @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("doPost"); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("doPost"); } //接收method=put的请求 @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("doPut"); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("doPut"); } //接收method=delete的请求 @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("doDelete"); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("doDelete"); }}运行本项目java运行1.2.3 生命周期定义:Servlet 生命周期由 Web容器(如Tomcat)管理,包含加载、初始化、处理请求和销毁四个阶段。每个阶段对应特定的方法调用,开发者可通过重写这些方法实现自定义逻辑1.类加载:Web容器通过类加载器加载 Servlet 类(通常首次请求触发或容器启动时预加载,字节码文件被加载到内存但未实例化)。具体类加载流程请阅读:Java虚拟机——JVM(JavaVirtualMachine)解析一1.5 实例化:确认Servlet类成功加载后立刻执行,在整个Web容器中每种Servlet类(如HttpServlet)只会有一个实例化对象2.初始化:Web容器调用刚刚创建好的Servlet实例的init(ServletConfig config)方法,在整个servlet实例的生命周期中仅调用一次,主要作用是读取配置和资源加载。若初始化失败,抛出ServletException,Servlet不会被加入可用队列3.处理请求:Web容器为每个请求创建线程,调用service(ServletRequest req, ServletResponse res)方法。service() 方法根据HTTP请求类型(get/post)调用doGet()或doPost()4.销毁:Web容器调用 destroy()方法,Servlet 实例被标记为垃圾回收2.SpringBootServlet是Java EE规范中处理Web请求的核心组件,但随着应用复杂度提升,Servlet的直接使用显得笨重。Spring框架通过一系列抽象和扩展,简化了企业级应用开发我们可以用一个非常形象的比喻来贯穿始终:建造一座房子第一阶段:Servlet 时代 - 自己烧砖砌墙目标:建造一个能遮风挡雨的房子(一个能处理HTTP请求的Web应用)你的工作状态:材料: 你有最基础的原材料——泥土(Java 语言)和水(JVM)。你需要自己烧制砖块(编写Servlet类)工具: 只有简单的泥瓦刀(Servlet API)过程:1.你为每一面墙、每一扇门都亲手烧制一块特定的砖(编写 LoginServlet, UserServlet, OrderServlet)2.你亲自规划每块砖的位置(在web.xml中配置大量的 < servlet > 和 < servlet-mapping >)3.你亲自搅拌水泥,一块一块地砌墙(在每个 Servlet 的doGet/doPost 方法中手动解析参数、处理业务、组装 HTML)核心特点:高度灵活: 你可以造出任何形状的砖极其繁琐: 大量重复性劳动(每个 Servlet 都有获取参数、关闭连接等样板代码)难以维护: 砖块之间紧密耦合(对象依赖硬编码),想换一扇窗(修改功能)可能会牵动整面墙依赖外部: 房子建在别人的地上(需要将war包部署到外部的Tomcat服务器)总结:Servlet提供了Web开发的基础能力,但开发效率极低,代码冗余且难以维护第二阶段:Spring 时代 - 使用预制件和设计图纸目标:用更高效、更标准化的方式建造一个结构更好、更易扩展的房子你的工作状态:材料: 你不再烧砖,而是使用工厂提供的标准化预制件(Spring Bean,如 @Controller, @Service)核心创新: 你聘请了一位神奇的管家(IoC 容器)你不再亲自“砌砖”(用new实例化对象),只需告诉管家你需要什么(用@Autowired声明依赖)管家会自动把预制件(Bean)按照图纸(配置)组装好,送到你手上(依赖注入DI)过程:1.一个总大门(DispatcherServlet): 房子只有一个入口,所有访客(请求)都先到这里2.管家调度: 总大门处的接待员(DispatcherServlet)根据访客需求,呼叫房子里对应的专业房间(@Controller中的方法)来接待3.开发者只需专注于房间内的专业服务(业务逻辑),而不用关心访客是怎么进来的核心特点:解耦: 预制件之间是松耦合的,易于更换和测试专业化: AOP(面向切面编程)可以像“装修队”一样,非侵入式地为所有房间统一安装中央空调(日志、安全、事务)效率提升: 避免了大量重复劳动,结构清晰配置复杂: 绘制详细的“组装图纸”(配置 Spring)本身成了一项复杂的工作总结:Spring框架通过IoC/DI和AOP等理念,解决了代码耦合和重复劳动问题,但引入了显著的配置复杂度Spring Boot 1.0.0正式发布于2014年4月1日,标志着该框架的首次稳定版本发布。SpringBoot基于SpringFramework 4进行设计,显著减少了开发者的配置工作量,彻底消除了Spring的配置地狱约定大于配置:约定了默认配置Start机制:是一种依赖管理机制,每个Starter包含特定功能所需的依赖库和自动配置类,开发者只需引入对应Starter即可快速启用功能模块嵌入式容器:内置了Tomcat等嵌入式容器,无需部署war文件到外部容器,直接运行即可启动应用3.Spring Web MVC3.1 概述官方描述:Spring Web MVC是基于Servlet API构建的原始Web框架,并从一开始就在 Spring框架中。正式名称“Spring Web MVC”, 来自其源模块的名称(spring-webmvc),但它通常被称为“Spring MVC”MVC的起源与发展:MVC(Model-View-Controller)模式最初由挪威计算机科学家Trygve Reenskaug于1978年在施乐帕克研究中心(Xerox PARC)提出,目的是为Smalltalk编程语言设计用户界面。其核心思想是将应用程序的逻辑分为三个独立组件:Model:处理数据逻辑和业务规则View:负责数据展示和用户界面Controller:接收用户输入并协调Model与View的交互Spring MVC与MVC的关系:Spring MVC是MVC模式在Spring框架中的具体化,同时扩展了传统MVC的功能以适应现代Web开发需求3.2 必需工具Postman:主要用于 API 的开发和测试。它提供了一个用户友好的界面,支持发送HTTP请求、管理请求历史、自动化测试以及团队协作下载地址:Download PostmanFiddler:是一个网络调试代理工具,主要用于监控和分析HTTP/HTTPS流量。它可以捕获设备与服务器之间的所有请求和响应,支持修改请求、重放请求以及性能分析下载地址:Fiddler - Download3.3 RequestMapping作用:是Spring MVC中最核心、最基础的注解之一,用于将HTTP请求映射到具体的方法上注解级别:类+方法作为类注解:可以为整个类提供一个统一的url前缀(可有可无)作为方法注解:指定该方法负责处理哪个url的请求(强制要求)@RequestMapping("/HelloController")//@RestController声明该类是一个Spring MVC控制器@RestControllerpublic class HelloController { //0.不接收参数 //作为 方法注解 时,@RequestMapping(value = "/hello",method = RequestMethod.GET)等同于@GetMapping(value = "/hello") //设置返回的响应是json格式,produces = "application/json" @RequestMapping(value = "/hello",method = RequestMethod.GET,produces = "application/json") public String hello() { return "{\"Hello\" : World}"; } //1.一个参数 @RequestMapping("/receiveAge1") //不传参或者传递的参数名不匹配时默认为null public String receiveAge1(Integer age) { return "接收到参数 age:" + age; } @RequestMapping("/receiveAge2") //不传参或者传递的参数名不匹配时尝试设置为null,但int无法被设置为null,所以抛出IllegalStateException public String receiveAge2(int age) { return "接收到参数 age:" + age; } //2.接收数组 @RequestMapping("/receiveArray") public String receiveArray(String[] array) { return "接收到参数 array:" + Arrays.toString(array); } //3.接收对象,需要保证传递的参数名称和数量与Java对象保持一致 @RequestMapping("/receivePerson") public String receivePerson(Person person) { return "接收到参数 person:" + person; }}运行本项目java运行3.4 RequestBody作用:将HTTP请求体中的json数据绑定到Java对象(方法注解)注解级别:方法 @RequestMapping("/receivePerson") //@RequestBody接收JSON格式的数据 public String receivePerson(@RequestBody Person person) { return "接收到参数 person:" + person; }运行本项目java运行3.5 RequestParam作用:是Spring MVC框架中从HTTP请求中提取参数/查询字符串的注解,主要用于将请求参数绑定到控制器方法的参数上注解级别:方法 @RequestMapping("/receiveRename") //@RequestParam将url中key=name的查询字符串绑定到控制器的userName参数上 //required = false设置该参数为非必传(默认为true,必传) public String receiveRename(@RequestParam(value = "name",required = false) String userName) { return "接收到参数name:" + userName; }运行本项目java运行注意:需要接收多个同名参数时(如param=value1¶m=value2),直接绑定到List类型需通过该注解明确声明 @RequestMapping("/receiveList1") public String receiveList1(ArrayList<String> list) { //返回的list为空 return "接收到参数 list:" + list; }运行本项目java运行(1)在Spring MVC中,参数绑定机制对集合类型和数组类型的处理存在差异(2)使用ArrayList< String >作为方法参数时,必须显式添加@RequestParam注解,原因如下:默认绑定规则:Spring默认将单个请求参数的值绑定到简单类型(如 String、int)或单个对象。对于集合类型,框架无法自动推断是否需要将多个同名参数合并为集合需要明确指示:@RequestParam注解会告知Spring将同名请求参数的值收集到一个集合中(3)数组(如 String[])无需 @RequestParam 注解即可正确接收,原因如下:内置支持:Spring对数组类型有原生支持,能自动将多个同名请求参数值绑定到数组。这是框架的默认行为,无需额外配置 @RequestMapping("/receiveList2") public String receiveList2(@RequestParam(required = false) ArrayList<String> list) { //正确返回 return "接收到参数 list:" + list; }运行本项目java运行 @RequestMapping("/receiveList3") public String receiveList3(List<String> list) { //报错 return "接收到参数 list:" + list; }运行本项目java运行后端报错:java.lang.IllegalStateException: No primary or single unique constructor found for interface java.util.List。receiveList3方法使用List< String >接口类型而非具体实现类。Spring虽然支持接口类型参数绑定,但需要满足特定条件:必须配合@RequestParam注解使用不能直接使用未注解的接口类型参数报错根本原因:Spring尝试实例化List接口失败(接口不可实例化)3.6 PathVariable作用:用于从URL路径中提取变量值并绑定到方法的参数上注解级别:方法 @RequestMapping("/receivePath/{article}/{blog}") //required = false设置该参数为非必传(默认为true,必传) public String receivePath(@PathVariable(value = "article",required = false)Integer title,@PathVariable(value = "blog",required = false)String content) { return "接收到参数 article:" + title + " blog:" + content; }运行本项目java运行3.7 RequestPart作用:用于处理 HTTP 请求中的 multipart/form-data 类型数据,通常用于文件上传或同时上传文件和其他表单字段的场景注解级别:方法 @RequestMapping("/receiveFile") public String receiveFile(@RequestPart(value = "file",required = false) MultipartFile imgFile,@RequestParam(value = "userName",required = false) String name) { //返回原始的文件名 return "用户:" + name+ ",接收到文件:" +imgFile.getOriginalFilename(); }运行本项目java运行3.8 Controller&ResponseBody&RestControllerController作用:是Spring MVC中的核心注解,用于标记一个类作为Web请求的处理器(声明一个类是一个Spring MVC控制器),负责处理HTTP请求并返回视图注解级别:类ResponseBody作用:指示方法返回值应直接写入HTTP响应体,而非通过视图解析器渲染注解级别:类+方法@RequestMapping("/ControllerResponse")@Controllerpublic class ControllerResponse { //返回视图 @RequestMapping("/HTMLView") public String HTMLView(){ return "/show.html"; } //返回数据 @ResponseBody @RequestMapping("/HTMLData") public String HTMLData(){ return "/show.html"; }}运行本项目java运行RestController作用:是Spring MVC中的一个组合注解,它结合了@Controller和@ResponseBody的功能,标记的类所有方法返回值默认直接作为 HTTP 响应体(JSON/XML 等格式),无需额外视图渲染注解级别:类4.Gitee————————————————原文链接:https://blog.csdn.net/2401_89167985/article/details/148194312
-
【前言】在Java面向对象编程中,抽象类和接口是两个非常重要的概念,它们为代码的抽象化、模块化和可扩展性提供了强大的支持。无论是开发大型企业级应用,还是小型程序,掌握抽象类和接口的使用都至关重要。本文将通过详细的理论讲解、丰富的代码示例、直观的图片以及对比表格,帮助你深入理解Java抽象类和接口的本质与应用。文章目录:一、抽象类1.什么是抽象类2.抽象类语法3.抽象类特征3.1 抽象类不能实例化对象3.2 抽象方法不能是private的3.3 抽象方法不能被final和static修饰,因为抽象方法要被子类重写3.4 抽象类必须被继承,并且继承后子类要重写父类的抽象方法,除非子类也是抽象类,用abstract修饰3.5 抽象类中不⼀定包含抽象⽅法,但是有抽象⽅法的类⼀定是抽象类3.6 抽象类中可以有构造⽅法,供⼦类创建对象时,初始化⽗类的成员变量4.抽象类的作用二、接口1.什么是接口?2.语法规则3.接口的使用4.接口特性4.1 接口是一种引用类型,但是不能直接new接口的对象4.2 接口中的方法会被默认为public abstract,其他修饰符会报错4.3 接口中的方法不能在接口中实现,只能通过接口的类来实现4.4 重写接口方法时,不能使用默认的访问权限4.5 接口中可以有变量,但他会被默认为public static final 变量4.6 接口中不能有静态代码块和构造方法5.实现多个接口6.接口间的继承7.接口使用实例8.Clonable接口和深拷贝8.1 Clonable接口8.2 浅拷贝8.3 深拷贝9.抽象类和接口的区别一、抽象类1.什么是抽象类在面对对象的概念中,所以对象都是通过类来描述的,但并不是所有的类都是用来描述对象的,如果一个类中没有包含足够的信息来描述一个具体的对象,这样的类就是抽象类Dog类和Cat类都属于Animal类,但Dog()和Cat()方法没有实际的行为,可以将他们设计成抽象方法,包含抽象方法的类就是抽象类。2.抽象类语法在Java中,被abstract修饰的类称抽象类,抽象类中,被abstract修饰的方法称为抽象方法,抽象方法不用给出具体的实现体。public abstract class Animal { abstract public void eat();//抽象方法:被abstract修饰,没有方法体 public double show(){ //自己增加的方法 return show1; } protected double show1;//参数}运行本项目java运行3.抽象类特征3.1 抽象类不能实例化对象public abstract class Animal { Animal animal = new Animal();}运行本项目java运行3.2 抽象方法不能是private的public abstract class Animal { abstract private void eat();//抽象方法:被abstract修饰,没有方法体}运行本项目java运行3.3 抽象方法不能被final和static修饰,因为抽象方法要被子类重写abstract final void eat();abstract public static void eat();运行本项目java运行3.4 抽象类必须被继承,并且继承后子类要重写父类的抽象方法,除非子类也是抽象类,用abstract修饰public abstract class Cat extends Animal { @Override public void eat(){ }}运行本项目java运行public abstract class Cat extends Animal { @Override public abstract void eat(); }运行本项目java运行3.5 抽象类中不⼀定包含抽象⽅法,但是有抽象⽅法的类⼀定是抽象类3.6 抽象类中可以有构造⽅法,供⼦类创建对象时,初始化⽗类的成员变量4.抽象类的作用抽象类本身不能被实例化,只能创建子类,并且重写抽象方法,这就起到检验的作用二、接口1.什么是接口?字面意思:我们生活中的水龙头,插座,充电线都是有接口的,才能插上。接口就是公共的行为规范标准,在实现时,只要符合规范标准,就能通用2.语法规则接口的定义和类基本相同,将class换成interface关键字public interface IShape { public static final int SIZE = 100;//接口变量默认public static final public abstract void test();//接口方法默认public abstract}运行本项目java运行【注意】接口命名一般以大写字母I开头;接口不能被实例化;3.接口的使用类与接口之间是implements的关系public class 类名 implenments 接口名{//...}运行本项目java运行这里建立一个IUSB接口和Mouse类public interface IUSB { void openDevice(); void closeDevice();}运行本项目java运行public class Mouse implements IUSB{ @Override public void openDevice() { System.out.println("打开鼠标"); } @Override public void closeDevice() { System.out.println("关闭鼠标"); }}运行本项目java运行4.接口特性4.1 接口是一种引用类型,但是不能直接new接口的对象public class TestUSB { public static void main(String[] args) { USB usb = new USB(); }}运行本项目java运行4.2 接口中的方法会被默认为public abstract,其他修饰符会报错public interface USB { private void openDevice();//private使用错误 void closeDevice();}运行本项目java运行4.3 接口中的方法不能在接口中实现,只能通过接口的类来实现public interface USB { //void openDevice(); void closeDevice(){ System.out.println("关闭USB设备"); }}运行本项目java运行4.4 重写接口方法时,不能使用默认的访问权限public class Mouse implements USB{ @Override public void openDevice() { System.out.println("打开鼠标"); }}运行本项目java运行4.5 接口中可以有变量,但他会被默认为public static final 变量public interface USB { int susu = 250;//默认被final public static修饰 void openDevice(); void closeDevice();}运行本项目java运行4.6 接口中不能有静态代码块和构造方法public interface USB { public USB(){ } { } int susu = 250;//默认被final public static修饰 void openDevice(); void closeDevice();}运行本项目java运行5.实现多个接口一个类可以实现多个接口,这就是继承所做不到的这里创建一个Dog类,可以让他实现多个接口,如IRunning,ISwimming,IFlying。每个接口的抽象方法都有重写,否则必须设置为抽象类AIT + InS快捷键重写public class Dog extends Animal implements IRunning,ISwimming{ @Override public void run() { } @Override public void swim() { }}运行本项目java运行狗:既能跑,又能游;【注意】有了接口之后,我们就不用注意具体类型,只需要关注这个类是否具备某种类型6.接口间的继承在Java中,类和类之间是单继承的,但一个类可以实现多个接口,接口与接口之间可以多继承这段代码就继承了两个接口:游和跑public class Dog extends Animal implements ISwimming,IRunning{ @Override public void run() { } @Override public void swim() { }}运行本项目java运行7.接口使用实例对象之间大小比较:public class Student { public String name; public int score; public Student (String name,int score){ this.name=name; this.score=score; } @Override public String toString() { return super.toString(); }}运行本项目java运行public class Test { public static void main(String[] args) { Student s1 = new Student("小华",20); Student s2 = new Student("小张",10); System.out.println(s1>s2); }}运行本项目java运行这样进行比较会报错,因为没有指定根据分数还是什么来比较,这样不太灵活,我们可以使用接口,如下:使用Comparable接口public class Student implements Comparable<Student>{ public String name; public int age; public Student (String name,int age){ this.name=name; this.age=age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { if(this.age>o.age) return 1; else if(this.age == o.age) return 0; else return -1; } }运行本项目java运行public class Test { public static void main(String[] args) { Student s1 = new Student("小华",20); Student s2 = new Student("小张",12); //System.out.println(s1>s2); if(s1.compareTo(s2)>0){ System.out.println("s1>s2"); } }}运行本项目java运行使用Comparator接口import java.util.Comparator;public class AgeComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.age-o2.age; }}运行本项目java运行public class Test { public static void main(String[] args) { Student s1 = new Student("小华",20); Student s2 = new Student("小张",12); AgeComparator ageComparator=new AgeComparator(); int ret = ageComparator.compare(s1,s2); if(ret>0){ System.out.println("s1>s2"); }运行本项目java运行如果是根据名字来比较的,就要看对应字母的大小8.Clonable接口和深拷贝8.1 Clonable接口Java 中内置了⼀些很有⽤的接⼝,Clonable就是其中之⼀.Object 类中存在⼀个clone⽅法,调⽤这个⽅法可以创建⼀个对象的"拷⻉".但是要想合法调⽤clone⽅法,必须要先实现Clonable接⼝,否则就会抛出CloneNotSupportedException异常.public class Person implements Cloneable{ public String name; public int age; public Person (String name,int age){ this.name=name; this.age=age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}运行本项目java运行public class Test1 { public static void main(String[] args) throws CloneNotSupportedException{ Person person1 = new Person("小华子",20); Person person2 = (Person) person1.clone();//强制类型转换 System.out.println(person2); }}运行本项目java运行受查异常/编译时异常解决方法:8.2 浅拷贝class Money implements Cloneable{ public double money = 9.9;运行本项目java运行 @Override protected Object clone() throws CloneNotSupportedException { return super.clone();运行本项目java运行lic class Test1 { public static void main(String[] args) throws CloneNotSupportedException{ Person person1 = new Person("小华子",20); Person person2 = (Person) person1.clone(); System.out.println(person1.m.money); System.out.println(person2.m.money); System.out.println("=============="); person2.m.money = 19.9; System.out.println(person1.m.money); System.out.println(person2.m.money); }运行本项目java运行通过clone,我们只是拷⻉了Person对象。但是Person对象中的Money对象,并没有拷⻉。通过person2这个引⽤修改了m的值后,person1这个引⽤访问m的时候,值也发⽣了改变。这⾥就是发⽣了浅拷⻉。8.3 深拷贝class Money implements Cloneable{ public double money = 9.9; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}public class Person implements Cloneable{ public String name; public int age; public Money m = new Money(); public Person (String name,int age){ this.name=name; this.age=age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override protected Object clone() throws CloneNotSupportedException { //return super.clone(); Person tmp = (Person) super.clone(); tmp.m = (Money) this.m.clone(); return tmp; }}运行本项目java运行public class Test1 { public static void main(String[] args) throws CloneNotSupportedException{ Person person1 = new Person("小华子",20); Person person2 = (Person) person1.clone(); System.out.println(person1.m.money); System.out.println(person2.m.money); System.out.println("=============="); person2.m.money = 19.9; System.out.println(person1.m.money); System.out.println(person2.m.money); }运行本项目java运行核心代码:9.抽象类和接口的区别抽象类中可以包含普通⽅法和普通字段,这样的普通⽅法和字段可以被⼦类直接使⽤(不必重写),⽽接⼝中不能包含普通⽅法,⼦类必须重写所有的抽象⽅法.No 区别 抽象类(abstract) 接口(interface)1 结构组成 普通类+抽象方法 抽象方法+全局常量2 权限 各种权限 public3 子类使用 使用extends关键字继承抽象类 使用implements关键字实现接口4 关系 一个抽象类可以实现若干接口 接口不能继承抽象类,可extends继承多个父接口5 子类限制 一个子类只能继承一个抽象类 一个子类可以实现多个接口【总结】在 Java 里,抽象类与接口是实现抽象编程的关键工具。抽象类融合普通类与抽象方法,可定义部分实现逻辑,权限灵活;接口由抽象方法和全局常量构成,成员权限默认 public 。子类继承抽象类用 extends ,实现接口用 implements 。关系上,抽象类能实现多个接口,接口可多继承。继承限制方面,子类单继承抽象类,却可多实现接口 。二者各有适用场景,抽象类适合提炼共性、留存部分实现;接口利于规范行为、实现多态解耦。合理运用它们,能让代码架构更清晰、可扩展与可维护,助力构建灵活且健壮的 Java 程序 。————————————————原文链接:https://blog.csdn.net/user340/article/details/148381303
-
引言 在Java编程中,尤其是在使用匿名内部类时,许多开发者都会遇到这样一个限制:从匿名内部类中访问的外部变量必须声明为final或是"等效final"。这个看似简单的语法规则背后,其实蕴含着Java语言设计的深层考量。本文将深入探讨这一限制的原因、实现机制以及在实际开发中的应用。 一、什么是匿名内部类? 在深入讨论之前,我们先简单回顾一下匿名内部类的概念。匿名内部类是没有显式名称的内部类,通常用于创建只使用一次的类实例。 button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("Button clicked!"); }});一键获取完整项目代码java 二、final限制的历史与现状1、Java 8之前的严格final要求在Java 8之前,语言规范强制要求:任何被匿名内部类访问的外部方法参数或局部变量都必须明确声明为final// Java 7及之前版本public void process(String message) { final String finalMessage = message; // 必须声明为final new Thread(new Runnable() { @Override public void run() { System.out.println(finalMessage); // 访问外部变量 } }).start();}一键获取完整项目代码java 2、Java 8的等效final(effectively final)Java 8引入了一个重要改进:等效final的概念如果一个变量在初始化后没有被重新赋值,即使没有明确声明为final,编译器也会将其视为final,这就是"等效final"// Java 8及之后版本public void process(String message) { // message是等效final的,因为它没有被重新赋值 new Thread(new Runnable() { @Override public void run() { System.out.println(message); // 可以直接访问 } }).start(); // 如果取消下面的注释,会导致编译错误 // message = "modified"; // 这会使message不再是等效final的}一键获取完整项目代码java 三、为什么不能修改外部局部变量?1、变量生命周期不一致核心问题:方法参数和局部变量的生命周期与匿名内部类实例的生命周期不一致局部变量存在于栈帧上,其生命周期随着方法的结束而结束但是匿名内部类或 Lambda 表达式可能在方法返回后仍然存在(比如被传递给其他线程、存储在成员变量中等),如果它们直接使用方法的局部变量,而该变量已经被销毁,就会出问题解决方案:为了保证Lambda/内部类能访问到局部变量,Java并没有直接引用该变量,而是捕获了它的值的一个副本(拷贝)public void example() { int value = 10; // 局部变量,存在于栈帧中 Runnable r = new Runnable() { @Override public void run() { // 这里拿到的是value的副本,不是原始变量(引用地址不一样) System.out.println(value); } }; new Thread(r).start(); // 方法结束后,value的栈帧被销毁,value不复存在}一键获取完整项目代码java 2、数据一致性保证如果允许你修改一个外部局部变量,而Lambda使用的是值的拷贝,那么你修改了变量,但 Lambda 内部看不到这个修改(因为用的是拷贝)或者你误以为你修改了 Lambda 使用的那个值,但实际上你修改的是另一个东西允许修改会导致一种错觉:好像Lambda和外部共享了状态,其实不是// 假设Java允许这样做(实际上不允许)public void problematicExample() { int counter = 0; Runnable r = new Runnable() { @Override public void run() { // 假设允许访问,但 value 是拷贝的 0 System.out.println(counter); } }; counter = 5; // 修改原始变量 r.run(); // 输出0,你以为你改成了5}一键获取完整项目代码java 而且匿名内部类可能在另一个线程中执行,而原始变量可能在原始线程中被修改。final限制避免了线程安全问题3、解决方案如果确实需要“共享可变状态”,可以使用一个单元素数组、或者一个Atomicxxx类(如 AtomicInteger),或者将变量封装到一个对象中public class LambdaWorkaround { public static void main(String[] args) { int[] counter = {0}; // 使用数组来包装 Runnable r = () -> { counter[0]++; // 合法:修改的是数组内容,不是外部变量本身 System.out.println("Count: " + counter[0]); }; r.run(); // Count: 1 r.run(); // Count: 2 }}一键获取完整项目代码java 注意:这里你修改的是数组的内容,而不是变量 holder的引用,所以不违反规则 四、底层实现机制Java编译器通过以下方式实现这一特性: 值拷贝:编译器将final变量的值拷贝到匿名内部类中合成字段:在匿名内部类中创建一个合成字段来存储捕获的值构造函数传递:通过构造函数将捕获的值传递给匿名内部类实例可以通过反编译匿名内部类来观察这一机制: // 源代码public class Outer { public void method(int param) { Runnable r = new Runnable() { @Override public void run() { System.out.println(param); } }; }}一键获取完整项目代码java 反编译后的内部类和内部类大致如下:(参数自动添加final,内部类通过构造方法引入变量) // 反编译原始类 public class Outer { public void method(final int var1) { Runnable var10000 = new Runnable() { public void run() { System.out.println(var1); } }; }} // 反编译后能看到单独生成的匿名内部类class Outer$1 implements Runnable { Outer$1(Outer var1, int var2) { this.this$0 = var1; this.val$param = var2; } public void run() { System.out.println(this.val$param); }}一键获取完整项目代码java 五、常见问题与误区1、为什么实例变量没有这个限制?因为实例变量(成员变量)存储在堆(Heap)中,和对象生命周期一致而局部变量存储在栈(Stack)中,方法结束后就被销毁了Java 为保证 Lambda / 匿名内部类能安全访问变量,对这两者的处理方式完全不同public class Outer { private int instanceVar = 10; // 实例变量 public void method() { new Thread(new Runnable() { @Override public void run() { instanceVar++; // 可以直接修改实例变量 } }).start(); }}一键获取完整项目代码java 2、等效final的实际含义等效final意味着变量虽然没有明确声明为final,但符合final的条件:只赋值一次且不再修改public void effectivelyFinalExample() { int normalVar = 10; // 等效final final int explicitFinal = 20; // 明确声明为final // 两者都可以在匿名内部类中使用 Runnable r = () -> { System.out.println(normalVar + explicitFinal); }; // 如果这里修改变量,同样会编译报错 // normalVar = 5;}———————————————— 原文链接:https://blog.csdn.net/qq_35512802/article/details/151362542
-
前言 自古以来,中秋佳节便与圆月紧密相连,成为人们寄托思念与团圆之情的象征。在民间流传着这样一种说法:“十五的月亮十六圆”,仿佛这已成为一种铁律,深入人心。然而,这种说法是否真的站得住脚呢?在这背后,隐藏着怎样的天文奥秘?又是否可以通过科学的方法来验证这一传统观念呢?在科技飞速发展的今天,我们不妨借助编程的力量,运用Java语言来实证求解,揭开中秋满月的真相。 中秋赏月的传统由来已久,早在《周礼》中就有“中秋夜迎寒”的记载,而到了唐代,中秋赏月、玩月的风俗开始盛行。文人墨客们更是留下了许多描写中秋月夜的佳作,如苏轼的“但愿人长久,千里共婵娟”,将中秋的月与人间的思念紧密相连,赋予了中秋月深厚的文化内涵。在这样的文化背景下,“十五的月亮十六圆”这一说法也逐渐流传开来,成为人们茶余饭后的话题之一。然而,这种说法真的准确无误吗? 本文通过Java实证求解中秋满月的时间,不仅可以验证传统的说法,还可以更深入地了解天文学中的相关知识。这不仅是一次对传统观念的挑战,也是一次对科学方法的实践。无论最终的结果如何,这一过程都将让我们对中秋满月有更深刻的认识,也将让我们感受到科学的魅力和力量。在接下来的章节中,我们将详细介绍如何使用Java语言进行天文数据的处理和计算,以及如何通过模拟实验来验证“十五的月亮十六圆”这一说法。我们将逐步展开这一探索之旅,最终揭示中秋满月的真相。让我们一起踏上这段充满趣味和挑战的旅程,用科学的视角重新审视中秋的圆月,探索其中隐藏的奥秘。一、天文上的满月 在天文学中,月亮的圆缺变化是一个非常有趣且复杂的自然现象,这种变化主要源于月球绕地球的公转运动。月球绕地球运行一周的时间大约是29.5天,这个周期被称为一个“朔望月”。在这个周期中,月球相对于太阳的位置不断变化,从而导致我们从地球上看到的月相也随之改变。博主不是专业天文专业,这里仅分享一些简单的满月基础知识,让大家有一个概念。1、形成原理及定义 说到满月就必须提及月相,月相的形成是由于太阳光照射月球的不同部分,而我们从地球上看到的只是月球被太阳照亮的那一部分。随着月球绕地球的公转,被太阳照亮的部分逐渐增加,依次出现“娥眉月”“上弦月”“凸月”“满月”“下弦月”“残月”等不同的月相。其中满月是指月球完全被太阳照亮的那一面朝向地球,此时月球与太阳在地球的两侧,三者几乎在一条直线上。理论上,满月应该出现在农历的十五或十六,但实际的情况并非总是如此。由于月球的公转轨道是椭圆形的,且受到多种因素的影响,如地球的引力、太阳的引力等,月球的实际运行轨迹并非完全规律,因此满月出现的时间也会有所变化。2、出现时间及观测 “十五的月亮十六圆”这一说法广为流传,但实际上满月并不总是出现在农历的十六。根据天文观测数据,满月可能出现在农历的十四到十七之间的任何一天。例如,在某些年份,满月可能出现在农历十四的晚上,而在另一些年份,满月可能出现在农历十七的早晨。这种变化是由于月球的公转速度和轨道形状的不规则性所导致的。满月是观测月球的最佳时机之一,因为此时月球的整个盘面都被照亮,可以清晰地看到月球表面的山脉、陨石坑和月海等特征。在满月期间,月球的亮度会达到最大,这使得它在夜空中格外明亮。3、文化意义 在许多文化中,满月都具有重要的象征意义。在中国文化中,满月象征着团圆和完满,因此中秋节成为了家人团聚的重要节日。在西方文化中,满月也常常与神秘和浪漫联系在一起,许多文学作品和民间传说都以满月为背景。二、Java模拟月满计算 随着计算机技术的发展,我们有了更强大的工具来探索和验证这些天文现象。Java作为一种广泛使用的编程语言,具有强大的功能和灵活性,可以用来编写各种复杂的算法和程序。在本研究中,我们将利用Java语言编写程序,通过计算月球在不同时间的位置,来确定中秋满月的具体时间。我们将收集多年来的天文数据,包括月球的公转周期、轨道参数等,然后利用这些数据进行模拟计算。通过这种方式,我们可以得到一个较为准确的中秋满月时间表,从而验证“十五的月亮十六圆”这一说法的准确性。1、整体实现逻辑 使用Java求解中秋满月整体时间逻辑如下:public class MidAutumnFullMoonCalculator { // 主计算方法 public static Date calculateFullMoonTime(int year, int month, int day) { ... } // 核心天文算法 private static double calculateFullMoonJulianDay(double jd) { ... } // 辅助方法 private static double normalizeAngle(double angle) { ... } private static double calendarToJulianDay(Calendar cal) { ... } private static Calendar julianDayToCalendar(double jd) { ... }}一键获取完整项目代码java2、主计算方法详解 功能:这是程序的入口点,接收农历中秋的公历日期,返回精确的满月时刻,核心方法如下:/** * -计算指定农历中秋日期的月亮最圆时刻 * @param year 年份 * @param month 农历月份(八月) * @param day 农历日期(十五) * @return 月亮最圆时刻的Date对象 */public static Date calculateFullMoonTime(int year, int month, int day) { // 创建农历中秋日期(使用中午12点作为基准时间) Calendar midAutumnDate = Calendar.getInstance(); midAutumnDate.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); midAutumnDate.set(year, month - 1, day, 12, 0, 0); // month-1因为Calendar月份从0开始 // 计算精确的满月时刻 return calculatePreciseFullMoonTime(midAutumnDate);}一键获取完整项目代码java 参数说明:year:公历年份(如2024)month:公历月份(如9)day:公历日期(如17) 处理流程:创建Calendar对象,设置为北京时间将时间设为中午12点作为计算基准调用核心算法计算精确的满月时刻3、核心天文算法详解 核心算法是相关计算中最核心的内容,主要包括儒略日的计算、时间参数的计算、天文参数计算和周期项修正等内容,这里的天文计算采用近似计算,如需精度计算,请使用更精准的天文算法。3.1 儒略日计算基础/** * -计算满月时刻的儒略日 * -基于Jean Meeus的天文算法 */private static double calculateFullMoonJulianDay(double jd) { // 计算从2000年1月6日(基准新月)开始的月相周期数 double k = Math.floor((jd - 2451550.09765) / 29.530588853); // 满月对应的k值(新月+0.5) k = k + 0.5; // 计算T(儒略世纪数) double T = k / 1236.85; //----其它计算}一键获取完整项目代码java 儒略日(Julian Day):天文学中常用的连续时间计数法,从公元前4713年1月1日格林尼治平午开始计算。月相周期数k:2451550.09765:2000年1月6日18:14的儒略日,作为一个基准新月时刻29.530588853:一个朔望月的平均长度(天)k:从基准时间开始经过的月相周期数k + 0.5:从新月到满月是半个周期3.2 时间参数计算// 计算T(儒略世纪数)double T = k / 1236.85; // 计算基础儒略日double JDE = 2451550.09765 + 29.530588853 * k + 0.0001337 * T * T - 0.000000150 * T * T * T + 0.00000000073 * T * T * T * T;一键获取完整项目代码java T(儒略世纪数):以36525天为一世纪的时间单位,用于高阶项的计算。 (儒略历书日):考虑了长期项修正的基础满月时刻。3.3 天文参数计算 // 计算太阳平近点角 double M = normalizeAngle(2.5534 + 29.10535669 * k - 0.0000218 * T * T - 0.00000011 * T * T * T); // 计算月亮平近点角double Mprime = normalizeAngle(201.5643 + 385.81693528 * k + 0.1017438 * T * T + 0.00001239 * T * T * T - 0.000000058 * T * T * T * T); // 计算月亮升交点平黄经double F = normalizeAngle(160.7108 + 390.67050274 * k - 0.0016341 * T * T - 0.00000227 * T * T * T + 0.000000011 * T * T * T * T); // 计算Omega(月亮轨道升交点经度)double Omega = normalizeAngle(124.7746 - 1.56375580 * k + 0.0020691 * T * T + 0.00000215 * T * T * T);一键获取完整项目代码java 天文参数说明:M(太阳平近点角):太阳在轨道上的平均位置角度系数:2.5534° + 29.10535669°/周期反映地球公转轨道的椭圆性影响M'(月亮平近点角):月亮在轨道上的平均位置角度系数:201.5643° + 385.81693528°/周期反映月球公转轨道的椭圆性影响F(月亮升交点平黄经):月球轨道与黄道交点的平均位置系数:160.7108° + 390.67050274°/周期反映月球轨道平面的进动Ω(月亮轨道升交点经度):更精确的轨道交点位置系数:124.7746° - 1.56375580°/周期3.4 周期项修正计算// 转换为弧度double M_rad = Math.toRadians(M);double Mprime_rad = Math.toRadians(Mprime);double F_rad = Math.toRadians(F);double Omega_rad = Math.toRadians(Omega);// 计算周期项修正double correction = 0;// 主要修正项correction += -0.40720 * Math.sin(Mprime_rad);correction += 0.17241 * 0.016708617 * Math.sin(M_rad);correction += 0.01608 * Math.sin(2 * Mprime_rad);correction += 0.01039 * Math.sin(2 * F_rad);correction += 0.00739 * 0.016708617 * Math.sin(Mprime_rad - M_rad);correction += -0.00514 * 0.016708617 * Math.sin(Mprime_rad + M_rad);correction += 0.00208 * 0.016708617 * 0.016708617 * Math.sin(2 * M_rad);correction += -0.00111 * Math.sin(Mprime_rad - 2 * F_rad);correction += -0.00057 * Math.sin(Mprime_rad + 2 * F_rad);correction += 0.00056 * 0.016708617 * Math.sin(2 * Mprime_rad + M_rad);correction += -0.00042 * Math.sin(3 * Mprime_rad);correction += 0.00042 * 0.016708617 * Math.sin(M_rad + 2 * F_rad);correction += 0.00038 * 0.016708617 * Math.sin(M_rad - 2 * F_rad);correction += -0.00024 * 0.016708617 * Math.sin(2 * Mprime_rad - M_rad);correction += -0.00017 * Math.sin(Omega_rad);correction += -0.00007 * Math.sin(Mprime_rad + 2 * M_rad);correction += 0.00004 * Math.sin(2 * Mprime_rad - 2 * F_rad);correction += 0.00004 * Math.sin(3 * M_rad);correction += 0.00003 * Math.sin(Mprime_rad + M_rad - 2 * F_rad);correction += 0.00003 * Math.sin(2 * Mprime_rad + 2 * F_rad);correction += -0.00003 * Math.sin(Mprime_rad + M_rad + 2 * F_rad);correction += 0.00003 * Math.sin(Mprime_rad - M_rad + 2 * F_rad);correction += -0.00002 * Math.sin(Mprime_rad - M_rad - 2 * F_rad);correction += -0.00002 * Math.sin(3 * Mprime_rad + M_rad);correction += 0.00002 * Math.sin(4 * Mprime_rad); // 应用修正double preciseJDE = JDE + correction;一键获取完整项目代码java修正项原理:每个修正项都对应一个特定的天文效应:-0.40720 × sin(M'):月球椭圆轨道的主要修正(中心差)0.17241 × e × sin(M):地球轨道偏心率对月相的影响0.01608 × sin(2M'):月球轨道的二阶椭圆项0.01039 × sin(2F):月球轨道倾角的影响0.00739 × e × sin(M' - M):地球和月球轨道相互影响-0.00514 × e × sin(M' + M):地球和月球轨道的组合效应e = 0.016708617:地球轨道偏心率这些修正项基于布朗月球运动理论,考虑了月球轨道的各种摄动因素。4、辅助方法详解 本小节将对辅助方法进行简单介绍。4.1 角度标准化/** * -将角度标准化到0-360度范围内 */private static double normalizeAngle(double angle) { angle = angle % 360; if (angle < 0) { angle += 360; } return angle;}一键获取完整项目代码java 功能:将角度限制在0-360度范围内,避免数值溢出。4.2 日历与儒略日转换/** * -将Calendar转换为儒略日 */private static double calendarToJulianDay(Calendar cal) { int year = cal.get(Calendar.YEAR); int month = cal.get(Calendar.MONTH) + 1; int day = cal.get(Calendar.DAY_OF_MONTH); int hour = cal.get(Calendar.HOUR_OF_DAY); int minute = cal.get(Calendar.MINUTE); int second = cal.get(Calendar.SECOND); double decimalHour = hour + minute / 60.0 + second / 3600.0; if (month <= 2) { year--; month += 12; } int a = year / 100; int b = 2 - a + a / 4; return Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + decimalHour / 24.0 + b - 1524.5;}一键获取完整项目代码java 转换公式:标准的天文儒略日计算公式,考虑了:闰年规则格里高利历改革(1582年)时间的小数部分处理4.3 儒略日转日历/** * -将儒略日转换为Calendar */private static Calendar julianDayToCalendar(double jd) { jd += 0.5; double z = Math.floor(jd); double f = jd - z; double a; if (z < 2299161) { a = z; } else { double alpha = Math.floor((z - 1867216.25) / 36524.25); a = z + 1 + alpha - Math.floor(alpha / 4); } double b = a + 1524; double c = Math.floor((b - 122.1) / 365.25); double d = Math.floor(365.25 * c); double e = Math.floor((b - d) / 30.6001); double day = b - d - Math.floor(30.6001 * e) + f; int month = (int) (e < 14 ? e - 1 : e - 13); int year = (int) (month > 2 ? c - 4716 : c - 4715); double time = day - Math.floor(day); int hour = (int) (time * 24); int minute = (int) ((time * 24 - hour) * 60); int second = (int) Math.round((((time * 24 - hour) * 60 - minute) * 60)); // 处理秒数进位 if (second >= 60) { second = 0; minute++; } if (minute >= 60) { minute = 0; hour++; } Calendar cal = Calendar.getInstance(); cal.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); cal.set(year, month - 1, (int) Math.floor(day), hour, minute, second); cal.set(Calendar.MILLISECOND, 0); return cal;}一键获取完整项目代码java 关键点:jd += 0.5:儒略日从中午开始,调整为从午夜开始处理格里高利历改革(1582年10月4日后跳过10天)精确的时间分量计算三、近年中秋满月计算及对比 本节将结合实例对每年的中秋月满时间进行计算,通过本小节就可以获取每年的满月日期和具体的时间,并且与官方提供的时间进行对比,大家通过对比就可以知晓问题的开始,是不是所有的月亮都是十六圆了。 1、近年中秋满月计算/** * -测试方法 - 计算未来几年的中秋节月亮最圆时刻 */public static void main(String[] args) { // 已知的农历中秋日期(公历日期) int[][] midAutumnDates = { {2019, 9, 13}, // 2019年中秋节 {2020, 10, 1}, // 2020年中秋节 {2021, 9, 21}, // 2021年中秋节 {2022, 9, 10}, // 2022年中秋节 {2023, 9, 29}, // 2023年中秋节 {2024, 9, 17}, // 2024年中秋节 {2025, 10, 6}, // 2025年中秋节 {2026, 9, 25}, // 2026年中秋节 }; SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒"); sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); System.out.println("中秋节月亮最圆时刻计算结果:"); System.out.println("================================="); for (int[] date : midAutumnDates) { int year = date[0]; int month = date[1]; int day = date[2]; Date fullMoonTime = calculateFullMoonTime(year, month, day); System.out.printf("%d年中秋节(公历%d月%d日)月亮最圆时刻: %s%n", year, month, day, sdf.format(fullMoonTime)); }}一键获取完整项目代码java 接下来我们在IDE中运行意以上成就可以得到以下结果: 以上就是实现一个从2019年到2026年,跨度为7年的中秋满月计算过程。2、近年计算与公布时间对比 通过以上7年的计算,再结合官方公布的满月日期及时刻,来对比一下我们的计算方法与官方公布的时间相差是多少?年份 中秋(公历) 满月时间(本地) 是否当天 满月时间(官方公布) 误差2019 2019-9-13 09月14日 08时39分21秒 否(十六) 9月14日12时33分 3时54分2020 2020-10-1 10月02日 01时28分17秒 否(十六) 10月2日 05时5分 3时37分2021 2021-9-21 09月21日 03时58分38秒 是(十五) 9月21日 07时54分 3时56分2022 2022-9-10 09月10日 13时34分12秒 是(十五) 9月10日 17时59分 4时25分2023 2023-9-29 09月29日 13时49分05秒 是(十五) 9月29日 17时58分 4时8分2024 2024-9-17 09月18日 06时15分39秒 否(十六) 9月18日 10时34分 4时19分2025 2024-10-06 10月07日 07时41分10秒 否(十六) 10月7日 11时48分 4时7分 结合近七年的满月日期及时刻来看,并不是所有的中秋月圆都是十六圆,有的是当天就圆了。所以,从这个角度来定义,十五的月亮十六圆可不是准确的哦。通过这种本地近似的计算,虽然在具体的时刻上有一些误差,但是日期是与官方公布的是完全一致的,时刻的误差通过近7年的验证,相差时间在4个小时左右,所以未来可以结合更长序列的时间进行相应的修正。四、总结 以上就是本文的主要内容,本文通过Java实证求解中秋满月的时间,不仅可以验证传统的说法,还可以更深入地了解天文学中的相关知识。这不仅是一次对传统观念的挑战,也是一次对科学方法的实践。无论最终的结果如何,这一过程都将让我们对中秋满月有更深刻的认识,也将让我们感受到科学的魅力和力量。通过Java满月近似求解,并结合2019年到2025年的中秋满月日期时刻的计算,得出了重要的一个结论,十五的月亮不一定十六圆,通过严谨的程序计算得到的数据支撑。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。————————————————原文链接:https://blog.csdn.net/yelangkingwuzuhu/article/details/152717752
-
一、AQS 是什么?AQS,全称 AbstractQueuedSynchronizer,即抽象队列同步器。抽象:它是一个抽象类,本身不能直接实例化,需要子类去继承它,并实现其保护方法来管理同步状态。队列:它内部维护了一个先进先出(FIFO)的等待队列,用于存放那些没有抢到锁的线程。同步器:它是构建锁和其他同步组件(如 Semaphore、CountDownLatch 等)的基础框架。核心思想:AQS 使用一个整型的 volatile 变量(state) 来表示同步状态(例如,锁被重入的次数、许可的数量等),并通过一个内置的 FIFO 队列来完成资源获取线程的排队工作。设计模式:AQS 是 模板方法模式 的经典应用。父类(AQS)定义了骨架和核心算法,而将一些关键的操作以 protected 方法的形式留给子类去实现。这样,实现一个自定义同步器只需要关注如何管理 state 状态即可,至于线程的排队、等待、唤醒等复杂操作,AQS 已经帮我们完成了。二、AQS 的核心结构AQS 的核心可以概括为三部分:同步状态(state)、等待队列 和 条件队列。1. 同步状态(State)这是一个 volatile int 类型的变量,是 AQS 的灵魂。private volatile int state;一键获取完整项目代码它的具体含义由子类决定,非常灵活:在 ReentrantLock 中,state 表示锁被同一个线程重复获取的次数。state=0 表示锁空闲,state=1 表示锁被占用,state>1 表示锁被重入。在 Semaphore 中,state 表示当前可用的许可数量。在 CountDownLatch 中,state 表示计数器当前的值。对 state 的操作是原子的,通过 getState(), setState(int newState), compareAndSetState(int expect, int update) 等方法进行。2. 等待队列(CLH 队列的变体)这是一个双向链表,是 AQS 实现阻塞锁的关键。当线程请求共享资源失败时,AQS 会将当前线程以及等待状态等信息构造成一个节点(Node) 并将其加入队列的尾部,同时阻塞该线程。头节点(Head):指向获取到资源的线程所在的节点。头节点不持有线程,是一个“虚节点”。尾节点(Tail):指向队列中最后一个节点。当一个线程释放资源时,它会唤醒后继节点,后继节点成功获取资源后,会将自己设置为新的头节点。主要原理图如下:AQS 使用一个 Volatile 的 int 类型的成员变量来表示同步状态,通过内置的 FIFO 队列来完成资源获取的排队工作,通过 CAS 完成对 State 值的修改。3. 条件队列(Condition Object)AQS 内部类 ConditionObject 实现了 Condition 接口,用于支持 await/signal 模式的线程间协作。每个 ConditionObject 对象都维护了一个自己的单向链表(条件队列)。当线程调用 Condition.await() 时,会释放锁,并将当前线程构造成节点加入条件队列,然后阻塞。当线程调用 Condition.signal() 时,会将条件队列中的第一个等待节点转移到 AQS 的等待队列中,等待重新获取锁。注意:一个 AQS 实例可以对应多个 Condition 对象(即多个条件队列),但只有一个等待队列。三、AQS 的设计与关键方法AQS 将资源获取的方式分为两种:独占模式(Exclusive):一次只有一个线程能执行,如 ReentrantLock。共享模式(Shared):多个线程可以同时执行,如 Semaphore、CountDownLatch。AQS 提供了顶层的入队和出队逻辑,而将尝试获取资源和尝试释放资源的具体策略留给了子类。需要子类重写的关键方法(Protected)这些方法在 AQS 中是 protected 的,默认抛出 UnsupportedOperationException。独占模式:boolean tryAcquire(int arg):尝试以独占方式获取资源。成功返回 true,失败返回 false。boolean tryRelease(int arg):尝试以独占方式释放资源。成功返回 true,失败返回 false。共享模式:int tryAcquireShared(int arg):尝试以共享方式获取资源。负数表示失败;0 表示成功,但后续共享获取可能失败;正数表示成功,且后续共享获取可能成功。boolean tryReleaseShared(int arg):尝试以共享方式释放资源。其他:boolean isHeldExclusively():当前同步器是否在独占模式下被线程占用。在 Condition 相关操作中会用到。供外部调用的重要方法(Public)这些是模板方法,子类一般不重写,使用者(或子类)直接调用。独占模式:void acquire(int arg):以独占模式获取资源,忽略中断。如果获取失败,会进入等待队列。void acquireInterruptibly(int arg):同上,但响应中断。boolean tryAcquireNanos(int arg, long nanosTimeout):在 acquireInterruptibly 基础上增加了超时限制。boolean release(int arg):以独占模式释放资源。共享模式:void acquireShared(int arg):以共享模式获取资源。void acquireSharedInterruptibly(int arg):响应中断的共享获取。boolean tryAcquireSharedNanos(int arg, long nanosTimeout):带超时的共享获取。boolean releaseShared(int arg):以共享模式释放资源。四、源码级工作流程解析(以 acquire 为例)我们来看一下最核心的 acquire 方法,它展示了 AQS 的完整工作流程:public final void acquire(int arg) { if (!tryAcquire(arg) && // 1. 尝试直接获取资源(子类实现) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 获取失败,则加入队列;3. 在队列中自旋/阻塞等待 selfInterrupt(); // 如果在等待过程中被中断,补上中断标记}一键获取完整项目代码tryAcquire(arg):这是子类实现的方法。比如在 ReentrantLock 的非公平锁实现中,它会直接尝试使用 CAS 修改 state,如果成功,就将当前线程设置为独占线程。如果 tryAcquire 成功,整个 acquire 方法就结束了,线程继续执行。如果失败,进入下一步。addWaiter(Node.EXCLUSIVE):创建一个代表当前线程的 Node 节点,模式为独占模式(Node.EXCLUSIVE)。通过 CAS 操作,快速地将这个新节点设置为尾节点。如果失败,则进入 enq(node) 方法,通过自旋 CAS 的方式确保节点被成功添加到队列尾部。acquireQueued(final Node node, int arg):这是核心中的核心。节点入队后,会在这个方法里进行自旋(循环)等待。在循环中,它会检查自己的前驱节点是不是头节点(p == head)。如果是,说明自己是队列中第一个等待的线程,会再次调用 tryAcquire 尝试获取资源(因为此时锁可能刚好被释放了,这是一个避免不必要的线程挂起、提高性能的优化)。如果获取成功,就将自己设为新的头节点,然后返回。如果前驱不是头节点,或者再次尝试获取失败,则会调用 shouldParkAfterFailedAcquire 方法,检查并更新前驱节点的状态(比如将其 waitStatus 设置为 SIGNAL,表示“当你释放锁时,需要唤醒我”)。如果一切就绪,就调用 parkAndCheckInterrupt() 方法,使用 LockSupport.park(this) 阻塞(挂起)当前线程。当线程被唤醒后(通常是由前驱节点释放锁时 unpark 的),会再次检查自己是否是头节点的后继,并重复上述自旋过程,直到成功获取资源。selfInterrupt():如果在等待过程中线程被中断,acquireQueued 方法会返回 true,这里会调用 selfInterrupt 补上中断标志,因为 AQS 在 acquire 过程中是忽略中断的。释放流程(release)相对简单:public final boolean release(int arg) { if (tryRelease(arg)) { // 1. 子类尝试释放资源 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 2. 唤醒后继节点 return true; } return false;}一键获取完整项目代码unparkSuccessor 会找到队列中第一个需要唤醒的线程(通常是头节点的下一个有效节点),然后调用 LockSupport.unpark(s.thread) 将其唤醒。五、AQS 的应用举例AQS 是 JUC 包的基石,几乎所有的同步工具都基于它:ReentrantLock:使用 AQS 的独占模式,state 表示重入次数。ReentrantReadWriteLock:读写锁。AQS 的 state 高16位表示读锁状态,低16位表示写锁状态。Semaphore:使用 AQS 的共享模式,state 表示可用许可数。CountDownLatch:使用 AQS 的共享模式,state 表示计数器值。countDown() 是 releaseShared,await() 是 acquireShared。ThreadPoolExecutor:其内部的工作线程 Worker 类,也继承了 AQS,用于实现独占锁,来判断线程是否空闲。六、总结AQS 的核心贡献在于,它提供了一个强大的框架,将复杂的线程排队、阻塞、唤醒等底层操作封装起来,让同步器的开发者只需要关注一个核心问题:如何管理那个 state 变量。它的优点:极大地降低了构建锁和同步器的复杂度。性能高效:通过自旋、CAS 等无锁编程技术,减少了线程上下文切换的开销。灵活强大:通过两种模式的区分,可以构建出各种复杂的同步工具。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/153201201
-
Spring AI 1.0 GA 深度解析:Java生态的AI革命已来作者按:在经历了8个里程碑版本的迭代后,Spring AI 1.0 GA于2025年5月20日正式发布。作为Spring生态的官方AI框架,它标志着Java开发者正式迈入AI原生应用时代。本文基于生产环境实践,深度剖析其核心架构与落地策略。一、为什么Spring AI是Java开发者的AI"入场券"?1.1 从Spring Boot到AI Boot:技术演进的必然想象一下这个场景:你的团队需要在现有Spring微服务中集成AI能力,但面对OpenAI、通义千问、Claude等不同API时,每个都需要单独适配。更痛苦的是,当业务需要从聊天机器人升级到RAG知识库,再到多Agent协作时,代码重构的噩梦就开始了。这正是Spring AI要解决的问题。它就像当年Spring整合JDBC、Hibernate一样,现在统一了AI领域的"混沌"。基于我们的生产实践,使用Spring AI后:代码量减少60%,模型切换成本从2人周降至2小时。 1.2 企业级AI的三座大山传统AI集成面临的核心痛点: 模型碎片化:OpenAI、Claude、通义千问各自为政 技术栈割裂:Python AI与Java业务系统难以融合 生产就绪度低:缺乏监控、限流、熔断等企业级能力 运行本项目Spring AI通过三层抽象完美解决:ChatClient:统一所有大模型调用VectorStore:屏蔽向量数据库差异Advisor:AOP式增强AI能力二、核心架构:比LangChain更懂Java的设计哲学2.1 ChatClient:AI世界的JDBCTemplate@RestControllerpublicclassSmartController{privatefinalChatClient chatClient;publicSmartController(ChatClient.Builder builder){this.chatClient = builder .defaultSystem("你是一个专业的Java架构师").build();}@GetMapping("/ai/code-review")publicCodeReviewreviewCode(@RequestParamString code){return chatClient.prompt().user("请分析这段代码的设计模式:{code}", code).call().entity(CodeReview.class);// 直接返回结构化对象}}运行本项目性能数据:在我们的压测中,ChatClient相比原生HTTP调用:平均延迟降低35%,内存使用减少40%2.2 向量数据库的"USB-C"接口Spring AI支持20种向量数据库的统一抽象,性能对比实测:数据库 百万级向量QPS 延迟P99 最佳场景Milvus 1200 15ms 大规模图像检索Weaviate 800 25ms 知识图谱场景Chroma 200 80ms 原型开发PGVector 500 40ms 已有PostgreSQL配置示例:spring:ai:vectorstore:milvus:host: localhost port:19530index-type: HNSW metric-type: COSINE 运行本项目2.3 Advisor:AI领域的Spring AOP通过拦截器链实现模型增强,核心Advisor对比:Advisor类型 作用 性能开销QuestionAnswerAdvisor RAG检索增强 +15msChatMemoryAdvisor 会话记忆 +5msSafeGuardAdvisor 敏感词过滤 +2ms三、生产级RAG架构实战3.1 亿级文档的RAG流水线基于某金融客户的真实案例,架构演进过程:阶段1:简单RAG(100万文档)@BeanpublicRetrievalAugmentationAdvisorragAdvisor(){returnRetrievalAugmentationAdvisor.builder().documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(milvusVectorStore).similarityThreshold(0.75).topK(5).build()).queryAugmenter(ContextualQueryAugmenter.builder().maxTokens(500).build()).build();}运行本项目阶段2:分布式RAG(1000万文档)引入MultiQueryExpansion:提升召回率30%DocumentReRanker:使用Cross-Encoder重排序混合检索:向量+BM25混合打分阶段3:实时RAG(亿级文档)增量索引:Kafka实时同步文档变更缓存策略:Redis缓存热点查询负载均衡:多向量库分片存储3.2 性能调优秘籍向量维度优化:实测发现:1536维vs768维在准确率上仅差2%,但存储减少50%建议:业务场景优先768维,精度敏感再用1536维分块策略:// 智能分块:按语义完整性切割DocumentSplitter splitter =newTokenTextSplitter(800,// 每块最大token200,// 重叠token5,// 最小块数10000// 最大token);运行本项目四、Function Calling:让AI"动手"的魔法4.1 从天气预报到股票交易工具定义:@ServicepublicclassStockService{@Tool(description ="获取股票实时价格")publicStockPricegetPrice(String symbol){return webClient.get().uri("/stock/{symbol}", symbol).retrieve().bodyToMono(StockPrice.class).block();}@Tool(description ="执行股票交易")publicTradeResultexecuteTrade(@ToolParam(description ="股票代码")String symbol,@ToolParam(description ="交易数量")int quantity,@ToolParam(description ="交易类型")TradeType type){// 实际交易逻辑}}运行本项目实测数据:工具调用成功率:99.2%(基于10000次调用)平均响应时间:180ms(含API往返)错误恢复:自动重试3次,指数退避4.2 复杂业务流程编排Agent工作流模式:模式类型 适用场景 代码复杂度Chain 顺序任务流 Parallel 批量处理 Routing 智能分流 Orchestrator 动态任务分解 实战案例:订单处理AgentpublicclassOrderAgent{publicvoidprocessOrder(String orderRequest){OrchestratorWorkersWorkflow workflow =newOrchestratorWorkersWorkflow(chatClient);// 1. 分析订单 -> 2. 检查库存 -> 3. 计算价格 -> 4. 生成发货单OrderResult result = workflow.process(orderRequest);}}运行本项目五、MCP协议:AI生态的TCP/IP5.1 什么是MCP?模型上下文协议(Model Context Protocol)就像AI世界的HTTP协议,它让任何AI应用都能:发现可用工具标准化调用方式安全权限控制Spring AI MCP架构:┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ AI App │────│ MCP Client │────│ MCP Server │ │ (ChatGPT) │ │ (Spring AI) │ │ (Weather) │ └─────────────┘ └──────────────┘ └─────────────┘ 运行本项目5.2 企业级MCP实践安全控制:@ConfigurationpublicclassMcpSecurityConfig{@BeanpublicSecurityFilterChainmcpSecurity(HttpSecurity http)throwsException{ http .requestMatchers("/mcp/**").authenticated().oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);return http.build();}}运行本项目性能监控:MCP调用监控:通过Micrometer导出QPS、延迟指标工具健康检查:集成Spring Boot Actuator审计日志:记录每次工具调用的参数与结果六、性能基准测试:真实数据说话6.1 测试环境硬件:16核CPU, 64GB内存, SSD模型:gpt-4-turbo, qwen-plus并发:1000虚拟用户6.2 关键指标对比场景 Spring AI 原生HTTP 提升简单问答 120ms 180ms 33%RAG查询 350ms 520ms 33%工具调用 200ms 280ms 29%内存使用 800MB 1.2GB 33%6.3 生产调优参数spring:ai:chat:options:temperature:0.7max-tokens:1000retry:max-attempts:3backoff:multiplier:2max-delay: 5s circuitbreaker:failure-rate-threshold: 50% wait-duration-in-open-state: 30s 运行本项目七、企业落地路线图7.1 三阶段演进策略阶段1:试点验证(2-4周)选择非核心业务场景(如内部知识问答)使用Chroma+OpenAI快速原型建立监控和评估体系阶段2:核心场景(2-3个月)迁移到Milvus企业级向量库集成Spring Cloud微服务体系实现多模型路由策略阶段3:全面AI化(6-12个月)构建企业AI能力中台实现MCP生态集成建立AI治理体系7.2 避坑指南技术陷阱: 直接在生产环境使用Chroma(超过100万文档性能急剧下降) 忽视Token成本控制(实测GPT-4每月账单可达数万美元)缺少限流熔断(大促期间API额度耗尽导致服务雪崩)最佳实践: 使用分层架构:原型用Chroma,生产用Milvus实现Token预算管理:按用户/业务线配额部署本地模型兜底:Ollama+Llama2作为备用八、未来展望:Java AI原生时代Spring AI 1.0 GA的发布不是终点,而是Java AI生态的起点。随着以下特性的roadmap逐步实现:2025 Q3:多模态支持(图像、音频处理)2025 Q4:分布式Agent框架2026 Q1:AI工作流可视化编排2026 Q2:自动模型优化与压缩我们可以预见,未来3年内:80%的Java企业应用将具备AI能力,而Spring AI将成为这个时代的"Spring Boot"。 附录:快速开始指南1. 创建项目spring init --dependencies=web,ai my-ai-app 运行本项目2. 配置模型spring:ai:openai:api-key: ${OPENAI_API_KEY}dashscope:api-key: ${DASHSCOPE_API_KEY}运行本项目3. 第一个AI接口@SpringBootApplicationpublicclassAiApplication{publicstaticvoidmain(String[] args){SpringApplication.run(AiApplication.clas从0到1只需30分钟,这就是Spring AI的魅力。————————————————原文链接:https://blog.csdn.net/mr_yuanshen/article/details/153403301
-
前言首先,我们必须明确一个核心观点:在分布式环境下,要实现强一致性(在任何时刻读取的数据都是最新的)是极其困难且代价高昂的,通常会严重牺牲性能。因此,在实践中,我们通常追求最终一致性,即允许在短暂的时间内数据不一致,但通过一些手段保证数据最终会保持一致。下面我将从基础概念、各种策略、最佳实践到最新方案,为你详细讲解。一、基础概念:为什么会有不一致?在一个包含 MySQL(作为可靠数据源)和 Redis(作为缓存)的系统中,所有的写操作(增、删、改)都必须同时处理这两个地方。 这个过程中,任何一步失败或延迟都会导致不一致:写 MySQL 成功,写 Redis 失败:导致 Redis 中是旧数据。写 Redis 成功,写 MySQL 失败:导致 Redis 中是“脏数据”,数据库中不存在。并发读写:一个线程在更新数据库,但还没更新缓存时,另一个线程读取了旧的缓存数据。二、核心策略与模式解决双写一致性有多种策略,我们需要根据业务场景(对一致性的要求、读写的比例等)进行选择。策略一:Cache-Aside Pattern(旁路缓存模式)这是最常用、最经典的缓存模式。核心原则是:应用程序直接与数据库和缓存交互,缓存不作为写入的必经之路。读流程:收到读请求。首先查询 Redis,如果数据存在(缓存命中),直接返回。如果 Redis 中没有数据(缓存未命中),则从 MySQL 中查询。将从 MySQL 查询到的数据写入 Redis(以便后续读取),然后返回数据。写流程:收到写请求。更新 MySQL 中的数据。删除 Redis 中对应的缓存。为什么是删除(Invalidate)缓存,而不是更新缓存?这是一个关键设计点!性能:如果更新缓存,每次数据库写操作都要伴随一次缓存写操作,如果该数据并不经常被读取,那么这次缓存写入就是浪费资源的。并发安全:在并发写场景下,更新缓存的顺序可能与更新数据库的顺序不一致,导致缓存中是旧数据。而删除操作是幂等的,更为安全。Cache-Aside 如何保证一致性?它通过“先更新数据库,再删除缓存”来尽力保证。但它依然存在不一致的窗口期:线程 A 更新数据库。线程 B 读取数据,发现缓存不存在,从数据库读取旧数据(因为 A 还没提交或刚提交)。线程 B 将旧数据写入缓存。线程 A 删除缓存。这种情况发生的概率较低,因为通常数据库写操作(步骤1)会比读操作(步骤2)耗时更长(因为涉及锁、日志等),所以步骤2在步骤1之前完成的概率很小。但这是一种理论上的可能。策略二:Write-Through / Read-Through Pattern(穿透读写模式)在这种模式下,缓存层(或一个独立的服务)自己负责与数据库交互。对应用来说,它只与缓存交互。写流程:应用写入缓存,缓存组件同步地写入数据库。只有两个都成功后才会返回成功。读流程:应用读取缓存,如果未命中,缓存组件自己从数据库加载并填充缓存,然后返回。优点:逻辑对应用透明,一致性比 Cache-Aside 更好。缺点:性能较差,因为每次写操作都必然涉及一次数据库写入。通常需要成熟的缓存中间件支持。策略三:Write-Behind Pattern(异步写回模式)Write-Through 的异步版本。应用写入缓存后立即返回,缓存组件在之后某个时间点(例如攒够一批数据或定时)批量异步地更新到数据库。优点:写性能极高。缺点:有数据丢失风险(缓存宕机),一致性最弱。适用于允许少量数据丢失的场景,如计数、点赞等。三、保证最终一致性的进阶方案为了弥补 Cache-Aside 模式中的缺陷,我们可以引入一些额外的机制。方案一:延迟双删针对 Cache-Aside 中提到的“先更新数据库,再删除缓存”可能带来的并发问题,可以引入一个延迟删除。线程 A 更新数据库。线程 A 删除缓存。线程 A 休眠一个特定的时间(如 500ms - 1s)。线程 A 再次删除缓存。第二次删除是为了清理掉在第1次删除后、其他线程可能写入的旧数据。这个休眠时间需要根据业务读写耗时来估算。优点:简单有效,能很大程度上解决并发读写导致的不一致。缺点:降低了写入吞吐量,休眠时间难以精确设定。方案二:通过消息队列异步删除为了解耦和重试,可以将删除缓存的操作作为消息发送到消息队列(如 RocketMQ, Kafka)。更新数据库。向消息队列发送一条删除缓存的消息。消费者消费该消息,执行删除 Redis 的操作。如果删除失败,消息会重试。这保证了删除缓存的操作至少会被执行一次,大大提高了可靠性。方案三:通过数据库 Binlog 同步(最优解)这是目前最成熟、对业务侵入性最小、一致性最好的方案。其核心是利用 MySQL 的二进制日志(Binlog)进行增量数据同步。工作原理:业务系统正常写入 MySQL。由一个中间件(如 Canal, Debezium)伪装成 MySQL 的从库,订阅 Binlog。中间件解析 Binlog,获取数据的变更详情(增、删、改)。中间件根据变更,调用 Redis 的 API 来更新或删除对应的缓存。优点:业务无侵入:业务代码只关心写数据库,完全不知道缓存的存在。高性能:数据库和缓存的同步是异步的,不影响主业务链路的性能。强保证:由于基于 Binlog,它能保证只要数据库变了,缓存最终一定会被同步。顺序也与数据库一致。缺点:架构复杂,需要维护额外的同步组件。同步有毫秒级到秒级的延迟。四、总结与最佳实践选择策略一致性保证性能复杂度适用场景Cache-Aside + 删除最终一致性(有微弱不一致风险)高低绝大多数场景的首选,读多写少Cache-Aside + 延迟双删更好的最终一致性中低对一致性要求稍高,且能接受一定延迟的写操作Write-Through强一致性中中写多读少,且对一致性要求非常高的场景Binlog 同步最终一致性(推荐)高高大型、高要求项目的最佳实践,对业务无侵入通用建议:首选方案:对于大多数应用,从 Cache-Aside(先更新数据库,再删除缓存) 开始。它简单、有效,在大多数情况下已经足够。进阶保障:如果 Cache-Aside 的不一致窗口无法接受,可以引入延迟双删或消息队列异步删除来增强。终极方案:当业务发展到一定规模,对一致性和系统解耦有更高要求时,投入资源搭建基于 Binlog 的异步同步方案。这是业界证明最可靠的方案。设置合理的过期时间:无论如何,都给 Redis 中的缓存设置一个过期时间(TTL)。这是一个安全网,即使同步逻辑出现问题,旧数据也会自动失效,最终从数据库加载新数据,保证最终一致性。业务容忍度:最重要的是,与产品经理确认业务对一致性的容忍度。很多时候,1-2秒内的数据不一致用户是感知不到的,不需要为此付出巨大的架构和性能代价。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/153478251
-
如何使用update-alternatives管理多版本Java JDK?(Windows、Mac、Ubuntu)摘要在实际开发中,往往会遇到既要维护老项目又要跟进新特性的场景,这就需要在一台机器上同时安装并切换多个Java JDK版本。本文将针对三大主流平台——Windows、macOS 和 Ubuntu,详细介绍如何安装多个 JDK,并使用各自平台上的“替代方案”工具来管理与切换。Windows:通过系统环境变量与批处理脚本实现版本切换macOS:利用 /usr/libexec/java_home 与 jEnv 工具Ubuntu:深入剖析 update-alternatives 原理与实战无论您是新手还是有一定经验的开发者,都能从中获得清晰的思路与操作指南。文章目录如何使用update-alternatives管理多版本Java JDK?(Windows、Mac、Ubuntu) 摘要引言作者名片 加入我们AI共创团队 加入猫头虎的共创圈,一起探索编程世界的无限可能! 正文1. Windows 平台1.1 环境变量基础1.2 安装多个 JDK1.3 手动切换1.4 使用批处理脚本自动切换2. macOS 平台2.1 `/usr/libexec/java_home` 命令2.2 使用 jEnv 统一管理(推荐)3. Ubuntu 平台(Debian系)3.1 `update-alternatives` 原理3.2 安装与注册 JDK3.2.1 使用 APT 安装(OpenJDK)3.2.2 手动下载并注册 Oracle JDK3.3 切换与查看查看当前注册项交互式切换4. 验证与示例5. 常见问题与解决6. 常见 QA总结粉丝福利联系我与版权声明 引言多版本 JDK 切换为何如此重要?兼容性测试:老项目可能依赖 Java 8,而新项目需要 Java 17。生态差异:Spring Boot 2.x 与 3.x 对 Java 版本的要求不同。CI/CD 集成:自动化构建需要在不同 JDK 下验证构建过程。三大平台各有生态与管理方式,因此本文将分别展开,帮助您在不同系统上搭建灵活的多版本 Java 环境。作者名片 博主:猫头虎全网搜索关键词:猫头虎作者微信号:Libin9iOak作者公众号:猫头虎技术团队更新日期:2025年07月21日欢迎来到猫头虎的博客 — 探索技术的无限可能!加入我们AI共创团队 猫头虎AI共创社群矩阵列表:点我进入共创社群矩阵入口点我进入新矩阵备用链接入口加入猫头虎的共创圈,一起探索编程世界的无限可能! 正文1. 🪟 Windows 平台1.1 环境变量基础Windows 管理可执行程序的核心是 系统路径(PATH) 与 环境变量(Environment Variables)。切换 JDK 版本,本质上就是让系统在 PATH 中优先找到对应版本的 java.exe 与 javac.exe。1.2 安装多个 JDK从 Oracle 官网或 AdoptOpenJDK 下载所需版本的 Windows 安装包(.exe)。依次安装到不同目录,如:C:\Program Files\Java\jdk1.8.0_381C:\Program Files\Java\jdk-17.0.71.3 手动切换打开系统环境变量:右键「此电脑」→「属性」→「高级系统设置」→「环境变量」。找到 系统变量 中的 JAVA_HOME、Path:修改 JAVA_HOME 为目标 JDK 目录。在 Path 里,将 %JAVA_HOME%\bin 放到最前面。点击「确定」,重新打开命令行窗口,即可 java -version 验证。1.4 使用批处理脚本自动切换为了避免每次手动修改环境变量,可编写简单的 .bat 脚本:@echo offREM 切换到 Java 8setx JAVA_HOME "C:\Program Files\Java\jdk1.8.0_381" /Msetx PATH "%%JAVA_HOME%%\bin;%%PATH%%" /Mecho 已切换到 Java 8一键获取完整项目代码bat保存为 switch-to-java8.bat,右键以管理员身份运行。同理可写 switch-to-java17.bat。运行后重启命令行窗口即可生效。2. macOS 平台2.1 /usr/libexec/java_home 命令macOS 自带命令 /usr/libexec/java_home,可列出并切换已安装的 JDK 版本。# 列出所有已安装JDK/usr/libexec/java_home -V# 切换到 Java 11export JAVA_HOME=$(/usr/libexec/java_home -v 11)export PATH=$JAVA_HOME/bin:$PATH一键获取完整项目代码bash-V:显示版本列表及安装路径。-v <version>:选择指定版本。将上述两行写入 ~/.zshrc 或 ~/.bash_profile,并配合 alias:alias j8='export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)'alias j11='export JAVA_HOME=$(/usr/libexec/java_home -v 11)'alias j17='export JAVA_HOME=$(/usr/libexec/java_home -v 17)'一键获取完整项目代码bash打开新终端后,输入 j11 即可切换。2.2 使用 jEnv 统一管理(推荐)jEnv 是跨平台的 Java 版本管理工具,支持 macOS、Linux。安装 jEnv(需先安装 Homebrew):brew install jenv一键获取完整项目代码bash将 jEnv 集成到 shell 配置:echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.zshrcecho 'eval "$(jenv init -)"' >> ~/.zshrcsource ~/.zshrc一键获取完整项目代码bash添加已安装的 JDK:jenv add /Library/Java/JavaVirtualMachines/jdk1.8.0_381.jdk/Contents/Homejenv add /Library/Java/JavaVirtualMachines/jdk-17.0.7.jdk/Contents/Home一键获取完整项目代码bash列出与切换:jenv versionsjenv global 11 # 全局切换到 Java 11jenv local 1.8 . # 针对当前目录切换到 Java 1.8jenv shell 17 # 仅对当前 shell 有效一键获取完整项目代码bashjEnv 会自动管理 JAVA_HOME 与 PATH,并支持插件扩展(Maven、Gradle 插件等)。3. Ubuntu 平台(Debian系)3.1 update-alternatives 原理Debian/Ubuntu 引入 alternatives 系统,允许对系统命令(如 java、javac)创建“组”,并在组内注册多个“备选项”。每个备选项由 可执行文件路径 和 优先级 组成。运行 update-alternatives --config <name> 即可交互式切换。3.2 安装与注册 JDK3.2.1 使用 APT 安装(OpenJDK)sudo apt updatesudo apt install -y openjdk-8-jdk openjdk-11-jdk openjdk-17-jdk一键获取完整项目代码bashAPT 安装后通常会自动注册到 alternatives,您可以直接执行下一步。3.2.2 手动下载并注册 Oracle JDK下载并解压到 /usr/lib/jvm:sudo mkdir -p /usr/lib/jvmsudo tar -xzf ~/Downloads/jdk-17.0.7_linux-x64_bin.tar.gz -C /usr/lib/jvm一键获取完整项目代码bash注册到 alternatives(以 Java 17 为例,优先级设为 2):sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/jdk-17.0.7/bin/java 2sudo update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/jdk-17.0.7/bin/javac 2sudo update-alternatives --install /usr/bin/jar jar /usr/lib/jvm/jdk-17.0.7/bin/jar 2一键获取完整项目代码bash第三个参数为命令组名(可省略后缀)。最后一个数字为优先级,数值越大越优先。3.3 切换与查看查看当前注册项update-alternatives --query java一键获取完整项目代码bash输出包含所有 java 备选路径及当前选择。交互式切换sudo update-alternatives --config java一键获取完整项目代码bash会列出所有已注册的 Java 可执行文件,按提示输入对应序号即可切换。同理切换 javac、jar 等。4. 验证与示例无论在哪个平台,切换后都应首先验证:java -versionjavac -version一键获取完整项目代码bash并可编写最简单的 HelloWorld 程序进行编译与运行测试。5. 常见问题与解决场景 原因与排查 解决思路切换后 java -version 仍指向旧版本 PATH 未更新或 shell 缓存未刷新 重新打开终端;Windows 重启 CMD;Linux hash -rWindows 脚本执行报 “权限不足” 未以管理员身份运行 .bat 右键 → “以管理员身份运行”macOS /usr/libexec/java_home 列不全 JDK 未正确安装到 /Library/Java/... 检查 JDK 文件夹;重启 shellUbuntu 手动注册后未见新选项 alternatives 配置不一致 再次执行 --install;检查路径拼写6. 常见 QAQ:为什么 Linux 上要用 update-alternatives?A:它能同时管理多个版本的同名命令,避免手动修改 PATH,且支持优先级与脚本化。Q:Windows 有没有类似 update-alternatives 的工具?A:官方没有,但可借助 jabba 或自定义批处理脚本。Q:macOS 上除了 jEnv 还有其他方案吗?A:也可使用 SDKMAN! 管理,但 SDKMAN! 对 Windows 支持有限。总结本文深入对比了 Windows、macOS 和 Ubuntu 三大平台上多版本 Java JDK 管理的思路与实践:Windows:环境变量 + 批处理脚本macOS:/usr/libexec/java_home + jEnvUbuntu:update-alternatives 原理详解掌握上述方法后,无论在本地开发还是在 CI/CD 环境,都能灵活切换 JDK 版本,确保兼容性与高效协同开发。祝您 Java 开发之路顺畅————————————————原文链接:https://blog.csdn.net/qq_44866828/article/details/149533211
-
本文简介目的:Spring生态为Java后端开发提供了强大支持,但将分散的技术点整合成完整解决方案往往令人困惑。本文将以登录接口为切入点,系统演示如何将IOC/DI、MyBatis数据持久化、MD5加密、Session/Cookie管理、JWT令牌和拦截器机制融合运用,打造企业级认证方案技术栈:前端:HTML + CSS + JavaScript + Jquery后端:SpringBoot + Mybatis + JWT搭建环境:数据库:MySQL8.4.0项目结构:maven前端框架:Jquery后端框架:SpringBootJDK:17编译器:IDEA目录结构:项目搭建及配置1.创建SpringBoot3.0.0+项目并添加依赖:Spring Web、MyBatis Framework、MySQL Driver、Lombok2.初始化数据库:create database spring_blog_login charset utf8mb4; use spring_blog_login;create table user_info (id int primary key auto_increment,user_name varchar(128) unique , password varchar(128) not null,delete_flag int default 0, create_time datetime default now(),update_time datetime default now());insert into user_info (user_name,password) values ('张三','123456'), ('李四','123456'), ('王五','123456');运行本项目sql3.将application.properties修改为application.yml并添加如下配置:spring: datasource: url: jdbc:mysql://127.0.0.1:3306/spring_blog_login?characterEncoding=utf8&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Drivermybatis: configuration: map-underscore-to-camel-case: true #自动驼峰转换server: port: 8080 #不显式设置默认为8080运行本项目yml按住Ctrl + F5,如果程序能运行成功则说明搭建及配置都没问题(MySQL服务器必须要处于运行状态)1.登录认证全栈实现 ->基础版1.1 后端实现1.1.1 架构设计本次登录功能采用Controller、Service、Mapper三层架构:Controller层依赖于Service层来执行业务逻辑并获取处理结果,而Service层又依赖于Mapper层来进行数据持久化操作1.1.2 实体类实体类用于封装业务数据,需要与数据库表结构一一对应import lombok.Data;import java.util.Date;@Datapublic class UserInfo { private Integer id; private String userName; private String password; private Integer deleteFlag; private Date createTime; private Date updateTime;}运行本项目java运行1.1.3 Controller处理HTTP请求、参数校验、返回响应import org.example.springlogin.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/user")public class UserController { private final UserService userService; @Autowired public UserController(UserService userService) { this.userService = userService; } @RequestMapping("/login") public String login(String userName,String password) { if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) { return "用户或密码为空"; } return userService.getUserInfoByUserName(userName,password); }}运行本项目java运行1.1.4 Service业务逻辑处理import org.example.springlogin.mapper.UserMapper;import org.example.springlogin.model.UserInfo;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class UserService { private final UserMapper userMapper; @Autowired public UserService(UserMapper userMapper) { this.userMapper = userMapper; } public String getUserInfoByUserName(String userName,String password) { UserInfo userInfo = userMapper.getUserInfoByUserName(userName); if (userInfo == null) { return "用户不存在"; } if (!password.equals(userInfo.getPassword())) { return "密码错误"; } return "登录成功"; }}运行本项目java运行1.1.5 Mapper数据持久化操作import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Select;import org.example.springlogin.model.UserInfo;@Mapperpublic interface UserMapper { @Select("select * from user_info where user_name = #{userName}") UserInfo getUserInfoByUserName(String userName);}运行本项目java运行1.2 前端实现Gitee:项目前端代码,Gitee上的前端代码是最新提交的,如下效果图仅作参考效果演示:1.用户或密码为空2.用户不存在3.密码错误4.登录成功2.Cookie/SessionHTTP(超文本传输协议)设计为无状态协议,指服务器默认不保留客户端请求之间的任何状态信息。每个请求独立处理,服务器不会记忆之前的交互内容(如下图)优点:请求独立性:每次请求被视为新请求,服务器不依赖历史请求数据简单高效:无状态设计降低服务器资源消耗,简化实现逻辑缺点:身份识别困难:需通过额外机制(如Cookies、Session)跟踪用户状态重复传输数据:每次请求需携带完整信息,可能增加冗余(如认证信息)cookie:是存储在客户端(浏览器)的小型文本数据,由服务器通过HTTP响应头Set-Cookie发送给客户端,并在后续请求中自动携带session:是存储在服务器端的用户状态信息,通常通过一个唯一的Session ID标识,该ID可能通过Cookie或URL传递如上图片引用自我的博客:Java EE(13)——网络原理——应用层HTTP协议,服务器内部实际上专门开辟了一个session空间用于存储用户信息,每当新用户发送第一次请求时服务器会将用户信息存储在session中并生成一个session id通过Set-Cookie方法返回给客户端,即cookiesession结构如下:修改Controller类代码:import jakarta.servlet.http.HttpSession;import lombok.extern.slf4j.Slf4j;import org.example.springlogin.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;@RestController@RequestMapping("/user")@Slf4jpublic class UserController { private final UserService userService; @Autowired public UserController(UserService userService) { this.userService = userService; } @RequestMapping("/login") public String login(String userName, String password, HttpSession session) { log.info("接收到参数,userName:{},password:{}",userName,password); if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) { return "用户或密码为空"; } String result = userService.getUserInfoByUserName(userName, password); if (result.equals("登录成功")){ HashMap<String,String> map = new HashMap<>(); map.put("userName",userName); map.put("password",password); //将map作为用户信息存储到session/会话中 session.setAttribute("cookie", map); log.info("登录成功"); } return result; }运行本项目java运行修改前端代码: function login() { $.ajax({ url: '/user/login', type: "post", data:{ userName:$('#username').val(), password:$('#password').val(), }, success: function(result) { alert(result); }, }) }运行本项目javascript运行Fiddler抓包结果:前端/浏览器按住Ctrl + Shift + i打开控制台点击应用程序/application,打开Cookie:3.统一返回结果封装统一返回结果封装是后端开发中的重要设计模式,能够保持API响应格式的一致性,便于前端处理1.创建枚举类:统一管理接口或方法的返回状态码和描述信息,标准化业务逻辑中的成功或失败状态import lombok.Getter;@Getterpublic enum ResultStatus { SUCCESS(200,"成功"), FAIL(-1,"失败"), ; private final Integer code; private final String message; ResultStatus(Integer code, String message) { this.code = code; this.message = message; }}运行本项目java运行2.创建Result< T >类:主要用于规范服务端返回给客户端的响应数据格式。通过固定结构(状态码、错误信息、数据)确保前后端交互的一致性import lombok.Data;@Data//通过泛型<T>设计,可以灵活封装任意类型的数据对象到data字段public class Result<T> { //业务码 private ResultStatus code; //错误信息 private String errorMessage; //数据 private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(ResultStatus.SUCCESS); result.setErrorMessage(null); result.setData(data); return result; } public static <T> Result<T> fail(String errorMessage) { Result<T> result = new Result<>(); result.setCode(ResultStatus.FAIL); result.setErrorMessage(errorMessage); result.setData(null); return result; }}运行本项目java运行3.修改Controller代码:@RestController@RequestMapping("/user")@Slf4jpublic class UserController { private final UserService userService; @Autowired public UserController(UserService userService) { this.userService = userService; } @RequestMapping("/login") public Result<String> login(String userName, String password, HttpSession session) { log.info("接收到参数,userName:{},password:{}",userName,password); if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) { return Result.fail("用户或密码为空"); } String result = userService.getUserInfoByUserName(userName, password); if (!result.equals("登录成功")){ return Result.fail(result); } HashMap<String,String> map = new HashMap<>(); map.put("userName",userName); map.put("password",password); //将map作为用户信息存储到session/会话中 session.setAttribute("cookie", map); log.info("登录成功"); return Result.success(result); }}运行本项目java运行4.修改前端代码: function login() { $.ajax({ url: '/user/login', type: "post", data:{ userName:$('#username').val(), password:$('#password').val(), }, success: function(result) { if (result.code === "SUCCESS") { alert(result.data) }else { alert(result.error) } }, }) }运行本项目javascript运行4.图形验证码图形验证码(captcha)是一种区分用户是人类还是自动化程序的技术,主要通过视觉或交互任务实现。其核心意义体现在以下方面:防止自动化攻击:通过复杂图形或扭曲文字,阻止爬虫、暴力破解工具等自动化程序批量注册或登录,降低服务器压力提升安全性:在敏感操作(如支付、修改密码)前增加验证步骤,减少数据泄露或恶意操作风险Hutool提供了CaptchaUtil类用于快速生成验证码,支持图形验证码和GIF动态验证码。在pom.xml文件中添加图下配置:<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <!-- 版本号应与springboot版本兼容 --> <version>5.8.40</version></dependency>运行本项目xml1.创建CaptchaController类,用于生成验证码并返回给前端import cn.hutool.captcha.CaptchaUtil;import cn.hutool.captcha.LineCaptcha;import jakarta.servlet.http.HttpServletResponse;import jakarta.servlet.http.HttpSession;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.io.IOException;@RestController@RequestMapping("/captcha")@Slf4jpublic class CaptchaController { //设置过期时间 public final static long delay = 60_000L; @RequestMapping("/get") public void getCaptcha(HttpSession session, HttpServletResponse response) { log.info("getCaptcha"); LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100); //设置返回类型 response.setContentType("image/jpeg"); //禁止缓存 response.setHeader("Pragma", "No-cache"); try { //通过响应输出生成的图形验证码 lineCaptcha.write(response.getOutputStream()); //保存code session.setAttribute("CAPTCHA_SESSION_CODE", lineCaptcha.getCode()); //保存当前时间 session.setAttribute("CAPTCHA_SESSION_DATE", System.currentTimeMillis()); //关闭输出流 response.getOutputStream().close(); } catch (IOException e) { throw new RuntimeException(e); } }}运行本项目java运行2.修改前端代码:最终版<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>微信登录</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <link rel="stylesheet" href="css/login.css"></head><body> <div class="login-container"> <div class="logo"> <i class="fab fa-weixin"></i> </div> <h2>微信登录</h2> <form id="loginForm"> <div class="input-group"> <i class="fas fa-user"></i> <label for="username"></label><input type="text" id="username" placeholder="请输入用户名" required> </div> <div class="input-group"> <i class="fas fa-lock"></i> <label for="password"></label><input type="password" id="password" placeholder="请输入密码" required> </div> <div class="input-group"> <div class="captcha-container"> <label for="inputCaptcha"></label><input type="text" id="inputCaptcha" class="captcha-input" placeholder="输入验证码"> <img id="verificationCodeImg" src="/captcha/get" class="captcha-img" title="看不清?换一张" alt="验证码"> </div> </div> <div class="agreement"> <input type="checkbox" id="agreeCheck" checked> <label for="agreeCheck">我已阅读并同意<a href="#">《服务条款》</a>和<a href="#">《隐私政策》</a></label> </div> <button type="submit" class="login-btn" onclick="login()">登录</button> </form> <div class="footer"> <p>版权所有 ©九转苍翎</p> </div> </div> <!-- 引入jQuery依赖 --> <script src="js/jquery.min.js"></script> <script> //刷新验证码 $("#verificationCodeImg").click(function(){ //new Date().getTime()).fadeIn()防止前端缓存 $(this).hide().attr('src', '/captcha/get?dt=' + new Date().getTime()).fadeIn(); }); //登录 function login() { $.ajax({ url: '/user/login', type: "post", data:{ userName:$('#username').val(), password:$('#password').val(), captcha:$('#inputCaptcha').val(), }, success: function(result) { console.log(result); if (result.code === "SUCCESS") { alert(result.data) }else { alert(result.error) } }, }) } </script></body></html>运行本项目html3.在UserController类新增captcha形参接收来自CaptchaController类的请求,并传递给UserServiceimport jakarta.servlet.http.HttpSession;import org.example.springlogin.controller.CaptchaController;import org.example.springlogin.mapper.UserMapper;import org.example.springlogin.model.UserInfo;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class UserService { private final UserMapper userMapper; @Autowired public UserService(UserMapper userMapper) { this.userMapper = userMapper; } public String getUserInfoByUserName(String userName, String password, String captcha, HttpSession session) { UserInfo userInfo = userMapper.getUserInfoByUserName(userName); if (userInfo == null) { return "用户不存在"; } if (!password.equals(userInfo.getPassword())) { return "密码错误"; } long saveTime = (long)session.getAttribute("CAPTCHA_SESSION_DATE"); if (System.currentTimeMillis() - saveTime > CaptchaController.delay) { return "验证码超时"; } if (!captcha.equalsIgnoreCase((String) session.getAttribute("CAPTCHA_SESSION_CODE"))) { return "验证码错误"; } return "登录成功"; }}运行本项目java运行实现效果:5.MD5加密MD5(Message-Digest Algorithm 5)是一种广泛使用的哈希函数,可将任意长度数据生成固定长度(128位,16字节)的哈希值,通常表示为32位十六进制字符串,常用于校验数据完整性或存储密码。但因其安全性不足,通常结合盐值(Salt)配合使用不可逆性:无法通过哈希值反推原始数据唯一性:理论上不同输入产生相同哈希值的概率极低(哈希碰撞)固定长度:无论输入数据大小,输出均为32位十六进制字符串1.创建SecurityUtil类用于生成和验证密文import org.springframework.util.DigestUtils;import org.springframework.util.StringUtils;import java.util.UUID;public class SecurityUtil { //加密 public static String encrypt(String inputPassword){ //生成随机盐值 String salt = UUID.randomUUID().toString().replaceAll("-", ""); //(密码+盐值)进行加密 String finalPassword = DigestUtils.md5DigestAsHex((inputPassword + salt).getBytes()); return salt + finalPassword; } //验证 public static boolean verify(String inputPassword, String sqlPassword){ if (!StringUtils.hasLength(inputPassword)){ return false; } if (sqlPassword == null || sqlPassword.length() != 64){ return false; } //取出盐值 String salt = sqlPassword.substring(0,32); //(输入密码 + 盐值)重新生成 加密密码 String finalPassword = DigestUtils.md5DigestAsHex((inputPassword + salt).getBytes()); //判断数据库中储存的密码与输入密码是否一致 return (salt + finalPassword).equals(sqlPassword); } public static void main(String[] args) { System.out.println(SecurityUtil.encrypt("123456")); }}运行本项目java运行2.将数据库中的密码替换为加密后的值3.修改验证密码的逻辑(UserService类) if (!SecurityUtil.verify(password,userInfo.getPassword())) { return "密码错误"; }运行本项目java运行6.拦截器Spring拦截器(Interceptor)是一种基于AOP的机制,用于在请求处理的不同阶段插入自定义逻辑。常用于权限校验、日志记录、参数预处理等场景1.创建拦截器类并实现HandlerInterceptor接口,该接口提供了三种方法:preHandle:在Controller方法执行前调用postHandle:Controller方法执行后、视图渲染前调用afterCompletion:请求完成、视图渲染完毕后调用import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;@Slf4j@Componentpublic class Interceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { //1.获取token String cookie = request.getHeader("cookie"); if (cookie == null) { response.setStatus(401); return false; } log.info("接收到cookie:{}",cookie); //2.校验token return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { log.info("postHandle"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { log.info("afterCompletion"); }}运行本项目java运行2.注册拦截器import org.example.springlogin.intercepter.Interceptor;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.Arrays;import java.util.List;@Configurationpublic class Config implements WebMvcConfigurer { private final Interceptor Interceptor; @Autowired public Config(Interceptor interceptor) { Interceptor = interceptor; } //排除不需要拦截的路径 private static final List<String> excludes = Arrays.asList( "/**/login.html", "/user/login", "/captcha/get" ); @Override public void addInterceptors(InterceptorRegistry registry) { //注册拦截器 registry.addInterceptor(Interceptor) //拦截所有路径 .addPathPatterns("/**") .excludePathPatterns(excludes); }}运行本项目java运行3.创建home.html文件,并且在登录成功后跳转到该页面(在login.html中添加location.href="/home.html")<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>home</title></head><body> <h1>Hello World</h1></body></html>运行本项目html实现效果:成功登陆时未登录直接访问home.html页面时————————————————原文链接:https://blog.csdn.net/2401_89167985/article/details/153261457
-
前言在现代企业级Java应用开发中,事务管理是确保数据一致性和完整性的核心机制。Spring框架作为Java生态系统中最重要的框架之一,提供了强大而灵活的事务管理功能。本文将从基础概念出发,深入探讨Spring事务管理的各个方面,通过丰富的代码示例和实践案例,帮助开发者全面掌握Spring事务管理的精髓。无论你是刚接触Spring事务的新手,还是希望深化理解的资深开发者,本文都将为你提供有价值的见解和实用的技巧。我们将先概览Spring事务的整体架构,然后深入各个具体模块,最后进行知识总结和扩展思考。第一章:Spring事务基础概念与核心原理1.1 事务的基本概念事务(Transaction)是数据库操作的基本单位,它是一个不可分割的工作逻辑单元。事务必须满足ACID特性:原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败回滚一致性(Consistency):事务执行前后,数据库的完整性约束没有被破坏隔离性(Isolation):并发执行的事务之间不能互相干扰持久性(Durability):事务一旦提交,其结果就是永久性的1.2 Spring事务管理架构Spring事务管理基于以下核心组件:1.2.1 PlatformTransactionManager接口public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException;}运行本项目java运行这是Spring事务管理的核心接口,定义了事务的基本操作。不同的数据访问技术有不同的实现:DataSourceTransactionManager:用于JDBC和MyBatisJpaTransactionManager:用于JPAHibernateTransactionManager:用于Hibernate1.2.2 TransactionDefinition接口public interface TransactionDefinition { int PROPAGATION_REQUIRED = 0; int PROPAGATION_SUPPORTS = 1; int PROPAGATION_MANDATORY = 2; // ... 其他传播行为常量 int getPropagationBehavior(); int getIsolationLevel(); int getTimeout(); boolean isReadOnly(); String getName();}运行本项目java运行1.3 Spring事务管理的实现原理Spring事务管理基于AOP(面向切面编程)实现,通过代理模式在方法调用前后添加事务逻辑:@Componentpublic class UserService { @Autowired private UserRepository userRepository; @Transactional public void createUser(User user) { // Spring会在此方法执行前开启事务 userRepository.save(user); // 方法正常结束时提交事务,异常时回滚 }}运行本项目java运行当Spring容器创建UserService的代理对象时,会织入事务管理逻辑:// 简化的代理逻辑示意public class UserServiceProxy extends UserService { private PlatformTransactionManager transactionManager; @Override public void createUser(User user) { TransactionStatus status = null; try { // 开启事务 status = transactionManager.getTransaction(new DefaultTransactionDefinition()); // 调用实际的业务方法 super.createUser(user); // 提交事务 transactionManager.commit(status); } catch (Exception e) { // 回滚事务 if (status != null) { transactionManager.rollback(status); } throw e; } }}运行本项目java运行第二章:Spring事务管理器详解2.1 事务管理器的选择与配置2.1.1 DataSourceTransactionManager配置对于使用JDBC或MyBatis的应用,通常选择DataSourceTransactionManager:@Configuration@EnableTransactionManagementpublic class TransactionConfig { @Bean public DataSource dataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/testdb"); dataSource.setUsername("root"); dataSource.setPassword("password"); return dataSource; } @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); }}运行本项目java运行2.1.2 JpaTransactionManager配置对于JPA应用,使用JpaTransactionManager:@Configuration@EnableTransactionManagement@EnableJpaRepositories(basePackages = "com.example.repository")public class JpaConfig { @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory( DataSource dataSource) { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource); em.setPackagesToScan("com.example.entity"); HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); em.setJpaVendorAdapter(vendorAdapter); Properties properties = new Properties(); properties.setProperty("hibernate.hbm2ddl.auto", "update"); properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect"); em.setJpaProperties(properties); return em; } @Bean public PlatformTransactionManager transactionManager( EntityManagerFactory entityManagerFactory) { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(entityManagerFactory); return transactionManager; }}运行本项目java运行2.2 多数据源事务管理在复杂的企业应用中,经常需要处理多个数据源的事务:@Configuration@EnableTransactionManagementpublic class MultiDataSourceConfig { @Bean @Primary public DataSource primaryDataSource() { // 主数据源配置 return DataSourceBuilder.create() .url("jdbc:mysql://localhost:3306/primary_db") .username("root") .password("password") .build(); } @Bean public DataSource secondaryDataSource() { // 从数据源配置 return DataSourceBuilder.create() .url("jdbc:mysql://localhost:3306/secondary_db") .username("root") .password("password") .build(); } @Bean @Primary public PlatformTransactionManager primaryTransactionManager( @Qualifier("primaryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean public PlatformTransactionManager secondaryTransactionManager( @Qualifier("secondaryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }}运行本项目java运行使用时可以通过@Transactional注解指定事务管理器:@Servicepublic class MultiDataSourceService { @Transactional("primaryTransactionManager") public void operateOnPrimaryDB() { // 操作主数据库 } @Transactional("secondaryTransactionManager") public void operateOnSecondaryDB() { // 操作从数据库 }}运行本项目java运行2.3 分布式事务管理对于跨多个资源的分布式事务,Spring提供了JTA支持:@Configuration@EnableTransactionManagementpublic class JtaConfig { @Bean public JtaTransactionManager transactionManager() { JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(); jtaTransactionManager.setUserTransaction(userTransaction()); jtaTransactionManager.setTransactionManager(atomikosTransactionManager()); return jtaTransactionManager; } @Bean public UserTransaction userTransaction() throws SystemException { UserTransactionImp userTransactionImp = new UserTransactionImp(); userTransactionImp.setTransactionTimeout(300); return userTransactionImp; } @Bean public TransactionManager atomikosTransactionManager() { UserTransactionManager userTransactionManager = new UserTransactionManager(); userTransactionManager.setForceShutdown(false); return userTransactionManager; }}运行本项目java运行第三章:声明式事务配置与使用3.1 @Transactional注解详解@Transactional是Spring声明式事务的核心注解,提供了丰富的配置选项:@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic @interface Transactional { String value() default ""; String transactionManager() default ""; Propagation propagation() default Propagation.REQUIRED; Isolation isolation() default Isolation.DEFAULT; int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; boolean readOnly() default false; Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {};}运行本项目java运行3.2 注解使用最佳实践3.2.1 类级别与方法级别注解@Service@Transactional(readOnly = true) // 类级别默认只读事务public class UserService { @Autowired private UserRepository userRepository; // 继承类级别的只读事务 public List<User> findAllUsers() { return userRepository.findAll(); } // 方法级别覆盖类级别配置 @Transactional(readOnly = false, rollbackFor = Exception.class) public User createUser(User user) { validateUser(user); return userRepository.save(user); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateUserStatus(Long userId, UserStatus status) { User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("User not found")); user.setStatus(status); userRepository.save(user); } private void validateUser(User user) { if (user.getEmail() == null || user.getEmail().isEmpty()) { throw new IllegalArgumentException("Email cannot be empty"); } }}运行本项目java运行3.2.2 异常处理与回滚配置@Servicepublic class OrderService { @Autowired private OrderRepository orderRepository; @Autowired private InventoryService inventoryService; @Autowired private PaymentService paymentService; // 默认只对RuntimeException和Error回滚 @Transactional public Order createOrder(OrderRequest request) { Order order = new Order(request); orderRepository.save(order); // 如果库存不足,抛出RuntimeException,事务会回滚 inventoryService.reserveItems(request.getItems()); return order; } // 指定对所有异常都回滚 @Transactional(rollbackFor = Exception.class) public void processPayment(Long orderId, PaymentInfo paymentInfo) throws PaymentException { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new OrderNotFoundException("Order not found")); try { paymentService.processPayment(paymentInfo); order.setStatus(OrderStatus.PAID); orderRepository.save(order); } catch (PaymentException e) { // PaymentException是检查异常,但配置了rollbackFor = Exception.class // 所以事务会回滚 throw e; } } // 指定某些异常不回滚 @Transactional(noRollbackFor = {BusinessException.class}) public void updateOrderWithBusinessLogic(Long orderId) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new OrderNotFoundException("Order not found")); try { // 执行业务逻辑 performBusinessLogic(order); orderRepository.save(order); } catch (BusinessException e) { // BusinessException不会导致事务回滚 // 但数据库操作仍然会提交 log.warn("Business logic failed, but transaction will commit", e); } }}运行本项目java运行3.3 XML配置方式虽然注解方式更加流行,但XML配置在某些场景下仍然有用:<!-- applicationContext.xml --><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 数据源配置 --> <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/testdb"/> <property name="username" value="root"/> <property name="password" value="password"/> </bean> <!-- 事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 事务通知 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="find*" read-only="true"/> <tx:method name="get*" read-only="true"/> <tx:method name="*" rollback-for="Exception"/> </tx:attributes> </tx:advice> <!-- AOP配置 --> <aop:config> <aop:pointcut id="serviceOperation" expression="execution(* com.example.service.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/> </aop:config></beans>运行本项目xml第四章:编程式事务管理4.1 TransactionTemplate使用TransactionTemplate提供了编程式事务管理的模板方法:@Servicepublic class ProgrammaticTransactionService { @Autowired private TransactionTemplate transactionTemplate; @Autowired private UserRepository userRepository; @Autowired private OrderRepository orderRepository; public User createUserWithOrder(User user, Order order) { return transactionTemplate.execute(status -> { try { // 保存用户 User savedUser = userRepository.save(user); // 设置订单的用户ID order.setUserId(savedUser.getId()); // 保存订单 orderRepository.save(order); return savedUser; } catch (Exception e) { // 手动标记回滚 status.setRollbackOnly(); throw new RuntimeException("Failed to create user with order", e); } }); } public void batchUpdateUsers(List<User> users) { transactionTemplate.execute(status -> { for (User user : users) { try { userRepository.save(user); } catch (Exception e) { log.error("Failed to update user: {}", user.getId(), e); // 可以选择继续处理其他用户,或者回滚整个事务 // status.setRollbackOnly(); } } return null; }); }}运行本项目java运行4.2 PlatformTransactionManager直接使用对于更细粒度的控制,可以直接使用PlatformTransactionManager:@Servicepublic class LowLevelTransactionService { @Autowired private PlatformTransactionManager transactionManager; @Autowired private UserRepository userRepository; public void complexBusinessOperation() { // 定义事务属性 DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); def.setTimeout(30); TransactionStatus status = transactionManager.getTransaction(def); try { // 第一阶段操作 performPhaseOne(); // 检查点 - 可以根据业务逻辑决定是否继续 if (!shouldContinue()) { transactionManager.rollback(status); return; } // 第二阶段操作 performPhaseTwo(); // 提交事务 transactionManager.commit(status); } catch (Exception e) { // 回滚事务 transactionManager.rollback(status); throw new RuntimeException("Business operation failed", e); } } private void performPhaseOne() { // 第一阶段业务逻辑 } private void performPhaseTwo() { // 第二阶段业务逻辑 } private boolean shouldContinue() { // 业务判断逻辑 return true; }}运行本项目java运行4.3 编程式事务的嵌套使用@Servicepublic class NestedTransactionService { @Autowired private PlatformTransactionManager transactionManager; public void parentOperation() { DefaultTransactionDefinition parentDef = new DefaultTransactionDefinition(); parentDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus parentStatus = transactionManager.getTransaction(parentDef); try { // 父事务操作 performParentOperation(); // 调用子事务 childOperation(); transactionManager.commit(parentStatus); } catch (Exception e) { transactionManager.rollback(parentStatus); throw e; } } private void childOperation() { DefaultTransactionDefinition childDef = new DefaultTransactionDefinition(); childDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); TransactionStatus childStatus = transactionManager.getTransaction(childDef); try { // 子事务操作(独立的事务) performChildOperation(); transactionManager.commit(childStatus); } catch (Exception e) { transactionManager.rollback(childStatus); // 子事务失败不影响父事务(因为使用了REQUIRES_NEW) log.error("Child operation failed", e); } }}运行本项目java运行第五章:事务传播行为和隔离级别深度解析5.1 事务传播行为详解Spring定义了7种事务传播行为,每种都有其特定的使用场景:5.1.1 PROPAGATION_REQUIRED(默认)@Servicepublic class PropagationRequiredService { @Autowired private UserService userService; @Transactional(propagation = Propagation.REQUIRED) public void outerMethod() { // 开启新事务T1 performOperation1(); // 调用内部方法,加入事务T1 userService.innerMethod(); performOperation2(); // 如果任何操作失败,整个事务T1回滚 }}@Servicepublic class UserService { @Transactional(propagation = Propagation.REQUIRED) public void innerMethod() { // 加入外部事务T1,不会创建新事务 performUserOperation(); }}运行本项目java运行5.1.2 PROPAGATION_REQUIRES_NEW@Servicepublic class PropagationRequiresNewService { @Autowired private AuditService auditService; @Transactional public void businessOperation() { try { // 主业务逻辑在事务T1中 performMainBusiness(); // 审计日志使用独立事务T2 auditService.logOperation("Business operation completed"); } catch (Exception e) { // 即使主业务失败,审计日志也会保存(因为是独立事务) auditService.logError("Business operation failed: " + e.getMessage()); throw e; } }}@Servicepublic class AuditService { @Transactional(propagation = Propagation.REQUIRES_NEW) public void logOperation(String message) { // 创建新事务T2,独立于调用者的事务 AuditLog log = new AuditLog(message, new Date()); auditRepository.save(log); // T2独立提交,不受T1影响 } @Transactional(propagation = Propagation.REQUIRES_NEW) public void logError(String errorMessage) { // 错误日志也使用独立事务,确保一定会保存 ErrorLog errorLog = new ErrorLog(errorMessage, new Date()); errorRepository.save(errorLog); }}运行本项目java运行5.1.3 PROPAGATION_NESTED@Servicepublic class PropagationNestedService { @Autowired private OrderService orderService; @Transactional public void processOrderBatch(List<OrderRequest> orders) { for (OrderRequest orderRequest : orders) { try { // 每个订单处理使用嵌套事务 orderService.processOrder(orderRequest); } catch (Exception e) { // 单个订单失败不影响其他订单 log.error("Failed to process order: {}", orderRequest.getId(), e); } } }}@Servicepublic class OrderService { @Transactional(propagation = Propagation.NESTED) public void processOrder(OrderRequest request) { // 创建嵌套事务(保存点) Order order = new Order(request); orderRepository.save(order); // 如果这里抛出异常,只回滚到保存点 // 外部事务可以继续执行 validateAndProcessPayment(order); }}运行本项目java运行5.1.4 其他传播行为示例@Servicepublic class OtherPropagationService { // PROPAGATION_SUPPORTS:支持当前事务,如果没有事务则以非事务方式执行 @Transactional(propagation = Propagation.SUPPORTS) public List<User> findUsers() { // 如果在事务中调用,加入事务 // 如果不在事务中调用,以非事务方式执行 return userRepository.findAll(); } // PROPAGATION_NOT_SUPPORTED:以非事务方式执行,如果当前存在事务则挂起 @Transactional(propagation = Propagation.NOT_SUPPORTED) public void performNonTransactionalOperation() { // 总是以非事务方式执行 // 如果当前有事务,会被挂起 performLongRunningOperation(); } // PROPAGATION_MANDATORY:必须在事务中执行,否则抛出异常 @Transactional(propagation = Propagation.MANDATORY) public void mandatoryTransactionMethod() { // 如果没有活动事务,抛出IllegalTransactionStateException performCriticalOperation(); } // PROPAGATION_NEVER:不能在事务中执行,否则抛出异常 @Transactional(propagation = Propagation.NEVER) public void neverInTransactionMethod() { // 如果当前有活动事务,抛出IllegalTransactionStateException performIndependentOperation(); }}运行本项目java运行5.2 事务隔离级别详解5.2.1 隔离级别对比@Servicepublic class IsolationLevelService { // READ_UNCOMMITTED:最低隔离级别,可能出现脏读 @Transactional(isolation = Isolation.READ_UNCOMMITTED) public List<User> readUncommittedExample() { // 可能读取到其他事务未提交的数据 return userRepository.findAll(); } // READ_COMMITTED:防止脏读,但可能出现不可重复读 @Transactional(isolation = Isolation.READ_COMMITTED) public User readCommittedExample(Long userId) { User user1 = userRepository.findById(userId).orElse(null); // 在这期间,其他事务可能修改了用户数据 performSomeOperation(); User user2 = userRepository.findById(userId).orElse(null); // user1和user2可能不同(不可重复读) return user2; } // REPEATABLE_READ:防止脏读和不可重复读,但可能出现幻读 @Transactional(isolation = Isolation.REPEATABLE_READ) public List<User> repeatableReadExample() { List<User> users1 = userRepository.findByStatus(UserStatus.ACTIVE); performSomeOperation(); List<User> users2 = userRepository.findByStatus(UserStatus.ACTIVE); // users1和users2中的现有记录相同,但users2可能包含新插入的记录(幻读) return users2; } // SERIALIZABLE:最高隔离级别,防止所有并发问题 @Transactional(isolation = Isolation.SERIALIZABLE) public void serializableExample() { // 完全串行化执行,性能最低但数据一致性最高 List<User> users = userRepository.findAll(); for (User user : users) { user.setLastAccessTime(new Date()); userRepository.save(user); } }}运行本项目java运行5.2.2 隔离级别实际应用场景@Servicepublic class BankingService { @Autowired private AccountRepository accountRepository; // 转账操作需要高隔离级别确保数据一致性 @Transactional(isolation = Isolation.SERIALIZABLE) public void transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) { Account fromAccount = accountRepository.findById(fromAccountId) .orElseThrow(() -> new AccountNotFoundException("From account not found")); Account toAccount = accountRepository.findById(toAccountId) .orElseThrow(() -> new AccountNotFoundException("To account not found")); if (fromAccount.getBalance().compareTo(amount) < 0) { throw new InsufficientFundsException("Insufficient funds"); } fromAccount.setBalance(fromAccount.getBalance().subtract(amount)); toAccount.setBalance(toAccount.getBalance().add(amount)); accountRepository.save(fromAccount); accountRepository.save(toAccount); } // 查询余额可以使用较低的隔离级别 @Transactional(isolation = Isolation.READ_COMMITTED, readOnly = true) public BigDecimal getBalance(Long accountId) { Account account = accountRepository.findById(accountId) .orElseThrow(() -> new AccountNotFoundException("Account not found")); return account.getBalance(); } // 生成对账单可以使用REPEATABLE_READ确保数据一致性 @Transactional(isolation = Isolation.REPEATABLE_READ, readOnly = true) public AccountStatement generateStatement(Long accountId, Date startDate, Date endDate) { Account account = accountRepository.findById(accountId) .orElseThrow(() -> new AccountNotFoundException("Account not found")); List<Transaction> transactions = transactionRepository .findByAccountIdAndDateBetween(accountId, startDate, endDate); return new AccountStatement(account, transactions, startDate, endDate); }}运行本项目java运行第六章:Spring事务最佳实践和常见问题6.1 事务使用最佳实践6.1.1 事务边界设计// 错误示例:事务边界过大@Servicepublic class BadTransactionService { @Transactional public void processLargeDataSet(List<Data> dataList) { for (Data data : dataList) { // 长时间运行的操作 processComplexData(data); // 外部服务调用 externalService.sendNotification(data); // 文件操作 fileService.writeToFile(data); } // 事务持续时间过长,容易导致锁竞争和超时 }}// 正确示例:合理的事务边界@Servicepublic class GoodTransactionService { public void processLargeDataSet(List<Data> dataList) { for (Data data : dataList) { try { // 每个数据项使用独立事务 processSingleData(data); } catch (Exception e) { log.error("Failed to process data: {}", data.getId(), e); // 单个失败不影响其他数据处理 } } } @Transactional public void processSingleData(Data data) { // 只包含数据库操作的事务 dataRepository.save(processData(data)); // 非事务操作放在事务外 CompletableFuture.runAsync(() -> { externalService.sendNotification(data); fileService.writeToFile(data); }); }}运行本项目java运行6.1.2 只读事务优化@Servicepublic class OptimizedReadService { // 明确标记只读事务 @Transactional(readOnly = true) public List<User> findActiveUsers() { return userRepository.findByStatus(UserStatus.ACTIVE); } // 复杂查询使用只读事务 @Transactional(readOnly = true) public UserStatistics generateUserStatistics() { long totalUsers = userRepository.count(); long activeUsers = userRepository.countByStatus(UserStatus.ACTIVE); long inactiveUsers = userRepository.countByStatus(UserStatus.INACTIVE); return new UserStatistics(totalUsers, activeUsers, inactiveUsers); } // 分页查询优化 @Transactional(readOnly = true) public Page<User> findUsersWithPagination(Pageable pageable) { return userRepository.findAll(pageable); }}运行本项目java运行6.1.3 异常处理策略@Servicepublic class ExceptionHandlingService { @Transactional(rollbackFor = Exception.class) public void robustBusinessOperation(BusinessRequest request) { try { // 主要业务逻辑 performMainOperation(request); } catch (ValidationException e) { // 验证异常,记录日志但不回滚 log.warn("Validation failed: {}", e.getMessage()); throw new BusinessException("Invalid request", e); } catch (ExternalServiceException e) { // 外部服务异常,可能需要重试 log.error("External service failed: {}", e.getMessage()); // 标记为需要重试 markForRetry(request); throw e; } catch (Exception e) { // 其他异常,记录详细信息 log.error("Unexpected error in business operation", e); throw new SystemException("System error occurred", e); } } // 使用自定义异常控制回滚行为 @Transactional(rollbackFor = {DataIntegrityException.class}, noRollbackFor = {BusinessWarningException.class}) public void selectiveRollbackOperation() { try { performDataOperation(); } catch (BusinessWarningException e) { // 业务警告不回滚事务,但记录警告 log.warn("Business warning: {}", e.getMessage()); } }}运行本项目java运行6.2 常见问题与解决方案6.2.1 事务失效问题// 问题:内部方法调用导致事务失效@Servicepublic class TransactionFailureService { public void publicMethod() { // 直接调用内部方法,@Transactional不会生效 this.internalTransactionalMethod(); } @Transactional private void internalTransactionalMethod() { // 事务不会生效,因为是内部调用 performDatabaseOperation(); }}// 解决方案1:使用自注入@Servicepublic class SelfInjectionService { @Autowired private SelfInjectionService self; public void publicMethod() { // 通过代理对象调用,事务生效 self.internalTransactionalMethod(); } @Transactional public void internalTransactionalMethod() { performDatabaseOperation(); }}// ✅ 解决方案2:拆分到不同的Service@Servicepublic class CallerService { @Autowired private TransactionalService transactionalService; public void publicMethod() { transactionalService.transactionalMethod(); }}@Servicepublic class TransactionalService { @Transactional public void transactionalMethod() { performDatabaseOperation(); }}运行本项目java运行6.2.2 事务超时处理@Servicepublic class TimeoutHandlingService { // 设置合理的超时时间 @Transactional(timeout = 30) // 30秒超时 public void normalOperation() { performQuickOperation(); } // 长时间运行的操作需要更长的超时时间 @Transactional(timeout = 300) // 5分钟超时 public void longRunningOperation() { performBatchOperation(); } // 对于可能很长的操作,考虑分批处理 public void processLargeDataset(List<Data> dataList) { int batchSize = 100; for (int i = 0; i < dataList.size(); i += batchSize) { List<Data> batch = dataList.subList(i, Math.min(i + batchSize, dataList.size())); processBatch(batch); } } @Transactional(timeout = 60) private void processBatch(List<Data> batch) { for (Data data : batch) { dataRepository.save(data); } }}运行本项目java运行6.2.3 死锁预防@Servicepublic class DeadlockPreventionService { // 按固定顺序获取锁,避免死锁 @Transactional public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) { // 确保按ID顺序获取锁 Long firstId = Math.min(fromAccountId, toAccountId); Long secondId = Math.max(fromAccountId, toAccountId); Account firstAccount = accountRepository.findByIdForUpdate(firstId); Account secondAccount = accountRepository.findByIdForUpdate(secondId); Account fromAccount = fromAccountId.equals(firstId) ? firstAccount : secondAccount; Account toAccount = toAccountId.equals(firstId) ? firstAccount : secondAccount; // 执行转账逻辑 performTransfer(fromAccount, toAccount, amount); } // 使用乐观锁避免死锁 @Transactional public void updateAccountWithOptimisticLock(Long accountId, BigDecimal amount) { int maxRetries = 3; int retryCount = 0; while (retryCount < maxRetries) { try { Account account = accountRepository.findById(accountId) .orElseThrow(() -> new AccountNotFoundException("Account not found")); account.setBalance(account.getBalance().add(amount)); accountRepository.save(account); return; // 成功,退出重试循环 } catch (OptimisticLockingFailureException e) { retryCount++; if (retryCount >= maxRetries) { throw new ConcurrencyException("Failed to update account after " + maxRetries + " retries"); } // 短暂等待后重试 try { Thread.sleep(100 * retryCount); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException("Thread interrupted", ie); } } } }}运行本项目java运行6.3 性能优化技巧6.3.1 批量操作优化@Servicepublic class BatchOptimizationService { // 低效的逐条处理 @Transactional public void inefficientBatchInsert(List<User> users) { for (User user : users) { userRepository.save(user); // 每次都执行SQL } } // 高效的批量处理 @Transactional public void efficientBatchInsert(List<User> users) { int batchSize = 50; for (int i = 0; i < users.size(); i += batchSize) { List<User> batch = users.subList(i, Math.min(i + batchSize, users.size())); userRepository.saveAll(batch); // 每批次后清理持久化上下文 if (i % batchSize == 0) { entityManager.flush(); entityManager.clear(); } } } // 使用JDBC批量操作 @Transactional public void jdbcBatchInsert(List<User> users) { String sql = "INSERT INTO users (name, email, status) VALUES (?, ?, ?)"; jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { User user = users.get(i); ps.setString(1, user.getName()); ps.setString(2, user.getEmail()); ps.setString(3, user.getStatus().name()); } @Override public int getBatchSize() { return users.size(); } }); }}运行本项目java运行6.3.2 连接池配置优化@Configurationpublic class DataSourceOptimizationConfig { @Bean public DataSource optimizedDataSource() { HikariConfig config = new HikariConfig(); // 基本连接配置 config.setJdbcUrl("jdbc:mysql://localhost:3306/testdb"); config.setUsername("root"); config.setPassword("password"); // 连接池优化配置 config.setMaximumPoolSize(20); // 最大连接数 config.setMinimumIdle(5); // 最小空闲连接数 config.setConnectionTimeout(30000); // 连接超时时间 config.setIdleTimeout(600000); // 空闲连接超时时间 config.setMaxLifetime(1800000); // 连接最大生存时间 // 性能优化配置 config.setLeakDetectionThreshold(60000); // 连接泄漏检测 config.addDataSourceProperty("cachePrepStmts", "true"); config.addDataSourceProperty("prepStmtCacheSize", "250"); config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); return new HikariDataSource(config); }}运行本项目java运行第七章:总结与展望7.1 核心知识点回顾通过本文的深入探讨,我们全面了解了Spring事务管理的各个方面:7.1.1 基础概念掌握ACID特性:原子性、一致性、隔离性、持久性是事务的基本保证Spring事务架构:PlatformTransactionManager、TransactionDefinition、TransactionStatus三大核心接口AOP实现原理:基于代理模式的声明式事务管理机制7.1.2 实践技能提升声明式事务:@Transactional注解的灵活使用和最佳实践编程式事务:TransactionTemplate和PlatformTransactionManager的直接使用传播行为:7种传播行为的适用场景和实际应用隔离级别:4种隔离级别的性能与一致性权衡7.1.3 问题解决能力常见陷阱:事务失效、死锁、超时等问题的识别和解决性能优化:批量操作、连接池配置、事务边界设计最佳实践:异常处理、只读事务、多数据源管理7.2 进阶学习路径7.2.1 深入源码研究// 建议研究的核心类// 1. AbstractPlatformTransactionManager - 事务管理器抽象实现// 2. TransactionInterceptor - 事务拦截器// 3. TransactionAspectSupport - 事务切面支持// 4. DefaultTransactionStatus - 事务状态实现// 示例:自定义事务管理器public class CustomTransactionManager extends AbstractPlatformTransactionManager { @Override protected Object doGetTransaction() throws TransactionException { // 获取事务对象的实现 return new CustomTransactionObject(); } @Override protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { // 开始事务的实现 } @Override protected void doCommit(DefaultTransactionStatus status) throws TransactionException { // 提交事务的实现 } @Override protected void doRollback(DefaultTransactionStatus status) throws TransactionException { // 回滚事务的实现 }}运行本项目java运行7.2.2 分布式事务探索// Seata分布式事务示例@GlobalTransactionalpublic class DistributedTransactionService { @Autowired private OrderService orderService; @Autowired private PaymentService paymentService; @Autowired private InventoryService inventoryService; public void createOrder(OrderRequest request) { // 创建订单 Order order = orderService.createOrder(request); // 扣减库存 inventoryService.deductInventory(request.getItems()); // 处理支付 paymentService.processPayment(order.getId(), request.getPaymentInfo()); // 如果任何服务失败,全局事务会回滚 }}运行本项目java运行7.2.3 响应式事务管理// Spring WebFlux响应式事务@Servicepublic class ReactiveTransactionService { @Autowired private ReactiveTransactionManager transactionManager; public Mono<User> createUserReactive(User user) { return transactionManager.execute(status -> { return userRepository.save(user) .doOnError(error -> status.setRollbackOnly()); }); }}运行本项目java运行7.3 扩展阅读建议7.3.1 官方文档与规范Spring Framework Reference Documentation - Transaction ManagementJDBC Transaction Management7.3.2 深度技术文章《Spring事务管理源码深度解析》《分布式事务解决方案对比》《数据库事务隔离级别实现原理》7.3.3 相关技术栈MyBatis事务集成:深入了解MyBatis与Spring事务的集成机制JPA事务管理:掌握JPA规范下的事务管理特性分布式事务框架:Seata、Saga、TCC等分布式事务解决方案7.4 实践项目建议7.4.1 基础练习项目// 项目1:银行转账系统// 功能要求:// 1. 实现账户间转账功能// 2. 支持并发转账处理// 3. 实现转账记录和审计日志// 4. 处理各种异常情况@Servicepublic class BankTransferProject { @Transactional(isolation = Isolation.SERIALIZABLE) public TransferResult transfer(TransferRequest request) { // 实现转账逻辑 // 考虑并发控制、异常处理、审计日志等 }}运行本项目java运行7.4.2 进阶挑战项目// 项目2:电商订单系统// 功能要求:// 1. 订单创建涉及多个服务(库存、支付、物流)// 2. 实现分布式事务管理// 3. 支持订单状态机和补偿机制// 4. 性能优化和监控@GlobalTransactionalpublic class ECommerceOrderProject { public OrderResult createOrder(OrderRequest request) { // 实现复杂的订单创建流程 // 涉及多个微服务的协调 }}运行本项目java运行7.5 技术发展趋势7.5.1 云原生事务管理随着微服务和云原生架构的普及,事务管理正朝着以下方向发展:Saga模式:长事务的分解和补偿机制事件驱动架构:基于事件的最终一致性服务网格集成:Istio等服务网格中的事务管理7.5.2 响应式编程支持Spring WebFlux和响应式编程的兴起带来了新的事务管理需求:非阻塞事务:基于响应式流的事务处理背压处理:在事务中处理背压和流控异步事务协调:跨异步边界的事务管理7.6 讨论与思考7.6.1 开放性问题性能vs一致性:在高并发场景下,如何平衡事务的性能和数据一致性?微服务事务:在微服务架构中,是否应该避免跨服务事务?有哪些替代方案?事务边界设计:如何设计合理的事务边界来平衡业务完整性和系统性能?7.6.2 实践挑战遗留系统改造:如何将传统的事务管理代码迁移到Spring事务管理?测试策略:如何有效测试事务相关的代码,特别是异常场景?监控和诊断:如何监控事务性能和诊断事务相关问题?7.7 结语Spring事务管理是企业级Java开发中的核心技能,掌握它不仅需要理解理论知识,更需要在实践中不断积累经验。本文提供了从基础到高级的全面指南,但技术的学习永无止境。希望读者能够:持续实践:在实际项目中应用所学知识深入研究:探索更深层次的实现原理分享交流:与同行分享经验和最佳实践关注发展:跟上技术发展的最新趋势记住,优秀的事务管理不仅仅是技术实现,更是对业务逻辑的深刻理解和对系统架构的整体把握。让我们在Spring事务管理的道路上持续前进,构建更加健壮、高效的企业级应用————————————————原文链接:https://blog.csdn.net/weixin_63944437/article/details/153634279
上滑加载中
推荐直播
-
华为云码道-玩转OpenClaw,在线养虾2026/03/11 周三 19:00-21:00
刘昱,华为云高级工程师/谈心,华为云技术专家/李海仑,上海圭卓智能科技有限公司CEO
OpenClaw 火爆开发者圈,华为云码道最新推出 Skill ——开发者只需输入一句口令,即可部署一个功能完整的「小龙虾」智能体。直播带你玩转华为云码道,玩转OpenClaw
回顾中 -
华为云码道-AI时代应用开发利器2026/03/18 周三 19:00-20:00
童得力,华为云开发者生态运营总监/姚圣伟,华为云HCDE开发者专家
本次直播由华为专家带你实战应用开发,看华为云码道(CodeArts)代码智能体如何在AI时代让你的创意应用快速落地。更有华为云HCDE开发者专家带你用码道玩转JiuwenClaw,让小艺成为你的AI助理。
回顾中 -
Skill 构建 × 智能创作:基于华为云码道的 AI 内容生产提效方案2026/03/25 周三 19:00-20:00
余伟,华为云软件研发工程师/万邵业(万少),华为云HCDE开发者专家
本次直播带来两大实战:华为云码道 Skill-Creator 手把手搭建专属知识库 Skill;如何用码道提效 OpenClaw 小说文本,打造从大纲到成稿的 AI 原创小说全链路。技术干货 + OPC创作思路,一次讲透!
回顾中
热门标签