首页 文章 精选 留言 我的

精选列表

搜索[认知分析],共10000篇文章
优秀的个人博客,低调大师

【干货】Kafka 事务特性分析

特性背景 消息事务是指一系列的生产、消费操作可以要么都完成,要么都失败,类似数据库的事务。这个特性在0.10.2的版本是不支持的,从0.11版本开始才支持。华为云DMS率先提供Kafka 1.1.0的专享版服务,支持消息事务特性。 支持事务消息有什么作用?消息事务是实现分布式事务的一种方案,可以确保分布式场景下的数据最终一致性。例如最常用的转账场景,小王 转账到小明,实际操作是小王账户减去相应金额,小明的账户增加相应金额,在分库分表的前提下,2个账户存储在不同的数据库中,这时需要分布式事务才能保证数据库一致性,单个数据库的事务无法保证跨库之间的原子性。如果小王账户先扣钱,再去发送消息到小明所在的数据库去通知增加钱,在没有事务消息的情况下,无论是先扣钱或者先发送通知增加钱,都会有数据不一致的问题,因为无法保证两者的原子性。而有了事务消息,可以保证发送通知与本地事务(扣钱)是一个原子操作,本地事务与发送通知可以同时成功或者同时失败,确保数据一致。 除了数据最终一致性外,还实现了消息Exactly once语义。所谓Exactly once语义是消息传递语义中最难实现的一种,包括At most once:最多一次(不会重复,但是可能丢失数据); At least once:至少投递一次(不会丢失,但是会导致重复)和Exactly once: 刚好一次(不丢不重),也即幂等性。Kafka的幂等性可以保证生产只对一个分区实现Exactl once语义,需要多个分区也实现这个语义,还需要引入消息事务确保原子性。 分布式事务介绍 当前系统架构主流是分布式架构与微服务架构,在这种架构下数据源不是单一的数据库,业务逻辑往往需要在多个数据库中实现原子操作,单个数据库中的强大的本地事务无法保证多节点原子操作。 此时需要分布式事务来确保数据的一致性。目前使用较多的分布式事务解决方案有几种: 1、XA事务:两阶段/三阶段提交 XA是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。实现XA事务的关键是两阶段和三阶段提交协议。 两阶段提交协议(Two-phase Commit,2PC)经常被用来实现分布式事务。一般分为协调器C和若干事务参与者Si两种角色,这里的事务参与者就是具体的数据库,协调器可以和事务参与者在一台机器上,如下图: 二阶段提交协议主要包括由2个阶段:第一个阶段为准备阶段(prepare),第二阶段为提交阶段。准备阶段由事务协调者向事务参与者发送prepare消息,各个参与者处理本地事务但不提交,然后向事务协调者返回事务状态。 提交阶段根据准备阶段各参与者的执行请求,协调者确定事务是提交或者回滚,向各个参与者发送命令。 二阶段提交协议主要的问题是在提交执行过程中,所有的参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,这样效率及其低下。特别是当协调者发出提交通知到部分参与者后宕机,其他参与者就会阻塞。 针对二阶段提交存在的问题,三阶段提交协议在prepare与commit阶段之间增加一个pre-commit阶段。Prepare阶段只询问参与者而不做事务,而在pre-commit阶段各个参与者才会执行本地事务但不提交。Commit阶段就是直接提交。这样做可以避免二阶段当协调者迟迟没有发出commit或者rollback通知,参与者在超时后可以自行提交或者回滚,避免阻塞事务(这是因为经过了prepare阶段已经确认了各个参与者是可以执行的,最后第三阶段直接执行即可)。 三阶段提交也存在很多问题,也不能完全保证数据一致,完全一致需要用到Paxos算法。 2、TCC补偿性事务解决方案 TCC分别对应Try、Confirm和Cancel三种操作,含义如下: Try:预留业务资源 Confirm:确认执行业务操作,执行事务 Cancel:取消执行业务操作 TCC解决了跨应用业务操作的原子性问题,在诸如组合支付、账务拆分场景非常实用。TCC实际上把数据库层的二阶段提交上提到了应用层来实现,对于数据库来说是一阶段提交,规避了数据库层的2PC性能低下问题。TCC需要业务提供使用,开发复杂和成本高。 3、事务消息 基于消息中间件的事务消息来完成分布式事务。事务消息可以确保本地执行事务与消息发送是原子的:先发送一条消息到消息中间件,然后执行本地事务,当本地事务成功后再发送提交确认到消息中间件,然后这条消息才能被其他业务消费者所能感知,从而确保原子性。 Kafka消息事务 一、基本概念 为了支持事务,Kafka 0.11.0版本引入以下概念: 1.事务协调者:类似于消费组负载均衡的协调者,每一个实现事务的生产端都被分配到一个事务协调者(Transaction Coordinator)。 2.引入一个内部Kafka Topic作为事务Log:类似于消费管理Offset的Topic,事务Topic本身也是持久化的,日志信息记录事务状态信息,由事务协调者写入。 3.引入控制消息(Control Messages):这些消息是客户端产生的并写入到主题的特殊消息,但对于使用者来说不可见。它们是用来让broker告知消费者之前拉取的消息是否被原子性提交。 4.引入TransactionId:不同生产实例使用同一个TransactionId表示是同一个事务,可以跨Session的数据幂等发送。当具有相同Transaction ID的新的Producer实例被创建且工作时,旧的且拥有相同Transaction ID的Producer将不再工作,避免事务僵死。 5.Producer ID:每个新的Producer在初始化的时候会被分配一个唯一的PID,这个PID对用户是不可见的。主要是为提供幂等性时引入的。 6.Sequence Numbler。(对于每个PID,该Producer发送数据的每个都对应一个从0开始单调递增的Sequence Number。 7.每个生产者增加一个epoch:用于标识同一个事务Id在一次事务中的epoch,每次初始化事务时会递增,从而让服务端可以知道生产者请求是否旧的请求。 8.幂等性:保证发送单个分区的消息只会发送一次,不会出现重复消息。增加一个幂等性的开关enable.idempotence,可以独立与事务使用,即可以只开启幂等但不开启事务。 二、事务流程 如下图所示: 1、查找事务协调者 生产者会首先发起一个查找事务协调者的请求(FindCoordinatorRequest)。协调者会负责分配一个PID给生产者。类似于消费组的协调者。 2、获取produce ID 在知道事务协调者后,生产者需要往协调者发送初始化pid请求(initPidRequest)。这个请求分两种情况: ●不带transactionID 这种情况下直接生成一个新的produce ID即可,返回给客户端 ●带transactionID 这种情况下,kafka根据transactionalId获取对应的PID,这个对应关系是保存在事务日志中(上图2a)。这样可以确保相同的TransactionId返回相同的PID,用于恢复或者终止之前未完成的事务。 3、启动事务 生产者通过调用beginTransaction接口启动事务,此时只是内部的状态记录为事务开始,但是事务协调者认为事务开始只有当生产者开始发送第一条消息才开始。 4、消费和生产配合过程 这一步是消费和生成互相配合完成事务的过程,其中涉及多个请求: ●增加分区到事务请求 当生产者有新分区要写入数据,则会发送AddPartitionToTxnRequest到事务协调者。协调者会处理请求,主要做的事情是更新事务元数据信息,并把信息写入到事务日志中(事务Topic)。 ●生产请求 生产者通过调用send接口发送数据到分区,这些请求新增pid,epoch和sequence number字段。 ●增加消费offset到事务 生产者通过新增的snedOffsets ToTransaction接口,会发送某个分区的Offset信息到事务协调者。协调者会把分区信息增加到事务中。 ●事务提交offset请求 当生产者调用事务提交offset接口后,会发送一个TxnOffsetCommitRequest请求到消费组协调者,消费组协调者会把offset存储在__consumer-offsets Topic中。协调者会根据请求的PID和epoch验证生产者是否允许发起这个请求。 消费offset只有当事务提交后才对外可见。 5、提交或回滚事务 用户通过调用commitTransaction或abortTranssaction方法提交或回滚事务。 ●EndTxnRequest 当生产者完成事务后,客户端需要显式调用结束事务或者回滚事务。前者会使得消息对消费者可见,后者会对生产数据标记为Abort状态,使得消息对消费者不可见。无论是提交或者回滚,都是发送一个EndTnxRequest请求到事务协调者,写入PREPARE_COMMIT或者PREPARE_ABORT信息到事务记录日志中(5.1a)。 ●WriteTxnMarkerRequest 这个请求是事务协调者向事务中每个TopicPartition的Leader发送的。每个Broker收到请求后会写入COMMIT(PID)或者ABORT(PID)控制信息到数据日志中(5.2a)。 这个信息用于告知消费者当前消息是哪个事务,消息是否应该接受或者丢弃。而对于未提交消息,消费者会缓存该事务的消息直到提交或者回滚。 这里要注意,如果事务也涉及到__consumer_offsets,即该事务中有消费数据的操作且将该消费的Offset存于__consumer_offsets中,Transaction Coordinator也需要向该内部Topic的各Partition的Leader发送WriteTxnMarkerRequest从而写入COMMIT(PID)或COMMIT(PID)控制信息(5.2a 左边)。 ●写入最终提交或回滚信息 当提交和回滚信息写入数据日子后,事务协调者会往事务日志中写入最终的提交或者终止信息以表示事务已经完成(图5.3),此时大部分于事务有关系的消息都可以被删除(通过标记后面在日志压缩时会被移除),我们只需要保留事务ID以及其时间戳即可。 接口 示例 欢迎工作一到五年的Java工程师朋友们加入Java架构开发:744677563 本群提供免费的学习指导 架构资料 以及免费的解答 不懂得问题都可以在本群提出来 之后还会有职业生涯规划以及面试指导

优秀的个人博客,低调大师

Docker Start命令流程分析

首先说明一下在控制台输入docker start命令后的主要流程: postContainersStart (api/server/router/container_routes) ContainerStart (daemon/start.go) GetContainer #根据名字获取景象 validateState #检查container的状态 daemon.containerStart # 进行请求 container.Lock() #上锁 daemon.conditionalMountOnStart #待补充详细过程 daemon.initializeNetworking #(daemon/container_operations) #初始化网络 在这里,网络模式有container,host,default模式 if IsContainer #如果网络模式是container,需要获取需要加入容器的网络配置 if IsHost #如果网络模式是host,hostName设置成主机名字,网络配置与宿主机相同 daemon.createSpec (daemon/oci_linux.go) #应该是设置具体性能配置的,比如cpu速率等 daemon.getLibcontainerdCreateOptions #得到创建容器的选择,应该是启动容器时,需要在容器内执行的命令 daemon.containerd.Create (libcontainerd/client_daemon) #创建啥玩意 daemon.containerd.Start (libcontainerd/client_daemon) #启动啥玩意 container.SetRunning #设置容器的状态为running daemon.setStateCounter #设置daemon的计数 daemon.initHealthMonitor #初始化容器监控 daemon.LogContainerEvent #日志记录容器启动

优秀的个人博客,低调大师

java core dump分析实战

hs_err_pid简介 hs_err_pid<pid>.log是java程序发生core的时候产生的文件,里面有当时出错时jvm的执行情况。 排查方法 头文件解读可以查看问题 头文件包含了简单的信息阐述,里面就core掉的原因 # # An unexpected error has been detected by Java Runtime Environment: # # EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x69ba3a96, pid=376, tid=1848 # # Java VM: Java HotSpot(TM) Client VM (11.0-b16 mixed mode, sharing windows-x86) # Problematic frame: # C [nvoglnt.dll+0x6a3a96] # 这里的问题是动态库的调用,这个库如果不是java自带的库,而是第三方的库,那么可以查看这个库是否和机型系统想匹配,如果不匹配换匹配的库就好,如果匹配,需要重新安装该库尝试。 # # Out of Memory Error (os_linux_x86.cpp:718), pid=29229, tid=140505843455744 # # JRE version: Java(TM) SE Runtime Environment (7.0_67-b01) (build 1.7.0_67-b01) # Java VM: Java HotSpot(TM) 64-Bit Server VM (24.65-b04 mixed mode linux-amd64 compressed oops) # Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again # 这里直接指出了问题的原因是oom,后面还会有物理内存状态,可以用来印证这个错误。 头文件+栈信息 # # An unexpected error has been detected by HotSpot Virtual Machine: # # EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x009fcf52, pid=4344, tid=5876 # # Java VM: Java HotSpot(TM) Client VM (1.5.0_14-b03 mixed mode) # Problematic frame: # V [jvm.dll+0x9cf52] # 头文件信息告诉我们是jvm.dll这个是jvm自己的库。此时就不能单纯的依靠这个信息来换库解决。 必须看栈信息,一般会有两个模块,一个是Native frames一个是Java frames。 Stack: [0x00030000,0x00070000), sp=0x0006f934, free space=254k Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code) V [jvm.dll+0x9cf52] C [NativeCode.dll+0x148b] C [NativeCode.dll+0x1253] j com.sy.test.TestNative.sayHello()V+0 j com.sy.test.TestNative.main([Ljava/lang/String;)V+22 v ~StubRoutines::call_stub V [jvm.dll+0x875dd] V [jvm.dll+0xdfd96] V [jvm.dll+0x874ae] V [jvm.dll+0x8e6f1] C [javaw.exe+0x14c5] C [javaw.exe+0x3151] C [kernel32.dll+0x16fd7] Java frames: (J=compiled Java code, j=interpreted, Vv=VM code) j com.sy.test.TestNative.sayHello()V+0 j com.sy.test.TestNative.main([Ljava/lang/String;)V+22 v ~StubRoutines::call_stub 上面的信息,是从java调用到c了,极大的可能是jni的形式,此时需要查看,后面的PATH,LD_LIBRARY_PATH,以及-Djava.library.path查看是否编写了JNI。 场景问题 80%的问题上面的方法都可以推断和解决,下面说说20%的情况 jit问题表征 # JRE version: Java(TM) SE Runtime Environment (8.0_25-b17) (build 1.8.0_25-b17) # Java VM: Java HotSpot(TM) 64-Bit Server VM (25.25-b02 mixed mode linux-amd64 compressed oops) # Problematic frame: # C 0x0000000764928700 Stack: [0x00002b9213782000,0x00002b9213803000], sp=0x00002b92137fe6d0, free space=497k Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code) C 0x0000000764928700 这种直接就是问题都是寄存器地址的,也没有栈信息的,基本问题在JIT # Problematic frame: # J org.apache.http.impl.cookie.BestMatchSpec.formatCookies(Ljava/util/List;)Ljava/util/List; 上面这种造成core的是java代码引起的,是解释器的问题,基本问题在JIT 纯jvm代码 # JRE version: 6.0_45-b06 # Java VM: Java HotSpot(TM) 64-Bit Server VM (20.45-b01 mixed mode linux-amd64 compressed oops) # Problematic frame: # v ~StubRoutines::jshort_disjoint_arraycopy Stack: [0x00007fecfc9dc000,0x00007fecfca1d000], sp=0x00007fecfca1b4b0, free space=253k Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code) v ~StubRoutines::jshort_disjoint_arraycopy 上图整体的信息都是jvm的代码,这种情况一般是jvm的bug,可以查jvm的bug列表并且更换jdk 引用案例出处http://bbs.csdn.net/topics/390975171http://blog.csdn.net/zct08/article/details/6333467http://m635674608.iteye.com/blog/2257650

优秀的个人博客,低调大师

ArrayList 序列化分析

0x1 摘要 相信ArrayList是Java开发过程中最常用的集合类之一,底层存储结构是数组,这篇文章不讲解底层数据结构的实现,主要讲解它的序列化机制,大家都知道ArrayList是可以序列化的,但也有一些人不知道具体是怎么序列化的,希望这篇章可以帮到大家。 0x2 ArrayList 类结构图 从类结构图可以看出实现了Serializable接口,说明是可以被序列化的。 0x3 序列化过程详解 在讲解序列过程前,先要清楚ArrayList底层数据结构,摘要中已经提到底层是通过数组实现,下面从源码看一下: /** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. Any * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA * will be expanded to DEFAULT_CAPACITY when the first element is added. */ transient Object[] elementData; // non-private to simplify nested class access elementData成员变量就是用来存储元素,细心的同学可能已经发现,此成员变量用transient关键字修饰,而transient关键字的作用是不需要序列化,存储数据的数组不序列化,那ArrayList是怎么实现数据序列化的呢?继续翻源码会发现存在writeObject、readObject两个方法, /** * Save the state of the <tt>ArrayList</tt> instance to a stream (that * is, serialize it). * * @serialData The length of the array backing the <tt>ArrayList</tt> * instance is emitted (int), followed by all of its elements * (each an <tt>Object</tt>) in the proper order. */ private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone() s.writeInt(size); // Write out all elements in the proper order. for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } /** * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is, * deserialize it). */ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { elementData = EMPTY_ELEMENTDATA; // Read in size, and any hidden stuff s.defaultReadObject(); // Read in capacity s.readInt(); // ignored if (size > 0) { // be like clone(), allocate array based upon size not capacity ensureCapacityInternal(size); Object[] a = elementData; // Read in all elements in the proper order. for (int i=0; i<size; i++) { a[i] = s.readObject(); } } } 从源码上可以看出,从数组中按size大小将元素取出来挨个写入到ObjectOutputStream中,反序列化时首先从ObjectInputStream中取得size大小,再循环取得元素。正常情况下,针对要序列化的对象不需要定义writeObject、readObject方法,采用默认序列化、反序列化方式,如果有特殊需求,则可以在对象中定义writeObject、readObject方法进行扩展,详细实现原理下次写序列化文章时再介绍。那么,ArrayList为什么要采用这种机制呢?上文中已经提到ArrayList底层采用数组存储,当声明一个数组时长度是固定的,在ArrayList中遇到元素个数超过数组大小时会采用自动扩展的方式来加大数组容量,而扩展方法是在原有数组容量的基础上扩大到1.5倍,具体实现可以查看ArrayList#grow方法。这种扩展方法会带来底层数组会存在空值问题,也就是真实数组容量远大于实际元素个数,在这样的前提下,如果直接序列elementData数组会带来不必要的开销,序列化很多空值,从而降低了序列化、反序列化性能,所以通过writeObject、readObject方法来实现序列化实际的元素,从而提升性能。

优秀的个人博客,低调大师

Java-- String源码分析

本篇博文基于java8,主要探讨java中的String源码。 首先,将一个类分为几个部分,分别是类定义(继承,实现接口等),全局变量,方法,内部类等等,再分别对这几个部分进行说明,这样到最后类的全貌也就比较直观了。 一:实现接口。 public final class String implements java.io.Serializable, Comparable<String>, CharSequence { java.io.Serializable 这个序列化接口没有任何方法和域,仅用于标识序列化的语意。 Comparable<String> 这个接口只有一个compareTo(T 0)接口,用于对两个实例化对象比较大小。 CharSequence 这个接口是一个只读的字符序列。包括length(), charAt(int index), subSequence(int start, int end)这几个API接口,值得一提的是,StringBuffer和StringBuild也是实现了改接口。 二:主要变量。 /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator(); 可以看到,value[]是存储String的内容的,即当使用String str = "abc";的时候,本质上,"abc"是存储在一个char类型的数组中的。 而hash是String实例化的hashcode的一个缓存。因为String经常被用于比较,比如在HashMap中。如果每次进行比较都重新计算hashcode的值的话,那无疑是比较麻烦的,而保存一个hashcode的缓存无疑能优化这样的操作。 最后,这个CASE_INSENSITIVE_ORDER在下面内部类中会说到,其根本就是持有一个静态内部类,用于忽略大小写得比较两个字符串。 三:内部类。 再String只有一个内部类,那就是 private static class CaseInsensitiveComparator implements Comparator<String>, java.io.Serializable { // use serialVersionUID from JDK 1.2.2 for interoperability private static final long serialVersionUID = 8575799808933029326L; public int compare(String s1, String s2) { int n1 = s1.length(); int n2 = s2.length(); int min = Math.min(n1, n2); for (int i = 0; i < min; i++) { char c1 = s1.charAt(i); char c2 = s2.charAt(i); if (c1 != c2) { c1 = Character.toUpperCase(c1); c2 = Character.toUpperCase(c2); if (c1 != c2) { c1 = Character.toLowerCase(c1); c2 = Character.toLowerCase(c2); if (c1 != c2) { // No overflow because of numeric promotion return c1 - c2; } } } } return n1 - n2; } /** Replaces the de-serialized object. */ private Object readResolve() { return CASE_INSENSITIVE_ORDER; } } 这里有一个疑惑,在String中已经有了一个compareTo的方法,为什么还要有一个CaseInsensitiveComparator的内部静态类呢? 其实这一切都是为了代码复用。 首先看一下这个类就会发现,其实这个比较和compareTo方法也是有差别的,这个方法在比较时是忽略大小写的。 而且这是一个单例,可以简单得用它来比较两个String,因为String类提供一个变量:CASE_INSENSITIVE_ORDER 来持有这个内部类,这样当要比较两个String时可以通过这个变量来调用。 其次,可以看到String类中提供的compareToIgnoreCase方法其实就是调用这个内部类里面的方法实现的。这就是代码复用的一个例子。 四:方法。 首先是一系列的初始化方法。 public String() { this.value = "".value; } String支持多种初始化方法,包括接收String,char[],byte[],StringBuffer等多种参数类型的初始化方法。但本质上,其实就是将接收到的参数传递给全局变量value[]。 public int length() { return value.length; } public boolean isEmpty() { return value.length == 0; } public char charAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return value[index]; } 知道了String其实内部是通过char[]实现的,那么就不难发现length(),isEmpty(),charAt()这些方法其实就是在内部调用数组的方法。 //返回指定索引的代码点 public int codePointAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return Character.codePointAtImpl(value, index, value.length); } //返回指定索引前一个代码点 public int codePointBefore(int index) { int i = index - 1; if ((i < 0) || (i >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return Character.codePointBeforeImpl(value, index, 0); } //返回指定起始到结束段内字符个数 public int codePointCount(int beginIndex, int endIndex) { if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) { throw new IndexOutOfBoundsException(); } return Character.codePointCountImpl(value, beginIndex, endIndex - beginIndex); } //返回指定索引加上codepointOffset后得到的索引值 public int offsetByCodePoints(int index, int codePointOffset) { if (index < 0 || index > value.length) { throw new IndexOutOfBoundsException(); } return Character.offsetByCodePointsImpl(value, 0, value.length, index, codePointOffset); } 这几个函数用得比较少,并且可以看到其本质上都是用Character这个类的一些静态方法来实现。这些功能在平常并不经常使用,个人认为,如果使用的话那应该是在对未知字符串进行处理,且重点在异常处理上。 这里说明一下,16 位unicode编码的所有 65,536 个字符并不能完全表示全世界所有正在使用或曾经使用的字符。于是,Unicode 标准已扩展到包含多达 1,112,064 个字符。那些超出原来的16 位限制的字符被称作增补字符。Java的char类型是固定16bits的。代码点在U+0000 — U+FFFF之内到是可以用一个char完整的表示出一个字符。但代码点在U+FFFF之外的,一个char无论如何无法表示一个完整字符。这样用char类型来获取字符串中的那些代码点在U+FFFF之外的字符就会出现问题。 增补字符是代码点在 U+10000 至 U+10FFFF 范围之间的字符,也就是那些使用原始的 Unicode 的 16 位设计无法表示的字符。从 U+0000 至 U+FFFF 之间的字符集有时候被称为基本多语言面 (BMP UBasic Multilingual Plane)。因此,每一个 Unicode 字符要么属于 BMP,要么属于增补字符。 //将字符串复制到dst数组中,复制到dst数组中的起始位置可以指定。值得注意的是,该方法并没有检测复制到dst数组后是否越界。 void getChars(char dst[], int dstBegin) { System.arraycopy(value, 0, dst, dstBegin, value.length); } public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { if (srcBegin < 0) { throw new StringIndexOutOfBoundsException(srcBegin); } if (srcEnd > value.length) { throw new StringIndexOutOfBoundsException(srcEnd); } if (srcBegin > srcEnd) { throw new StringIndexOutOfBoundsException(srcEnd - srcBegin); } System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); } 可以看到,这个两个重载方法本质上都是调用System.arraycopy()这个函数,包括在jdk很多其他源码中都是这样,比如ThreadPoolExcuter,看似有很多个重载,其实本质上都是调用同样的一个函数,只是会给你不同的默认初始值。 //获取当前字符串的二进制 public void getBytes(int srcBegin, int srcEnd, byte dst[], int dstBegin) { if (srcBegin < 0) { throw new StringIndexOutOfBoundsException(srcBegin); } if (srcEnd > value.length) { throw new StringIndexOutOfBoundsException(srcEnd); } if (srcBegin > srcEnd) { throw new StringIndexOutOfBoundsException(srcEnd - srcBegin); } Objects.requireNonNull(dst); int j = dstBegin; int n = srcEnd; int i = srcBegin; char[] val = value; /* avoid getfield opcode */ while (i < n) { dst[j++] = (byte)val[i++]; } } public byte[] getBytes(String charsetName) throws UnsupportedEncodingException { if (charsetName == null) throw new NullPointerException(); return StringCoding.encode(charsetName, value, 0, value.length); } public byte[] getBytes() { return StringCoding.encode(value, 0, value.length); } 将String字符串转成二进制的几种方式,可以指定byte数组,也能让其返回一个byte数组。本质上,其实都是调用了StringCoding.encode()这个静态方法。 public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; } public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; } hashCode()和equals()两个方法比较重要且有所关系就放一起了,equals()是string能成为广泛用于Map[key,value]中key的关键所在。 此外除equals()外,还有只比较内容的contentEquals(); public boolean contentEquals(CharSequence cs) { // Argument is a StringBuffer, StringBuilder if (cs instanceof AbstractStringBuilder) { if (cs instanceof StringBuffer) { synchronized(cs) { return nonSyncContentEquals((AbstractStringBuilder)cs); } } else { return nonSyncContentEquals((AbstractStringBuilder)cs); } } // Argument is a String if (cs instanceof String) { return equals(cs); } // Argument is a generic CharSequence char v1[] = value; int n = v1.length; if (n != cs.length()) { return false; } for (int i = 0; i < n; i++) { if (v1[i] != cs.charAt(i)) { return false; } } return true; } 这个主要是用来比较String和StringBuffer或者StringBuild的内容是否一样。可以看到传入参数是CharSequence ,这也说明了StringBuffer和StringBuild同样是实现了CharSequence。源码中先判断参数是从哪一个类实例化来的,再根据不同的情况采用不同的方案,不过其实大体都是采用上面那个for循环的方式来进行判断两字符串是否内容相同。 public int compareTo(String anotherString) { int len1 = value.length; int len2 = anotherString.value.length; int lim = Math.min(len1, len2); char v1[] = value; char v2[] = anotherString.value; int k = 0; while (k < lim) { char c1 = v1[k]; char c2 = v2[k]; if (c1 != c2) { return c1 - c2; } k++; } return len1 - len2; } 这个就是String对Comparable接口中方法的实现了。其核心就是那个while循环,通过从第一个开始比较每一个字符,当遇到第一个较小的字符时,判定该字符串小。 但还有一种是在较小长度的字符粗每个字符都和另一个字符串的每个字符相等,那么字符串长度较大的较大。 public int compareToIgnoreCase(String str) { return CASE_INSENSITIVE_ORDER.compare(this, str); } 这个也是比较字符串大小,规则和上面那个比较方法基本相同,差别在于这个方法忽略大小写。可以看到这是通过一个String 内部一个static的内部类实现的,那么为什么还要特地写一个内部类呢,这样其实就是为了代码复用,这样在其他情况下也可以使用这个static内部类。 public boolean regionMatches(int toffset, String other, int ooffset, int len) { char ta[] = value; int to = toffset; char pa[] = other.value; int po = ooffset; // Note: toffset, ooffset, or len might be near -1>>>1. if ((ooffset < 0) || (toffset < 0) || (toffset > (long)value.length - len) || (ooffset > (long)other.value.length - len)) { return false; } while (len-- > 0) { if (ta[to++] != pa[po++]) { return false; } } return true; } 比较该字符串和其他一个字符串从分别指定地点开始的n个字符是否相等。看代码可知道,其原理还是通过一个while去循环对应的比较区域进行判断,但在比较之前会做判定,判定给定参数是否越界。 public boolean startsWith(String prefix, int toffset) { char ta[] = value; int to = toffset; char pa[] = prefix.value; int po = 0; int pc = prefix.value.length; // Note: toffset might be near -1>>>1. if ((toffset < 0) || (toffset > value.length - pc)) { return false; } while (--pc >= 0) { if (ta[to++] != pa[po++]) { return false; } } return true; } 判断当前字符串是否以某一段其他字符串开始的,和其他字符串比较方法一样,其实就是通过一个while来循环比较。 public int indexOf(int ch, int fromIndex) { final int max = value.length; if (fromIndex < 0) { fromIndex = 0; } else if (fromIndex >= max) { // Note: fromIndex might be near -1>>>1. return -1; } if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) { // handle most cases here (ch is a BMP code point or a // negative value (invalid code point)) final char[] value = this.value; for (int i = fromIndex; i < max; i++) { if (value[i] == ch) { return i; } } return -1; } else { return indexOfSupplementary(ch, fromIndex); } } public int indexOf(int ch) { return indexOf(ch, 0); } 可以看到这里在if中有一句 ch < Character.MIN_SUPPLEMENTARY_CODE_POINT而在Character中看到 public static final int MIN_SUPPLEMENTARY_CODE_POINT = 0x010000; 这表明在java中char存储的值通常都是比ox010000小的,就是BMP类型的字符。 而当比这个值大的时候,就是增补字符了,那么会调用Character先判断是否是有效的字符,再进一步处理。 public int lastIndexOf(int ch, int fromIndex) { if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) { // handle most cases here (ch is a BMP code point or a // negative value (invalid code point)) final char[] value = this.value; int i = Math.min(fromIndex, value.length - 1); for (; i >= 0; i--) { if (value[i] == ch) { return i; } } return -1; } else { return lastIndexOfSupplementary(ch, fromIndex); } } 和indexOf基本一致,只是顺序反过来。 static int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex) { if (fromIndex >= sourceCount) { return (targetCount == 0 ? sourceCount : -1); } if (fromIndex < 0) { fromIndex = 0; } if (targetCount == 0) { return fromIndex; } char first = target[targetOffset]; int max = sourceOffset + (sourceCount - targetCount); for (int i = sourceOffset + fromIndex; i <= max; i++) { /* Look for first character. */ if (source[i] != first) { while (++i <= max && source[i] != first); } /* Found first character, now look at the rest of v2 */ if (i <= max) { int j = i + 1; int end = j + targetCount - 1; for (int k = targetOffset + 1; j < end && source[j] == target[k]; j++, k++); if (j == end) { /* Found whole string. */ return i - sourceOffset; } } } return -1; } 这个是上面indexOf的一个重载,主要是实现找到某个子串在当前字符串的起始位置,若没找到,则返回-1。 大致说下这里的实现思路:先是进行一系列的初始判定,比如子串长度不能大于当前字符串。然后在当前字符串中找到子串的第一个字符的位置 i ,从这个位置开始,和子串每一个字符比较。若完全匹配,则返回结果,如果在这个过程中,某个字符不匹配,则从 i+1 的位置开始继续寻找子串第一个字符的位置,后继续比较。 public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); } 这个方法可以返回字符串中一个子串,看最后一行可以发现,其实就是指定头尾,然后构造一个新的字符串。 public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true); } concat的作用是将str拼接到当前字符串后面,通过代码也可以看出其实就是建一个新的字符串。 public String replace(char oldChar, char newChar) { if (oldChar != newChar) { int len = value.length; int i = -1; char[] val = value; /* avoid getfield opcode */ while (++i < len) { if (val[i] == oldChar) { break; } } if (i < len) { char buf[] = new char[len]; for (int j = 0; j < i; j++) { buf[j] = val[j]; } while (i < len) { char c = val[i]; buf[i] = (c == oldChar) ? newChar : c; i++; } return new String(buf, true); } } return this; } 替换操作,主要是将原来字符串中的oldChar全部替换成newChar。看这里实现,主要是先找到第一个所要替换的字符串的位置 i ,将i之前的字符直接复制到一个新char数组。然后从 i 开始再对每一个字符进行判断是不是所要替换的字符。 public boolean matches(String regex) { return Pattern.matches(regex, this); } public String replaceFirst(String regex, String replacement) { return Pattern.compile(regex).matcher(this).replaceFirst(replacement); } public String replaceAll(String regex, String replacement) { return Pattern.compile(regex).matcher(this).replaceAll(replacement); } public String replace(CharSequence target, CharSequence replacement) { return Pattern.compile(target.toString(), Pattern.LITERAL).matcher( this).replaceAll(Matcher.quoteReplacement(replacement.toString())); } 这几个方法都是使用了正则的方式来进行处理的。包括最后一个虽然参数不用提供正则规则,但内部其实也是使用了Pattern类的正则操作。 public String[] split(String regex, int limit) { /* fastpath if the regex is a (1)one-char String and this character is not one of the RegEx's meta characters ".$|()[{^?*+\\", or (2)two-char String and the first char is the backslash and the second is not the ascii digit or ascii letter. */ char ch = 0; if (((regex.value.length == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) || (regex.length() == 2 && regex.charAt(0) == '\\' && (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && ((ch-'a')|('z'-ch)) < 0 && ((ch-'A')|('Z'-ch)) < 0)) && (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE)) { int off = 0; int next = 0; boolean limited = limit > 0; ArrayList<String> list = new ArrayList<>(); while ((next = indexOf(ch, off)) != -1) { if (!limited || list.size() < limit - 1) { list.add(substring(off, next)); off = next + 1; } else { // last one //assert (list.size() == limit - 1); list.add(substring(off, value.length)); off = value.length; break; } } // If no match was found, return this if (off == 0) return new String[]{this}; // Add remaining segment if (!limited || list.size() < limit) list.add(substring(off, value.length)); // Construct result int resultSize = list.size(); if (limit == 0) { while (resultSize > 0 && list.get(resultSize - 1).length() == 0) { resultSize--; } } String[] result = new String[resultSize]; return list.subList(0, resultSize).toArray(result); } return Pattern.compile(regex).split(this, limit); } 这个方法看起来比较复杂,但其实我们一般都不会用到那一大串的内容,一般我们用到最后那一句return Pattern.compile(regex).split(this, limit); 即同样是使用Pattern的正则方式去解析并拆分成字符串数组。 那么进到那些复杂的代码里面需要什么条件呢,看那个if: 1. 如果regex只有一位,且不为列出的特殊字符; 2.如regex有两位,第一位为转义字符且第二位不是数字或字母,“|”表示或,即只要ch小于0或者大于9任一成立,小于a或者大于z任一成立,小于A或大于Z任一成立 3.第三个是不属于utf-16之间的字符 其中的关系为( (1 || 2) && 3 ),光看第三点就知道这是为了应对特殊情况的。其实也就是使用一个ArrayList<String>存放每一段找到分割点的字符串,不断循环。 public String trim() { int len = value.length; int st = 0; char[] val = value; /* avoid getfield opcode */ while ((st < len) && (val[st] <= ' ')) { st++; } while ((st < len) && (val[len - 1] <= ' ')) { len--; } return ((st > 0) || (len < value.length)) ? substring(st, len) : this; } 这个函数平时用的应该比较多,删除字符串前后的空格,原理是通过找出前后第一个不是空格的字符串,返回原字符串的该子串。 总结: 在String中,其实最底层的实现就是通过一个final char value[] 来保存String字符串的,抓住这一点,其实很多设计方法,方法的实现方式就显而易见了。

优秀的个人博客,低调大师

spring bean scope 用法分析

Spring 容器是应用组件 bean 创建和组装专家,支持 InitializingBean, AbstractBeanDefinition.setInitMethodName(String), @PostConstruct 等多种方法初始化(及销毁)bean。 单例(singleton)bean 最常使用的是单例(singleton)bean,Spring 自动帮我们管理其生命周期。 代码示例如下: class Rick { @PostConstruct void init() { System.out.println("rick init"); } @PreDestroy void destroy() { System.out.println("rick des

优秀的个人博客,低调大师

php内核分析(五)-zval

这里阅读的php版本为PHP-7.1.0 RC3,阅读代码的平台为linux 实际上,从这个函数开始,就已经进入到了zend引擎的范围了。 zend_eval_string_ex(exec_direct, NULL, "Command line code", 1) 实际上是调用Zend/zend_execute_API.c zend_eval_stringl_ex(str, strlen(str), retval_ptr, string_name, handle_exceptions); 再进去是调用 result = zend_eval_stringl(str, str_len, retval_ptr, string_name); 这里的retval_ptr为NULL,string_name为"Command line code", str为"echo 12;" zend_eval_stringl 其实这个函数主流程并不复杂。简化下来就如下 ZEND_API int zend_eval_stringl(char *str, size_t str_len, zval *retval_ptr, char *string_name) /* {{{ */ { ... new_op_array = zend_compile_string(&pv, string_name); // 这个是把php代码编译成为opcode的过程 ... zend_execute(new_op_array, &local_retval); // 这个是具体的执行过程,执行opcode,把结果存储到local_retval中 ... retval = SUCCESS; return retval; } 先把php编译为opcode,然后执行这个opcode。只是这个函数有一些关键的结构需要理一下。 zval 我们会看到 zval local_retval; 这样的变量,然后会对这个变量进行如下操作: ZVAL_UNDEF(&local_retval); ZVAL_NULL(z) ZVAL_FALSE(z) ZVAL_TRUE(z) ZVAL_BOOL(z, b) ZVAL_LONG(z, l) ZVAL_DOUBLE(z, d) ZVAL_STR(z, s) ZVAL_INTERNED_STR(z, s) ZVAL_NEW_STR(z, s) ZVAL_STR_COPY(z, s) ZVAL_ARR(z, a) ZVAL_NEW_ARR(z) ZVAL_NEW_PERSISTENT_ARR(z) ZVAL_OBJ(z, o) ZVAL_RES(z, r) ZVAL_NEW_RES(z, h, p, t) ZVAL_NEW_PERSISTENT_RES(z, h, p, t) ZVAL_REF(z, r) ZVAL_NEW_EMPTY_REF(z) ZVAL_NEW_REF(z, r) ZVAL_NEW_PERSISTENT_REF(z, r) ZVAL_NEW_AST(z, a) ZVAL_INDIRECT(z, v) ZVAL_PTR(z, p) ZVAL_FUNC(z, f) ZVAL_CE(z, c) ZVAL_ERROR(z) php是一个弱类型的语言,它可以用一个$var来代表string,int,array,object等。这个就是归功于zval_struct结构 // zval的结构 struct _zval_struct { zend_value value; // 存储具体值,它的结构根据类型不同而不同 union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar type, // 这个位置标记了这个val是什么类型的(IS_STRING/IS_INT) zend_uchar type_flags, // 这个位置标记了这个val是什么属性 (IS_CALLABLE等) zend_uchar const_flags, // 常量的一些属性 (IS_CONSTANT_CLASS) zend_uchar reserved) // 保留的一些字段 } v; uint32_t type_info; // 类型的一些额外信息 } u1; // 保存类型的一些关键信息 union { uint32_t next; // 如果是在hash链表中,这个指针代表下一个元素的index uint32_t cache_slot; /* literal cache slot */ uint32_t lineno; /* line number (for ast nodes) */ uint32_t num_args; /* arguments number for EX(This) */ uint32_t fe_pos; /* foreach position */ uint32_t fe_iter_idx; /* foreach iterator index */ uint32_t access_flags; /* class constant access flags */ uint32_t property_guard; /* single property guard */ } u2; // 一些附属字段 }; 这个接口最重要的两个字段是 value,存储变量的值。另一个是u1.v.type 存储变量的类型。这里,value也是一个结构 typedef union _zend_value { zend_long lval; /* long value */ double dval; /* double value */ zend_refcounted *counted; zend_string *str; // string zend_array *arr; // array zend_object *obj; // object zend_resource *res; // resource zend_reference *ref; // 指针 zend_ast_ref *ast; // ast指针 zval *zv; void *ptr; zend_class_entry *ce; // class实体 zend_function *func; // 函数实体 struct { uint32_t w1; uint32_t w2; } ww; } zend_value; 如果u1.v.type == IS_STRING, 那么value.str就是指向了zend_string结构。好了,php的垃圾回收是通过引用计数来进行的,这个引用计数的计数器就放在zval.value.counted里面。 我们对zval设置的时候设置了一些宏来进行设置,比如:ZVAL_STRINGL是设置string,我们仔细看下调用堆栈: ZVAL_STRINGL(&pv, str, str_len); // 把pv设置为string类型,值为str 这个函数就是把pv设置为zend_string类型 // 带字符串长度的设置zend_sting类型的zval #define ZVAL_STRINGL(z, s, l) do { \ ZVAL_NEW_STR(z, zend_string_init(s, l, 0)); \ } while (0) 注意到,这里使用了一个写法,do {} while(0) 来设置一个宏,这个是C里面比较好的写法,这样写,能保证宏中定义的东西在for,if,等各种流程语句中不会出现语法错误。不过其实我们学习代码的时候,可以忽略掉这个框框写法。 zend_string_init(s, l, 0) ... // 从char* + 长度 + 是否是临时变量(persistent为0表示最迟这个申请的空间在请求结束的时候就进行释放),转变为zend_string* static zend_always_inline zend_string *zend_string_init(const char *str, size_t len, int persistent) { zend_string *ret = zend_string_alloc(len, persistent); // 申请空间,申请的大小为zend_string结构大小(除了val)+ len + 1 memcpy(ZSTR_VAL(ret), str, len); ZSTR_VAL(ret)[len] = '\0'; return ret; } 这个函数可以看的点有几个: persistent 这个参数是用来代表申请的空间是不是“临时”的。这里说的临时是zend提供的一种内存管理器,相关请求数据只服务于单个请求,最迟会在请求结束的时候释放。 临时内存申请对应的函数为: void *emalloc(size_t size) 而永久内存申请对应的函数为: malloc zend_string_alloc static zend_always_inline zend_string *zend_string_alloc(size_t len, int persistent) { zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent); GC_REFCOUNT(ret) = 1; GC_TYPE_INFO(ret) = IS_STRING | ((persistent ? IS_STR_PERSISTENT : 0) << 8); zend_string_forget_hash_val(ret); ZSTR_LEN(ret) = len; return ret; } 我们先看看zend_string的结构: // 字符串 struct _zend_string { zend_refcounted_h gc; // gc使用的被引用的次数 zend_ulong h; // 如果这个字符串作为hashtable的key在查找时候需要重复计算它的hash值,所以保存一份在这里 size_t len; // 字符串长度 char val[1]; // 柔性数组,虽然我们定义了数组只有一个元素,但是在实际分配内存的时候,会分配足够的内存 }; _ZSTR_STRUCT_SIZE(len) gc+h+len的空间,最后给了val留了len+1的长度 #define _ZSTR_STRUCT_SIZE(len) (_ZSTR_HEADER_SIZE + len + 1) ## GC_REFCOUNT(ret) = 1; #define GC_REFCOUNT(p) (p)->gc.refcount 这里就看到一个结构zend_refcounted_h typedef struct _zend_refcounted_h { uint32_t refcount; // 真正的计数 union { struct { ZEND_ENDIAN_LOHI_3( zend_uchar type, // 冗余了zval中的类型值 zend_uchar flags, // used for strings & objects中有特定作用 uint16_t gc_info) // 在GC缓冲区中的索引位置 } v; uint32_t type_info; // 冗余zval中的type_info } u; // 类型信息 } zend_refcounted_h; 回到我们的实例,我们调用的是 zend_string_init(s, l, 0) // s=char*(echo 12;) l=8 返回的zend_string实际值为: struct _zend_string { struct { uint32_t refcount; // 1 union { struct { ZEND_ENDIAN_LOHI_3( zend_uchar type, // IS_STRING zend_uchar flags, uint16_t gc_info) } v; uint32_t type_info; //IS_STRING | 0 => IS_STRING } u; } gc; zend_ulong h; // 0 size_t len; // 8 char val[1]; // echo 12;\0 }; 结合到zval里面,那么ZVAL_STRINGL(&pv, str, str_len);返回的zval为 // zval的结构 struct _zval_struct { union _zend_value { zend_long lval; double dval; zend_refcounted *counted; zend_string *str; // 指向到上面定义的那个zend_string中 zend_array *arr; zend_object *obj; zend_resource *res; zend_reference *ref; zend_ast_ref *ast; zval *zv; void *ptr; zend_class_entry *ce; zend_function *func; struct { uint32_t w1; uint32_t w2; } ww; } value; union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar type, zend_uchar type_flags, zend_uchar const_flags, zend_uchar reserved) } v; uint32_t type_info; // IS_STRING_EX } u1; union { uint32_t next; uint32_t cache_slot; uint32_t lineno; uint32_t num_args; uint32_t fe_pos; uint32_t fe_iter_idx; uint32_t access_flags; uint32_t property_guard; } u2; }; 这里,就对zval结构有初步了解了。 另外建议记住几个常用的类型,后续调试的时候会很有用 /* regular data types */ #define IS_UNDEF 0 #define IS_NULL 1 #define IS_FALSE 2 #define IS_TRUE 3 #define IS_LONG 4 #define IS_DOUBLE 5 #define IS_STRING 6 #define IS_ARRAY 7 #define IS_OBJECT 8 #define IS_RESOURCE 9 #define IS_REFERENCE 10 /* constant expressions */ #define IS_CONSTANT 11 #define IS_CONSTANT_AST 12 本文转自轩脉刃博客园博客,原文链接:http://www.cnblogs.com/yjf512/p/6108444.html,如需转载请自行联系原作者

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册