开着皮卡写代码
IP:
0关注数
0粉丝数
2获得的赞
工作年
编辑资料
链接我:

创作·52

全部
问答
动态
项目
学习
专栏
开着皮卡写代码

【JVM进阶之路】三:探究虚拟机对象

1、对象创建过程单纯从语言层面,新建一个对象,可以通过new、反射、复制、反序列化等等。接下来,我们探究以下在虚拟机中,对象的创建是一个什么样的过程。我们以虚拟机遇到一个new指令开始:首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就先执行相应的类加载过程类加载检查通过后,接下来虚拟机将为新生对象分配内存。内存分配有两种方式,指针碰撞(Bump The Pointer)、空闲列表(Free List)指针碰撞:假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”两种方式的选择由Java堆是否规整决定Java堆规整由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定内存分配完成之后,虚拟机将分配到的内存空间(但不包括对象头)都初始化为零值。接下来设置对象头,请求头里包含了对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这个过程大概图示如下:分配内存线程安全问题:对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。线程安全问题有两种解可选方案:一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。从虚拟机角度来看,设置完对象头信息以后初始化就已经完成了,但是对于Java程序而言,new指令之后会接着执行<init> ()方法,对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。2、对象的内存布局在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。HotSpot虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它为“Mark Word”。考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间。例如在64位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,Mark Word的64个比特存储空间中的31个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,在其他状态(轻量级锁、重量级锁、偏向锁)下对象的存储内容变化如图示。对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,查找对象的元数据信息并不一定要经过对象本身,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。3、对象的访问定位Java程序会通过栈上的reference数据来操作堆上的具体对象。由于reference类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种:如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如图所示:如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如图所示:这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本。HotSpot虚拟机主要使用直接指针来进行对象访问。
0
0
0
浏览量684
开着皮卡写代码

【JVM进阶之路】九:性能监控工具-可视化工具篇

在前面已经学习了JVM性能监控的命令行工具,接下来学习JVM性能监控的命令行工具,通过可视化工具可以更直观地监控JVM性能、处理JVM相关问题。1、JConsoleJConsole( Java Monitoring and Management Console),是一款基于 JMX( Java Manage-ment Extensions) 的可视化监视管理工具。它的功能主要是对系统进行收集和参数调整,不仅可以用在虚拟机本身的管理上,还可以用于运行于虚拟机之上的软件中。1.1、JConsole连接Java程序JConsole程序位于%JAVA_HOME%bin目录下,直接通过命令启动。在新建连接对话框中,罗列了所有的本地Java应用程序,选择需要连接的程序即可。下面还有一个用于连接远程进程的文本框,输入正确的远程地址即可连接。如果一个程序需要使用JConsole与那成连接,则需要在启动Java程序时,加上以下参数:JAVA_OPTS="-Dfile.encoding=UTF-8" JAVA_OPTS="$JAVA_OPTS -Dlog.dir=$LOG_PATH" JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=xxx.xxx.xxx.xxx(本机IP) -Dcom.sun.management.jmxremote" JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=xx" JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.local.only=false" JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"1.2、Java程序概况使用JConsole连接了一个本地程序,在概述可以看到Java程序运行的概览信息,包括堆内存使用情况、线程、类、CPU使用情况四项信息的曲线图。1.3、内存监控内存的作用相当于可视化的jstat命令,用于监视被收集器管理的虚拟机内存。它不仅包含堆内存的整体信息,更细化到eden区、suvivior区、老年代的使用情况。为了更加清晰地查看内存地变化,运行下面一段程序,然后连接:/** * VM参数: -Xms100m -Xmx100m -XX:+UseSerialGC */ public class JConcoleRAMMonitor { /*** * 内存占位符对象,一个OOMObject大约占64KB */ static class OOMObject { public byte[] placeholder = new byte[64 * 1024]; } public static void fillHeap(int num) throws InterruptedException { List<OOMObject> list = new ArrayList<OOMObject>(); for (int i = 0; i < num; i++) { // 稍作延时,令监视曲线的变化更加明显 Thread.sleep(300); list.add(new OOMObject()); } System.gc(); } public static void main(String[] args) throws Exception { fillHeap(2000); } }这段代码的作用是以64KB/50ms的速度向Java堆中填充数据,一共填充1000次。观察Eden区的运行趋势,发现呈折线。观察堆内存使用,发现以稍有曲折的状态向上增长。执行System.gc()之后,老年代的柱状图仍然显示峰值状态,最后程序会以堆内存溢出结束,这是因为空间未能回收——List<OOMObject>list对象一直存活, fillHeap()方法仍然没有退出,如果把 System.gc()移动到fillHeap()方法外调用就可以回收掉全部内存。1.4、线程监控JConcole还可以监控线程,相当于可视化的jstack命令。如图,JConcole显示了系统内的线程数量,并在屏幕下方显示了程序中所有的线程。单击线程名称,就可以查看线程的栈信息。使用JConsole还可以快速定位死锁问题。这是一段会产生死锁的代码:public class ThreadLockDemo { /** * 线程死锁等待演示 */ static class SynAddRunalbe implements Runnable { int a, b; public SynAddRunalbe(int a, int b) { this.a = a; this.b = b; } @Override public void run() { synchronized (Integer.valueOf(a)) { synchronized (Integer.valueOf(b)) { System.out.println(a + b); } } } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(new SynAddRunalbe(1, 2)).start(); new Thread(new SynAddRunalbe(2, 1)).start(); } } }出现线程死锁以后,点击JConsole线程面板的检测到死锁按钮,将会看到线程的死锁信息。可以看到线程Thread-199等待线程Thread-21持有的资源。1.5、类加载情况如图,类页面显示了已经装载的类数量。在详细信息栏中,还显示了已经卸载的类的数量。1.6、虚拟机信息在VM摘要,JConsole显示了当前应用程序的运行环境,包括虚拟机类型、版本、堆信息以及虚拟机参数等。2、VisualVMVisualVM(All-in-One Java Troubleshooting Tool)是功能最强大的运行监视和故障处理程序之一,曾经在很长一段时间内是Oracle官方主力发展的虚拟机故障处理工具。相比一些第三方工具,VisualVM有一个很大的优点:不需要被监视的程序基于特殊Agent去运行,因此它的通用性很强,对应用程序实际性能的影响也较小,使得它可以直接应用在生产环境中。2.1、VisualVM安装插件在JDK6 Update7以后,VisualVM便作为JDK的一部分发布,它在%JAVA_HOME%bin 目录下,点击就可以启动。VisualVM的精华之处在于它的插件。插件安装可以手动安装或者自动安装。手动安装,从地址 visualvm.github.io/pluginscent… 下载载nbm包,点击“工具->插件->已下载”菜单,然后在弹出对话框中指定nbm包路径便可完成安装。一般选择自动安装,点击工具-> 插件菜单,在可用插件里可以看到可安装的插件,按需安装即可。VisualVM中概述,监视、线程,MBeans的功能与前面介绍的JConsole差别不大,这里就不在赘言。2.2、生成、浏览堆转储快照在VisualVM中生成堆转储快照文件有两种方式,可以执行下列任一操作:在应用程序窗口中右键单击应用程序节点,然后选择堆Dump。在应用程序窗口中双击应用程序节点以打开应用程序标签,然后在“监视”标签中单击堆Dump。生成堆转储快照文件之后,该堆的应用程序下增加了一个以[heap-dump]开头的子节点。如果需要把堆转储快照保存或发送出去,就需要heapdump节点上右键选择“另存为”菜单,否则当VisualVM关闭时,生成的堆转储快照文件会被当作临时文件自动清理掉。要打开一个由已经存在的堆转储快照文件,通过文件菜单中的“装入”功能,选择磁盘上的文件即可。2.3、分析程序性能要开始性能分析,先选择“CPU”和“内存”按钮中的一个,然后切换到应用程序中对程序进行操作,VisualVM会记录这段时间中应用程序执行过的所有方法。如果是进行处理器执行时间分析,将会统计每个方法的执行次数、执行耗时;如果是内存分析,则会统计每个方法关联的对象数以及这些对象所占的空间。等要分析的操作执行结束后,点击“停止”按钮结束监控过程。2.4、BTrace动态日志跟踪BTrace是个很有意思的插件,它可以在不停机的情况下,通过字节码注入动态监控系统的运行情况。Btrace自动安装如下,到github的网络可能存在不稳定的问题,可以重试,或者手动安装在VisualVM中安装了BTrace插件后,在应用程序面板中右击要调试的程序,会出现“Trace Application…”菜单:点击将进入BTrace面板。这个面板看起来就像一个简单的Java程序开发环境:现在来尝试使用BTrace追踪正在运行的程序。一段简单的Java代码:产生两个1000以内的随机整数,输出这两个数字相加的结果。public class BTraceTest { public int add(int a, int b) { return a + b; } public static void main(String[] args) throws IOException { BTraceTest test = new BTraceTest(); BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); for (int i = 0; i < 10; i++) { reader.readLine(); int a = (int) Math.round(Math.random() * 1000); int b = (int) Math.round(Math.random() * 1000); System.out.println(test.add(a, b)); } } }运行程序,现在需要在不停止程序的情况下,监控程序中生成的两个随机数。在VisualVM中打开该程序的监视,在BTrace页 签填充TracingScript的内容,输入调试代码:/* BTrace Script Template */ import com.sun.btrace.annotations.*; import static com.sun.btrace.BTraceUtils.*; @BTrace public class TracingScript { @OnMethod(clazz = "cn.fighter3.test.BTraceTest", method = "add", location = @Location(Kind.RETURN) ) public static void func(@Self cn.fighter3.test.BTraceTest instance, int a, int b, @Return int result) { println("调用堆栈:"); jstack(); println(strcat("方法参数A:", str(a))); println(strcat("方法参数B:", str(b))); println(strcat("方法结果:", str(result))); } }点击start按钮,当程序运行时将会在Output面板输出调试信息:BTrace的用途很广泛,打印调用堆栈、参数、返回值只是它最基础的使用形式,更多应用可以查看官方仓库 github.com/btraceio/bt… 。3、Java Mission ControlJMV最初是JRockit虚拟机提供的一款诊断工具。在Oracle JDK7 Update 40以后,它就绑定在Oracle JDK中发布。JMC位置是%JAVA_HOME%/bin/jmc.exe,打开软件界面:在左侧的“JVM浏览器”面板中自动显示了通过JDP协议(Java Discovery Protocol)找到的本机正在运行的HotSpot虚拟机进程。3.1、MBean服务器点击本地进程的MBean服务器:可以看到,以飞行仪表的视图显示了Java堆使用率,CPU使用率和Live Set+Fragmentation。3.2、飞行记录器(Flight Recorder)飞行记录器是JMC提供的另一大功能,它通过记录程序在一段时间内的运行情况,将记录结果进行分析和展示,可以更进一步对系统的性能进行分析和诊断。要使用JFR,程序启动需要带以下参数:-XX:+UnlockCommercialFeatures -XX:+FlightRecorder连接加了相关参数启动的程序,启动飞行记录,进行一分钟的性能记录:记录结束后,JMC会自动打开刚才的记录:JFR提供的数据质量通常也要比其他工具通过代理形式采样获得或者从MBean中取得的数据高得多。以垃圾搜集为例,HotSpot的MBean中一般有各个分代大小、收集次数、时间、占用率等数据(根据收集器不同有所差别),这些都属于“结果”类的信息,而JFR中还可以看到内存中这段时间分配了哪些对象、哪些在TLAB中(或外部)分配、分配速率 和压力大小如何、分配归属的线程、收集时对象分代晋升的情况等。4、第三方工具以上三个都是JDK自带的性能监控工具,除此之外还有一些第三方的性能监控工具。MATJava 堆内存分析工具。GChistoGC 日志分析工具。GCViewerGC 日志分析工具。JProfiler商用的性能分析利器。arthas阿里开源诊断工具。async-profilerJava 应用性能分析工具,开源、火焰图、跨平台。这里只是简单罗列,就不再展开详细介绍。
0
0
0
浏览量605
开着皮卡写代码

【JVM进阶之路】二:Java内存区域

1、运行时数据区Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:1.1、程序计数器程序计数器(Program Counter Register)也被称为PC寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。每一条 Java虚拟机线程都有自己的程序计数器。在任意时刻,一条 Java 虚拟机线程只会执行一个方法的代码,这个正在被线程执行的方法称为该线程的当前方法如果这个方法不是 native 的,那 PC 寄存器就保存 Java 虚拟机正在执行的字节码指令的地址,如果该方法是 native 的,那 PC 寄存器的值是 undefined。1.2、Java虚拟机栈与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。把Java内存区域可以粗略地划分为堆内存(Heap)和栈内存(Stack),其中,“栈”通常就是指这里讲的虚拟机栈,或者更多的情况下只是指虚拟机栈中局部变量表部分。局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和 double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定 的,在方法运行期间不会改变局部变量表的大小。Java 虚拟机栈可能发生如下异常情况:如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量时,Java 虚拟机将会抛出一个 StackOverflowError 异常。如果 Java 虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。1.3、本地方法栈本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。Java 虚拟机规范允许本地方法栈被实现成固定大小的或者是根据计算动态扩展和收缩的。本地方法栈可能发生如下异常情况:如果线程请求分配的栈容量超过本地方法栈允许的最大容量时,Java 虚拟机将会抛出一个StackOverflowError 异常。如果本地方法栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的本地方法栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。1.4、Java堆对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java里“几乎”所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”(Garbage Collected Heap,)。从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现“新生代”“老年代”“永久代”“Eden空间”“From Survivor空间”“To Survivor空间”等名词,需要注意的是这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个Java虚拟机具体 实现的固有内存布局,更不是《Java虚拟机规范》里对Java堆的进一步细致划分。如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区 (Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。不过无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java 堆细分的目的只是为了更好地回收内存,或者更快地分配内存。根据《Java虚拟机规范》的规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该 被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。1.5、方法区方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。《Java虚拟机规范》对方法区的约束是非常宽松的,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域的确是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。值得一提的是:很多人都更愿意把方法区称呼为“永久代”(Permanent Generation),或将两者混为一谈。本质上这两者并不是等价的,因为仅仅是当时的HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得 HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。但是对于其他虚拟机实现,譬如BEA JRockit、IBM J9等来说,是不存在永久代的概念的。1.6、运行时常量池运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。Java虚拟机对于Class文件每一部分(自然也包括常量池)的格式都有严格规定,如每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、加载和执行,但对于运行时常量池,《Java虚拟机规范》并没有做任何细节的要求,不同提供商实现的虚拟机可以按照自己的需要来实现这个内存区域,不过一般来说,除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量 一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。1.7、直接内存直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据。显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到 本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得 各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError异常。2、JDK的内存区域变迁2.1、jdk1.6/1.7/1.8内存区域变化在上一节提到了,HotSpot虚拟机是是Sun/OracleJDK和OpenJDK中的默认Java虚拟机,是JVM应用最广泛的一种实现。上面提到,Java虚拟机规范对方法区的约束很宽松,而且HotSpot虚拟机在这一区域发生过一些bug,所以HotSpot的方法区经历了一些变迁,我们来看看HotSpot虚拟机内存区域的变迁。JDK1.6时期和我们上面讲的JVM内存区域是一致的:JDK1.7时发生了一些变化,将字符串常量池、静态变量,存放在堆上在JDK1.8时彻底干掉了方法区,而在直接内存中划出一块区域作为元空间,运行时常量池、类常量池都移动到元空间。2.2、为什么替换掉方法区方法区为什么被替代了呢?当然,或者更准确的说法应该是永久代为什么被替换了?——Java虚拟机规范规定的方法区只是换种方式实现。有客观和主观两个原因。客观上使用永久代来实现方法区的决定的设计导致了Java应用更容易遇到内存溢出的问题(永久代有-XX:MaxPermSize的上限,即使不设置也有默认大小,而J9和JRockit只要没有触碰到进程可用内存的上限,例如32位系统中的4GB限制,就不会出问题),而且有极少数方法 (例如String::intern())会因永久代的原因而导致不同虚拟机下有不同的表现。主观上当Oracle收购BEA获得了JRockit的所有权后,准备把JRockit中的优秀功能,譬如Java Mission Control管理工具,移植到HotSpot 虚拟机时,但因为两者对方法区实现的差异而面临诸多困难。考虑到HotSpot未来的发展,在JDK 6的 时候HotSpot开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了,到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了 JDK 8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
1
0
0
浏览量1122
开着皮卡写代码

【JVM进阶之路】一:Java虚拟机概览

1、Java简史Java语言是一门通用的、面向对象的、支持并发的程序语言。全球从事Java相关开发的人员已经数以百万计。从1995年“Java”正式出现以来,Java已经经历了二十几年的发展。Java语言之所以能广受欢迎,其中的原因之一是Java是一门可以跨平台的语言。而跨平台的特性就是通过Java虚拟机(JVM)是实现的。2、JVM简介JVM是整个Java平台的基石。JVM可以看作抽象的计算机。编译器将Java文件编译为Java字节码文件(.class),接下来JVM对字节码文件进行解释,翻译成特定底层平台匹配的机器指令并运行。JVM和Java语言没有必然的联系,它只与class文件格式关联。也就是任何语言,只要能编译成符合规范的字节码文件,都是能被Jvm运行的。也就是说JVM是跨语言的平台。3、Java虚拟机规范我们还要认识到,Java虚拟机是一种规范,它指定了Java虚拟机结构、class文件格式、类加载过程等。我们平时所提到的Java虚拟机一般指的是一种具体的Java虚拟机的实现,例如最知名的hotspot,遵循Java虚拟机规范,甚至可以自己实现Java虚拟机。4、Java虚拟机常见实现4.1、HotSpot VMHotSpot虚拟机是现在应用最广泛的虚拟机,它是Sun/OracleJDK和OpenJDK中的默认Java虚拟机。但是这款虚拟机在最初并非由Sun公司所开发,而是由一家名为“Longview Technologies”的小公司设计;甚至这个虚拟机最初并非是为Java语言而研发的,它来源于Strongtalk虚拟机。Oracle收购Sun以后,建立了HotRockit项目来把原来BEA JRockit中的优秀特性融合到HotSpot之中。到了2014年的JDK 8时期,里面的HotSpot就已是两者融合的结果,HotSpot在这个过程 里移除掉永久代,吸收了JRockit的Java Mission Control监控工具等功能。 得益于Sun/OracleJDK在Java应用中的统治地位,HotSpot理所当然地成为全世界使用最广泛的Java 虚拟机,是虚拟机家族中毫无争议的“武林盟主”。4.2、BEA JRockit/IBM J9 VM历史上除了Sun/Oracle公司以外,也有其他组织、公司开发过虚拟机的实现。除了HotSpot之外,BEA JRockit和IBM J9 VM曾经与HotSpot并称“三大商业Java虚拟机”,它们分别是BEA System公司和 IBM公司开发。除BEA和IBM公司外,其他一些大公司也号称有自己的专属JDK和虚拟机,但是它们要么是通过从Sun/Oracle公司购买版权的方式获得的(如HP、SAP等),要么是基于OpenJDK项目改进而来的 (如阿里巴巴、Twitter等),都并非自己独立开发。5、JDK&JRE&JVMJDK&JRE&JVM三者常常被用来比较。JDK(Java Development Kit Java 开发工具包),JDK 是提供给 Java 开发人员使用的,其中包含了 Java 的开发工具,也包括了 JRE。其中的开发工具包括编译工具(javac.exe) 打包工具(jar.exe)等。JRE(Java Runtime Environment Java 运行环境) 是 JDK 的子集,也就是包括 JRE 所有内容,以及开发应用程序所需的编译器和调试器等工具。JRE 提供了库、Java 虚拟机(JVM)和其他组件,用于运行 Java 编程语言、小程序、应用程序。JVM(Java Virtual Machine Java 虚拟机),JVM 可以理解为是一个虚拟出来的计算机,具备着计算机的基本运算方式,它主要负责把 Java 程序生成的字节码文件。三者关系简图如下:
1
0
0
浏览量36
开着皮卡写代码

【JVM进阶之路】十:JVM调优总结

1、调优原则JVM调优听起来很高大上,但是要认识到,JVM调优应该是Java性能优化的最后一颗子弹。比较认可廖雪峰老师的观点,要认识到JVM调优不是常规手段,性能问题一般第一选择是优化程序,最后的选择才是进行JVM调优。JVM的自动内存管理本来就是为了将开发人员从内存管理的泥潭里拉出来。即使不得不进行JVM调优,也绝对不能拍脑门就去调整参数,一定要全面监控,详细分析性能数据。2、JVM调优的时机不得不考虑进行JVM调优的是那些情况呢?Heap内存(老年代)持续上涨达到设置的最大内存值;Full GC 次数频繁;GC 停顿时间过长(超过1秒);应用出现OutOfMemory 等内存异常;应用中有使用本地缓存且占用大量内存空间;系统吞吐量与响应性能不高或下降。3、JVM调优的目标吞吐量、延迟、内存占用三者类似CAP,构成了一个不可能三角,只能选择其中两个进行调优,不可三者兼得。延迟:GC低停顿和GC低频率;低内存占用;高吞吐量;选择了其中两个,必然会会以牺牲另一个为代价。下面展示了一些JVM调优的量化目标参考实例:Heap 内存使用率 <= 70%;Old generation内存使用率<= 70%;avgpause <= 1秒;Full gc 次数0 或 avg pause interval >= 24小时 ;注意:不同应用的JVM调优量化目标是不一样的。4、JVM调优的步骤一般情况下,JVM调优可通过以下步骤进行:分析系统系统运行情况:分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点;确定JVM调优量化目标;确定JVM调优参数(根据历史JVM参数来调整);依次确定调优内存、延迟、吞吐量等指标;对比观察调优前后的差异;不断的分析和调整,直到找到合适的JVM参数配置;找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。以上操作步骤中,某些步骤是需要多次不断迭代完成的。一般是从满足程序的内存使用需求开始的,之后是时间延迟的要求,最后才是吞吐量的要求,要基于这个步骤来不断优化,每一个步骤都是进行下一步的基础,不可逆行。5、JVM参数下面来看一下JDK的JVM参数。5.1、基本参数参数名称含义默认值-Xms初始堆大小内存的1/64默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.-Xmx最大堆大小内存的1/4默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制-Xmn年轻代大小注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。 整个堆大小=年轻代大小 + 年老代大小 + 持久代大小. 增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8-XX:NewSize设置年轻代大小-XX:MaxNewSize年轻代最大值-XX:PermSize设置持久代(perm gen)初始值内存的1/64JDK1.8以前-XX:MaxPermSize设置持久代最大值内存的1/4JDK1.8以前-Xss每个线程的堆栈大小JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右 一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长) 和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"” -Xss is translated in a VM flag named ThreadStackSize” 一般设置这个值就可以了。-XX:ThreadStackSizeThread Stack Size(0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.]-XX:NewRatio年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)-XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5 Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。-XX:SurvivorRatioEden区与Survivor区的大小比值设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10-XX:LargePageSizeInBytes内存页的大小不可设置过大, 会影响Perm的大小=128m-XX:+UseFastAccessorMethods原始类型的快速优化-XX:+DisableExplicitGC关闭System.gc()这个参数需要严格的测试-XX:+ExplicitGCInvokesConcurrent关闭System.gc()disabledEnables invoking of concurrent GC by using the System.gc() request. This option is disabled by default and can be enabled only together with the -XX:+UseConcMarkSweepGC option.-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses关闭System.gc()disabledEnables invoking of concurrent GC by using the System.gc() request and unloading of classes during the concurrent GC cycle. This option is disabled by default and can be enabled only together with the -XX:+UseConcMarkSweepGC option.-XX:MaxTenuringThreshold垃圾最大年龄如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率 该参数只有在串行GC时才有效.-XX:+AggressiveOpts加快编译-XX:+UseBiasedLocking锁机制的性能改善-Xnoclassgc禁用垃圾回收-XX:SoftRefLRUPolicyMSPerMB每兆堆空闲空间中SoftReference的存活时间1ssoftly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap-XX:PretenureSizeThreshold对象超过多大是直接在旧生代分配0单位字节 新生代采用Parallel Scavenge GC时无效 另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.-XX:TLABWasteTargetPercentTLAB占eden区的百分比1%-XX:+CollectGen0FirstFullGC时是否先YGCfalseJdk7版本的主要参数参数名称含义默认值-XX:PermSize设置持久代Jdk7版本及以前版本-XX:MaxPermSize设置最大持久代Jdk7版本及以前版本Jdk8版本的重要特有参数参数名称含义默认值-XX:MetaspaceSize元空间大小Jdk8版本-XX:MaxMetaspaceSize最大元空间Jdk8版本5.2、并行收集器相关参数参数名称含义默认值-XX:+UseParallelGCFull GC采用parallel MSC (此项待验证)选择垃圾收集器为并行收集器.此配置仅对年轻代有效.即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集.(此项待验证)-XX:+UseParNewGC设置年轻代为并行收集可与CMS收集同时使用 JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值-XX:ParallelGCThreads并行收集器的线程数此值最好配置与处理器数目相等 同样适用于CMS-XX:+UseParallelOldGC年老代垃圾收集方式为并行收集(Parallel Compacting)这个是JAVA 6出现的参数选项-XX:MaxGCPauseMillis每次年轻代垃圾回收的最长时间(最大暂停时间)如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值.-XX:+UseAdaptiveSizePolicy自动选择年轻代区大小和相应的Survivor区比例设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开.-XX:GCTimeRatio设置垃圾回收时间占程序运行时间的百分比公式为1/(1+n)-XX:+ScavengeBeforeFullGCFull GC前调用YGCtrueDo young generation GC prior to a full GC. (Introduced in 1.4.1.)5.3、CMS相关参数参数名称含义默认值-XX:+UseConcMarkSweepGC使用CMS内存收集测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明.所以,此时年轻代大小最好用-Xmn设置.???-XX:+AggressiveHeap试图是使用大量的物理内存 长时间大内存使用的优化,能检查计算资源(内存, 处理器数量) 至少需要256MB内存 大量的CPU/内存, (在1.4.1在4CPU的机器上已经显示有提升)-XX:CMSFullGCsBeforeCompaction多少次后进行内存压缩由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理.-XX:+CMSParallelRemarkEnabled降低标记停顿-XX+UseCMSCompactAtFullCollection在FULL GC的时候, 对年老代的压缩CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。 可能会影响性能,但是可以消除碎片-XX:+UseCMSInitiatingOccupancyOnly使用手动定义初始化定义开始CMS收集禁止hostspot自行触发CMS GC-XX:CMSInitiatingOccupancyFraction=70使用cms作为垃圾回收 使用70%后开始CMS收集92为了保证不出现promotion failed(见下面介绍)错误,该值的设置需要满足以下公式CMSInitiatingOccupancyFraction计算公式-XX:CMSInitiatingPermOccupancyFraction设置Perm Gen使用到达多少比率时触发92-XX:+CMSIncrementalMode设置为增量模式用于单CPU情况-XX:+CMSClassUnloadingEnabled5.4、辅助信息参数名称含义默认值-XX:+PrintGC输出形式: [GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]-XX:+PrintGCDetails输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]-XX:+PrintGCTimeStamps-XX:+PrintGC:PrintGCTimeStamps可与-XX:+PrintGC -XX:+PrintGCDetails混合使用 输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]-XX:+PrintGCApplicationStoppedTime打印垃圾回收期间程序暂停的时间.可与上面混合使用输出形式:Total time for which application threads were stopped: 0.0468229 seconds-XX:+PrintGCApplicationConcurrentTime打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用输出形式:Application time: 0.5291524 seconds-XX:+PrintHeapAtGC打印GC前后的详细堆栈信息-Xloggc:filename把相关日志信息记录到文件以便分析. 与上面几个配合使用-XX:+PrintClassHistogramgarbage collects before printing the histogram.-XX:+PrintTLAB查看TLAB空间的使用情况XX:+PrintTenuringDistribution查看每次minor GC后新的存活周期的阈值Desired survivor size 1048576 bytes, new threshold 7 (max 15) new threshold 7即标识新的存活周期的阈值为7。6、主要工具6.1、JDK工具JDK自带了很多性能监控工具,我们可以用这些工具来监测系统和排查内存性能问题。6.2、Linux 命令行工具进行性能监控和问题排查的时候,常常是结合操作系统本身的命令行工具来进行。命令说明top实时显示正在执行进程的 CPU 使用率、内存使用率以及系统负载等信息vmstat对操作系统的虚拟内存、进程、CPU活动进行监控pidstat监控指定进程的上下文切换iostat监控磁盘IO其它还有一些第三方的监控工具,同样是性能分析和故障排查的利器,如MAT、GChisto、JProfiler、arthas。7、常用调优策略这里还是要提一下,及时确定要进行JVM调优,也不要陷入“知见障”,进行分析之后,发现可以通过优化程序提升性能,仍然首选优化程序。7.1、选择合适的垃圾回收器CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。CPU多核,关注吞吐量 ,那么选择PS+PO组合。CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,那么选择CMS。CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。参数配置: //设置Serial垃圾收集器(新生代) 开启:-XX:+UseSerialGC ​ //设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器 开启 -XX:+UseParallelOldGC ​ //CMS垃圾收集器(老年代) 开启 -XX:+UseConcMarkSweepGC ​ //设置G1垃圾收集器 开启 -XX:+UseG1GC7.2、调整内存大小现象:垃圾收集频率非常频繁。原因:如果内存太小,就会导致频繁的需要进行垃圾收集才能释放出足够的空间来创建新的对象,所以增加堆内存大小的效果是非常显而易见的。注意:如果垃圾收集次数非常频繁,但是每次能回收的对象非常少,那么这个时候并非内存太小,而可能是内存泄露导致对象无法回收,从而造成频繁GC。参数配置: //设置堆初始值 指令1:-Xms2g 指令2:-XX:InitialHeapSize=2048m ​ //设置堆区最大值 指令1:`-Xmx2g` 指令2: -XX:MaxHeapSize=2048m ​ //新生代内存配置 指令1:-Xmn512m 指令2:-XX:MaxNewSize=512m7.3、设置符合预期的停顿时间现象:程序间接性的卡顿原因:如果没有确切的停顿时间设定,垃圾收集器以吞吐量为主,那么垃圾收集时间就会不稳定。注意:不要设置不切实际的停顿时间,单次时间越短也意味着需要更多的GC次数才能回收完原有数量的垃圾.参数配置: //GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间 -XX:MaxGCPauseMillis 7.4、调整内存区域大小比率现象:某一个区域的GC频繁,其他都正常。原因:如果对应区域空间不足,导致需要频繁GC来释放空间,在JVM堆内存无法增加的情况下,可以调整对应区域的大小比率。注意:也许并非空间不足,而是因为内存泄造成内存无法回收。从而导致GC频繁。参数配置: //survivor区和Eden区大小比率 指令:-XX:SurvivorRatio=6 //S区和Eden区占新生代比率为1:6,两个S区2:6 ​ //新生代和老年代的占比 -XX:NewRatio=4 //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=27.5、调整对象升老年代的年龄现象:老年代频繁GC,每次回收的对象很多。原因:如果升代年龄小,新生代的对象很快就进入老年代了,导致老年代对象变多,而这些对象其实在随后的很短时间内就可以回收,这时候可以调整对象的升级代年龄,让对象不那么容易进入老年代解决老年代空间不足频繁GC问题。注意:增加了年龄之后,这些对象在新生代的时间会变长可能导致新生代的GC频率增加,并且频繁复制这些对象新生的GC时间也可能变长。配置参数://进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,默认值7 -XX:InitialTenuringThreshol=7 7.6、调整大对象的标准现象:老年代频繁GC,每次回收的对象很多,而且单个对象的体积都比较大。原因:如果大量的大对象直接分配到老年代,导致老年代容易被填满而造成频繁GC,可设置对象直接进入老年代的标准。注意:这些大对象进入新生代后可能会使新生代的GC频率和时间增加。配置参数: //新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。 -XX:PretenureSizeThreshold=1000000 7.7、调整GC的触发时机现象:CMS,G1 经常 Full GC,程序卡顿严重。原因:G1和CMS 部分GC阶段是并发进行的,业务线程和垃圾收集线程一起工作,也就说明垃圾收集的过程中业务线程会生成新的对象,所以在GC的时候需要预留一部分内存空间来容纳新产生的对象,如果这个时候内存空间不足以容纳新产生的对象,那么JVM就会停止并发收集暂停所有业务线程(STW)来保证垃圾收集的正常运行。这个时候可以调整GC触发的时机(比如在老年代占用60%就触发GC),这样就可以预留足够的空间来让业务线程创建的对象有足够的空间分配。注意:提早触发GC会增加老年代GC的频率。配置参数: //使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小 -XX:CMSInitiatingOccupancyFraction ​ //G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65% -XX:G1MixedGCLiveThresholdPercent=65 7.8、调整 JVM本地内存大小现象:GC的次数、时间和回收的对象都正常,堆内存空间充足,但是报OOM原因: JVM除了堆内存之外还有一块堆外内存,这片内存也叫本地内存,可是这块内存区域不足了并不会主动触发GC,只有在堆内存区域触发的时候顺带会把本地内存回收了,而一旦本地内存分配不足就会直接报OOM异常。注意: 本地内存异常的时候除了上面的现象之外,异常信息可能是OutOfMemoryError:Direct buffer memory。 解决方式除了调整本地内存大小之外,也可以在出现此异常时进行捕获,手动触发GC(System.gc())。配置参数: XX:MaxDirectMemorySize8、JVM调优实例以下是整理自网络的一些JVM调优实例:8.1、网站流量浏览量暴增后,网站反应页面响很慢1、问题推测:在测试环境测速度比较快,但是一到生产就变慢,所以推测可能是因为垃圾收集导致的业务线程停顿。2、定位:为了确认推测的正确性,在线上通过jstat -gc 指令 看到JVM进行GC 次数频率非常高,GC所占用的时间非常长,所以基本推断就是因为GC频率非常高,所以导致业务线程经常停顿,从而造成网页反应很慢。3、解决方案:因为网页访问量很高,所以对象创建速度非常快,导致堆内存容易填满从而频繁GC,所以这里问题在于新生代内存太小,所以这里可以增加JVM内存就行了,所以初步从原来的2G内存增加到16G内存。4、第二个问题:增加内存后的确平常的请求比较快了,但是又出现了另外一个问题,就是不定期的会间断性的卡顿,而且单次卡顿的时间要比之前要长很多。5、问题推测:练习到是之前的优化加大了内存,所以推测可能是因为内存加大了,从而导致单次GC的时间变长从而导致间接性的卡顿。6、定位:还是通过jstat -gc 指令 查看到 的确FGC次数并不是很高,但是花费在FGC上的时间是非常高的,根据GC日志 查看到单次FGC的时间有达到几十秒的。7、解决方案: 因为JVM默认使用的是PS+PO的组合,PS+PO垃圾标记和收集阶段都是STW,所以内存加大了之后,需要进行垃圾回收的时间就变长了,所以这里要想避免单次GC时间过长,所以需要更换并发类的收集器,因为当前的JDK版本为1.7,所以最后选择CMS垃圾收集器,根据之前垃圾收集情况设置了一个预期的停顿的时间,上线后网站再也没有了卡顿问题。8.2、后台导出数据引发的OOM**问题描述:**公司的后台系统,偶发性的引发OOM异常,堆内存溢出。1、因为是偶发性的,所以第一次简单的认为就是堆内存不足导致,所以单方面的加大了堆内存从4G调整到8G。2、但是问题依然没有解决,只能从堆内存信息下手,通过开启了-XX:+HeapDumpOnOutOfMemoryError参数 获得堆内存的dump文件。3、VisualVM 对 堆dump文件进行分析,通过VisualVM查看到占用内存最大的对象是String对象,本来想跟踪着String对象找到其引用的地方,但dump文件太大,跟踪进去的时候总是卡死,而String对象占用比较多也比较正常,最开始也没有认定就是这里的问题,于是就从线程信息里面找突破点。4、通过线程进行分析,先找到了几个正在运行的业务线程,然后逐一跟进业务线程看了下代码,发现有个引起我注意的方法,导出订单信息。5、因为订单信息导出这个方法可能会有几万的数据量,首先要从数据库里面查询出来订单信息,然后把订单信息生成excel,这个过程会产生大量的String对象。6、为了验证自己的猜想,于是准备登录后台去测试下,结果在测试的过程中发现到处订单的按钮前端居然没有做点击后按钮置灰交互事件,结果按钮可以一直点,因为导出订单数据本来就非常慢,使用的人员可能发现点击后很久后页面都没反应,结果就一直点,结果就大量的请求进入到后台,堆内存产生了大量的订单对象和EXCEL对象,而且方法执行非常慢,导致这一段时间内这些对象都无法被回收,所以最终导致内存溢出。7、知道了问题就容易解决了,最终没有调整任何JVM参数,只是在前端的导出订单按钮上加上了置灰状态,等后端响应之后按钮才可以进行点击,然后减少了查询订单信息的非必要字段来减少生成对象的体积,然后问题就解决了。8.3、单个缓存数据过大导致的系统CPU飚高1、系统发布后发现CPU一直飚高到600%,发现这个问题后首先要做的是定位到是哪个应用占用CPU高,通过top 找到了对应的一个java应用占用CPU资源600%。2、如果是应用的CPU飚高,那么基本上可以定位可能是锁资源竞争,或者是频繁GC造成的。3、所以准备首先从GC的情况排查,如果GC正常的话再从线程的角度排查,首先使用jstat -gc PID 指令打印出GC的信息,结果得到得到的GC 统计信息有明显的异常,应用在运行了才几分钟的情况下GC的时间就占用了482秒,那么问这很明显就是频繁GC导致的CPU飚高。4、定位到了是GC的问题,那么下一步就是找到频繁GC的原因了,所以可以从两方面定位了,可能是哪个地方频繁创建对象,或者就是有内存泄露导致内存回收不掉。5、根据这个思路决定把堆内存信息dump下来看一下,使用jmap -dump 指令把堆内存信息dump下来(堆内存空间大的慎用这个指令否则容易导致会影响应用,因为我们的堆内存空间才2G所以也就没考虑这个问题了)。6、把堆内存信息dump下来后,就使用visualVM进行离线分析了,首先从占用内存最多的对象中查找,结果排名第三看到一个业务VO占用堆内存约10%的空间,很明显这个对象是有问题的。7、通过业务对象找到了对应的业务代码,通过代码的分析找到了一个可疑之处,这个业务对象是查看新闻资讯信息生成的对象,由于想提升查询的效率,所以把新闻资讯保存到了redis缓存里面,每次调用资讯接口都是从缓存里面获取。8、把新闻保存到redis缓存里面这个方式是没有问题的,有问题的是新闻的50000多条数据都是保存在一个key里面,这样就导致每次调用查询新闻接口都会从redis里面把50000多条数据都拿出来,再做筛选分页拿出10条返回给前端。50000多条数据也就意味着会产生50000多个对象,每个对象280个字节左右,50000个对象就有13.3M,这就意味着只要查看一次新闻信息就会产生至少13.3M的对象,那么并发请求量只要到10,那么每秒钟都会产生133M的对象,而这种大对象会被直接分配到老年代,这样的话一个2G大小的老年代内存,只需要几秒就会塞满,从而触发GC。9、知道了问题所在后那么就容易解决了,问题是因为单个缓存过大造成的,那么只需要把缓存减小就行了,这里只需要把缓存以页的粒度进行缓存就行了,每个key缓存10条作为返回给前端1页的数据,这样的话每次查询新闻信息只会从缓存拿出10条数据,就避免了此问题的 产生。8.4、CPU经常100% 问题定位问题分析:CPU高一定是某个程序长期占用了CPU资源。1、所以先需要找出那个进行占用CPU高。 top 列出系统各个进程的资源占用情况。2、然后根据找到对应进行里哪个线程占用CPU高。 top -Hp 进程ID 列出对应进程里面的线程占用资源情况3、找到对应线程ID后,再打印出对应线程的堆栈信息printf "%x\n" PID 把线程ID转换为16进制。 jstack PID 打印出进程的所有线程信息,从打印出来的线程信息中找到上一步转换为16进制的线程ID对应的线程信息。4、最后根据线程的堆栈信息定位到具体业务方法,从代码逻辑中找到问题所在。查看是否有线程长时间的watting 或blocked 如果线程长期处于watting状态下, 关注watting on xxxxxx,说明线程在等待这把锁,然后根据锁的地址找到持有锁的线程。8.5、内存飚高问题定位分析: 内存飚高如果是发生在java进程上,一般是因为创建了大量对象所导致,持续飚高说明垃圾回收跟不上对象创建的速度,或者内存泄露导致对象无法回收。1、先观察垃圾回收的情况jstat -gc PID 1000 查看GC次数,时间等信息,每隔一秒打印一次。 jmap -histo PID | head -20 查看堆内存占用空间最大的前20个对象类型,可初步查看是哪个对象占用了内存。如果每次GC次数频繁,而且每次回收的内存空间也正常,那说明是因为对象创建速度快导致内存一直占用很高;如果每次回收的内存非常少,那么很可能是因为内存泄露导致内存一直无法被回收。2、导出堆内存文件快照jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump堆内存信息到文件。3、使用visualVM对dump文件进行离线分析,找到占用内存高的对象,再找到创建该对象的业务代码位置,从代码和业务场景中定位具体问题。8.6、数据分析平台系统频繁 Full GC平台主要对用户在 App 中行为进行定时分析统计,并支持报表导出,使用 CMS GC 算法。数据分析师在使用中发现系统页面打开经常卡顿,通过 jstat 命令发现系统每次 Young GC 后大约有 10% 的存活对象进入老年代。原来是因为 Survivor 区空间设置过小,每次 Young GC 后存活对象在 Survivor 区域放不下,提前进入老年代。通过调大 Survivor 区,使得 Survivor 区可以容纳 Young GC 后存活对象,对象在 Survivor 区经历多次 Young GC 达到年龄阈值才进入老年代。调整之后每次 Young GC 后进入老年代的存活对象稳定运行时仅几百 Kb,Full GC 频率大大降低。8.7、业务对接网关 OOM网关主要消费 Kafka 数据,进行数据处理计算然后转发到另外的 Kafka 队列,系统运行几个小时候出现 OOM,重启系统几个小时之后又 OOM。通过 jmap 导出堆内存,在 eclipse MAT 工具分析才找出原因:代码中将某个业务 Kafka 的 topic 数据进行日志异步打印,该业务数据量较大,大量对象堆积在内存中等待被打印,导致 OOM。8.8、鉴权系统频繁长时间 Full GC系统对外提供各种账号鉴权服务,使用时发现系统经常服务不可用,通过 Zabbix 的监控平台监控发现系统频繁发生长时间 Full GC,且触发时老年代的堆内存通常并没有占满,发现原来是业务代码中调用了 System.gc()。
0
0
1
浏览量9
开着皮卡写代码

【JVM进阶之路】八:性能监控工具-命令行篇

定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。在实际的故障排查、性能监控中,常常是操作系统的工具和Java虚拟机的工具结合使用。1、操作系统工具1.1、top:显示系统整体资源使用情况top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用情况。在Linux使用top命令的部分输出如下:top命令的输出可以分为两个部分:前半部分是系统统计信息,后半部分是进程信息。在统计信息中:第1行是任务队列信息,从左到右依次表示:系统当前时间、系统运行时间、当前登录用户,最后的load average表示系统的平均负载。第2行是进程统计信息,分别有正在运行的进程数、睡眠进程数、停止的进程数、僵尸进程数。第3行是CPU统计信息,us表示用户空间CPU占用率,sy表示内核空间CPU占用率、ni表示用户进程空间改变过优先级的进程cpu的占用率、id表示空闲cpu占用率、wa表示等待输入输出的CPU时间百分比、hi表示硬件中断请求、si表示软件中断请求。在进程信息区中,显示了系统各个进程的资源使用情况。主要字段的含义:PID:进程idUSER:进程所有者的用户名PR:优先级NI:nice值,负值表示高优先级,正值表示低优先级TIME+:进程使用的CPU时间总计,单位1/100秒COMMAND:命令名/命令行1.2、vmstat:监控内存和CPUvmstat也是一款功能比较齐全的性能监测工具。它可以统计CPU、内存使用情况、swap使用情况能信息。一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数,单位是秒,第二个参数是采样的次数,如:以下命令表示每秒采样一次,共三次。输出的各个列的含义:分类说明Procsr: 运行队列中进程数量b: 等待IO的进程数量Memory(内存)swpd: 使用虚拟内存大小free: 可用内存大小buff: 用作缓冲的内存大小cache: 用作缓存的内存大小Swap:si: 每秒从交换区写到内存的大小so: 每秒写入交换区的内存大小IO:(现在的Linux版本块的大小为1024bytes)bi: 每秒读取的块数bo: 每秒写入的块数系统in: 每秒中断数,包括时钟中断cs: 每秒上下文切换数CPU(以百分比表示)us: 用户进程执行时间(user time)sy: 系统进程执行时间(system time)id: 空闲时间(包括IO等待时间),中央处理器的空闲时间 ,以百分比表示。wa: 等待IO时间1.3、iostat:监控IO使用iostat可以提供磁盘IO的监控数据:avg-cpu: %user %nice %system %iowait %steal %idle 1.44 0.00 0.39 0.00 0.00 98.17 Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn sda 0.37 0.47 30.30 3561197 229837730 dm-0 0.44 0.33 29.97 2518272 227313194 dm-1 0.12 0.13 0.33 1013276 2520308 dm-2 0.00 0.00 0.00 502 2068以上命令显示了CPU的使用概况和磁盘I/O的信息。输出结果各个列的含义:iostat结果面板 avg-cpu 描述的是系统cpu使用情况:%user:CPU处在用户模式下的时间百分比。%nice:CPU处在带NICE值的用户模式下的时间百分比。%system:CPU处在系统模式下的时间百分比。%iowait:CPU等待输入输出完成时间的百分比。%steal:管理程序维护另一个虚拟处理器时,虚拟CPU的无意识等待时间百分比。%idle:CPU空闲时间百分比。1.4、netstat:监控网络使用在web程序中,可能运行需要网络,可以使用netstat命令监控网络流量。netstat -a Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 localhost:30037 *:* LISTEN udp 0 0 *:bootpc *:* Active UNIX domain sockets (servers and established) Proto RefCnt Flags Type State I-Node Path unix 2 [ ACC ] STREAM LISTENING 6135 /tmp/.X11-unix/X0 unix 2 [ ACC ] STREAM LISTENING 5140 /var/run/acpid.socket ... 以上命令表示列出所有端口信息。2、JDK性能监控工具除了我们比较熟悉的java.exe、javac.exe这两个命令行工具,在jdk的bin目录下,还有一些其它的工具。。除了编译和运行Java程序外,打包、部署、签名、调试、监控、运维等各种场景都可能会用到它们。2.1、jps:虚拟机进程查看jps类似Linux下的ps,它会列出Java程序的进程。jps命令格式:jps [ options ] [ hostid ] jps命令示例:jps的常用选项见表:选项列表描述-q只输出进程 ID,忽略主类信息-l输出主类全名,或者执行 JAR 包则输出路径-m输出虚拟机进程启动时传递给主类 main()函数的参数-v输出虚拟机进程启动时的 JVM 参数2.2、jstat:虚拟机运行时信息查看jsta是一个强大的工具。它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。jstat命令格式为:jstat [ option vmid [interval[s|ms] [count]] ] 选项option代表用户希望查询的虚拟机信息,主要分为三类:类加载、垃圾收集、运行期编译状况。如下,输出Java进程5728的ClassLoader相关信息,每秒统计一次信息,一共输出两次。下例展示了与GC相关的堆信息的输出:jstat工具主要选项:选项列表描述-class监视类加载、卸载数量、总空间以及类装载所耗费时长-gc监视 Java 堆情况,包括 Eden 区、2 个 Survivor 区、老年代、永久代或者 jdk1.8 元空间等,容量、已用空间、垃圾收集时间合计等信息-gccapacity监视内容与-gc 基本一致,但输出主要关注 Java 堆各个区域使用到的最大、最小空间-gcutil监视内容与-gc 基本相同,但输出主要关注已使用空间占总空间的百分比-gccause与 -gcutil 功能一样,但是会额外输出导致上一次垃圾收集产生的原因-gcnew监视新生代垃圾收集情况-gcnewcapacity监视内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间-gcold监视老年代垃圾收集情况-gcoldcapacity监视内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间-compiler输出即时编译器编译过的方法、耗时等信息-printcompilation输出已经被即时编译的方法2.3、jinfo:虚拟机配置查看jinfo的作用是实时查看和调整虚拟机各项参数。jinfo命令格式:jinfo [ option ] pid下例显示了新生爱对象晋升老年代的最大年龄。在应用程序启动时,没有指定,但通过jinfo,查看该参数的当前数值。查看是否打印GC详细信息:2.4、jmap:内存映像(导出)jmap命令用于生成堆转储快照(一般称为heapdump或dump文件)jmap的作用并不仅仅是为了获取堆转储快照,它还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。jmap命令格式:jmap [ option ] vmid如下,使用jmap生成PID为5728的Java程序的对象统计信息, 并输出到dump.txt中。dump.txt的结构如下:jmap更重要的功能是得到Java程序的当前堆快照:如图,将应用程序的堆快照输出到D盘的heap.hprof文件中,之后,可以通过多种工具分析该堆文件。jmap工具主要选项:选项描述-dump生成 Java 堆转储快照。-finalizerinfo显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象。Linux平台-heap显示 Java 堆详细信息,比如:用了哪种回收器、参数配置、分代情况。Linux 平台-histo显示堆中对象统计信息,包括类、实例数量、合计容量-permstat显示永久代内存状态,jdk1.7,永久代-F当虚拟机进程对 -dump 选项没有响应式,可以强制生成快照。Linux平台2.5、jhat:堆转储快照分析JDK提供jhat命令与jmap搭配使用,来分析jmap生成的堆转储快照。 jhat内置了一个微型的HTTP/Web服务器,生成堆转储快照的分析结果后,可以在浏览器中查看。以前面生成的heap.hprof为例:屏幕显示“Server is ready.”的提示后,用户在浏览器中输入http://localhost:7000/可以看到分析结果2.6、jstack:Java堆栈跟踪jstack命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者 javacore文件)。jstack命令格式:jstack [ option ] vmid 如下,使用stack查看线程堆栈的部分结果:jstack工具主要选项:选项描述-F当正常输出的请求不被响应时,强制输出线程堆栈-l除了堆栈外,显示关于锁的附加信息-m如果调用的是本地方法的话,可以显示 c/c++的堆栈2.7、jcmd:多功能命令在jdk1.7以后,新增了一个请打的命令行工具jcmd,它可以实现上面除了jstat外所有命令的功能。例如,使用jcmd列出当前系统中的所有运行中的Java虚拟机:jmcd命令格式: jcmd <pid | main class> <command ... | PerfCounter.print | -f file>jmcd工具主要选项:选项描述help打印帮助信息,示例:jcmd help []ManagementAgent.stop停止JMX AgentManagementAgent.start_local开启本地JMX AgentManagementAgent.start开启JMX AgentThread.print参数-l打印java.util.concurrent锁信息,相当于:jstackPerfCounter.print相当于:jstat -J-Djstat.showUnsupported=true -snapGC.class_histogram相当于:jmap -histoGC.heap_dump相当于:jmap -dump:format=b,file=xxx.binGC.run_finalization相当于:System.runFinalization()GC.run相当于:System.gc()VM.uptime参数-date打印当前时间,VM启动到现在的时候,以秒为单位显示VM.flags参数-all输出全部,相当于:jinfo -flags , jinfo -flagVM.system_properties相当于:jinfo -syspropsVM.command_line相当于:jinfo -syspropsgrep commandVM.version相当于:jinfo -syspropsgrep version
0
0
2
浏览量621
开着皮卡写代码

【JVM进阶之路】十二:字节码指令

在前面的 【JVM进阶之路】三:探究虚拟机对象 里,提到了对象的初始化过程,对象初始化用的是new指令——这就是字节码指令。在【JVM进阶之路】十一:Class文件结构 中已经学习了JVM 字节码是JVM能直接识别的语言,了解了字节码文件的文件结构。接下来,我们进一步学习字节码的相关指令。首先我们来看一个简单的程序:public class Main { public static void main(String[] args) { int x=3,y=2; int r=x+y; System.out.println(x+y); } }编译运行,使用JDK自带的javap查看字节码:javap -c -s -v -l Main.class我们来找找相加指令在哪里:对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表 float,d代表double,a代表reference。因为Java虚拟机的操作码长度只有一字节,所以包含了数据类型的操作码就为指令集的设计带来了很大的压力:如果每一种与数据类型相关的指令都支持Java虚拟机所有运行时数据类型的话,那么 指令的数量恐怕就会超出一字节所能表示的数量范围了。因此Java字节码指令支持的数据类型的坑位有限,不被支持的智能改头换面用支持的字节码指令来处理。JVM主要支持byte、short、int、long、float、double、char、reference集中数据类型,每种数据类型的操作码分别以不同的字母开头,例如iadd表示int类型的相加指令码:接下来,我们看看不同类型的字节码指令。1、加载和存储指令加载(load)和存储(store)指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输:这类的主要指令有:将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_、dload、 dload_、aload、aload_<n>将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、 fstore_、dstore、dstore_<n>、astore、astore_<n>将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、 iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>扩充局部变量表的访问索引的指令:wide存储数据的操作数栈和局部变量表主要由加载和存储指令进行操作,除此之外,还有少量指令, 如访问对象的字段或数组元素的指令也会向操作数栈传输数据。iload_这一类以尖括号结尾的指令,实际上代表了一组指令,例如iload_,它可能代表了iload_0、iload_1、iload_2和iload_3这几条指令,这几条指令表示把第1、2、3个局部变量加载进操作数栈。2、运算指令算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。大体上运算指令可以分为两种:对整型数据进行运算的指令与对浮点型数据进行运算的指令。所有的算术指令包括:加法指令:iadd、ladd、fadd、dadd减法指令:isub、lsub、fsub、dsub乘法指令:imul、lmul、fmul、dmul除法指令:idiv、ldiv、fdiv、ddiv 这类的主要指令有:求余指令:irem、lrem、frem、drem取反指令:ineg、lneg、fneg、dneg位移指令:ishl、ishr、iushr、lshl、lshr、lushr按位或指令:ior、lor按位与指令:iand、land按位异或指令:ixor、lxor局部变量自增指令:iinc比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp3、类型转换指令类型转换指令可以将两种不同的数值类型相互转换,这些转换操作有两个作用:显示类型操作转换字节码指令不支持的类型转换类型转换指令主要分为两种:1)宽化,小类型向大类型转换,比如 int–>long–>float–>double,对应的指令有:i2l、i2f、i2d、l2f、l2d、f2d。从 int 到 long,或者从 int 到 double,是不会有精度丢失的;从 int、long 到 float,或者 long 到 double 时,可能会发生精度丢失;从 byte、char 和 short 到 int 的宽化类型转换实际上是隐式发生的,这样可以减少字节码指令,毕竟字节码指令只有 256 个,占一个字节。2)窄化,大类型向小类型转换,比如从 int 类型到 byte、short 或者 char,对应的指令有:i2b、i2s、i2c;从 long 到 int,对应的指令有:l2i;从 float 到 int 或者 long,对应的指令有:f2i、f2l;从 double 到 int、long 或者 float,对应的指令有:d2i、d2l、d2f。窄化很可能会发生精度丢失,毕竟是不同的数量级;但 Java 虚拟机并不会因此抛出运行时异常。4、对象创建与访问指令在前面我们已经接触过了对象创建的指令。ava虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素,这些指令包括:创建类实例的指令:new创建数组的指令:newarray、anewarray、multianewarray访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield、getstatic、putstatic把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、 daload、aaload将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、 dastore、aastore取数组长度的指令:arraylength检查类实例类型的指令:instanceof、checkcast5、操作数栈管理指令如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:将操作数栈的栈顶一个或两个元素出栈:pop、pop2复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2将栈最顶端的两个数值互换:swap6、控制转移指令控制转移指令可以让Java虚拟机有条件或无条件地从指定位置指令(而不是控制转移指令)的下一条指令继续执行程序,从概念模型上理解,可以认为控制指令就是在有条件或无条件地修改PC寄存器的值。控制转移指令包括:条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、 if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne复合条件分支:tableswitch、lookupswitch无条件分支:goto、goto_w、jsr、jsr_w、ret在Java虚拟机中有专门的指令集用来处理int和reference类型的条件分支比较操作,为了可以无须明显标识一个数据的值是否null,也有专门的指令用来检测null值。7、方法调用和返回指令方法调用在后面会学到,我们这里只是了解一下方法调用的一些指令:invokevirtual指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派), 这也是Java语言中最常见的方法分派方式。invokeinterface指令:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。invokespecial指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。invokestatic指令:用于调用类静态方法(static方法)。invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法。并执行该方法。前面四条调用指令的分派逻辑都固化在Java虚拟机内部,用户无法改变,而invokedynamic指令的分派逻辑 是由用户所设定的引导方法决定的。方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。8、异常处理指令在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,除了用throw语句显式抛出异常的情况之外,《Java虚拟机规范》还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如当除数为零时,虚拟机会在idiv或ldiv指令中抛出 ArithmeticException异常。而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和ret指令来实现,现在已经不用了),而是采用异常表来完成。9、同步指令Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor,更常见的是直接将它称为“锁”)来实现的。方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持。例如一段代码: void onlyMe(String f) { synchronized (f) { System.out.println(f); } }编译后查看字节码指令: 0: aload_1 1: dup 2: astore_2 3: monitorenter // 以栈顶元素作为锁,开始同步 4: getstatic #2 7: aload_1 8: invokevirtual #3 11: aload_2 12: monitorexit // 退出同步 13: goto 21 16: astore_3 17: aload_2 18: monitorexit 19: aload_3 20: athrow 21: return
0
0
0
浏览量576
开着皮卡写代码

【JVM进阶之路】十四:类加载器和类加载机制

在上一章里,我们已经学习了类加载的过程,我们知道在加载阶段需要”通过一个类的全限定名来获取描述该类的二进制字节流“,而来完成这个工作的就是类加载器(Class Loader)。1、类与类加载器类加载器只用于实现类的加载动作。但对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每 一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。如下演示了不同的类加载器对instanceof关键字运算的结果的影响。public class ClassLoaderTest { public static void main(String[] args) throws Exception { //自定义一个简单的类加载器 ClassLoader myLoader = new ClassLoader() { @Override //加载类方法 public Class<?> loadClass(String name) throws ClassNotFoundException { try { //获取文件名 String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; //加载输入流 InputStream is = getClass().getResourceAsStream(fileName); //使用父类加载 if (is == null) { return super.loadClass(name); } byte[] b = new byte[is.available()]; is.read(b); //从流中转化类的实例 return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException(name); } } }; //使用自己实现的类加载器加载 Object obj = myLoader.loadClass("cn.fighter3.loader.ClassLoaderTest").newInstance(); System.out.println(obj.getClass()); //实例判断 System.out.println(obj instanceof cn.fighter3.loader.ClassLoaderTest); } }运行结果:在代码里定义了一个简单的类加载器,使用这个类加载器去加载cn.fighter3.loader.ClassLoaderTest类并创建实例,去做类型检查的时候,发现结果是false。2、双亲委派模型从Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。站在Java开发人员的角度来看,类加载器就应当划分得更细致一些。自JDK 1.2以来,Java一直保持着三层类加载器、双亲委派的类加载架构。双亲委派模型如上图:启动类加载器(Bootstrap Class Loader):负责加载存放在 <JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,能被Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类。扩展类加载器(Extension Class Loader):负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。应用程序类加载器(Application Class Loader):负责加载用户类路径 (ClassPath)上所有的类库,如果没有自定义类加载器,一般情况下这个加载器就是程序中默认的类加载器。用户还可以加入自定义的类加载器器来进行扩展。双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。为什么要用双亲委派机制呢?答案是为了保证应用程序的稳定有序。例如类java.lang.Object,它存放在rt.jar之中,通过双亲委派机制,保证最终都是委派给处于模型最顶端的启动类加载器进行加载,保证Object的一致。反之,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的 ClassPath中,那系统中就会出现多个不同的Object类。双亲委派模型的代码实现非常简单,在java.lang.ClassLoader.java中有一个 loadClass方法: protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,判断类是否被加载过 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果父类加载器抛出ClassNotFoundException // 说明父类加载器无法完成加载请求 } if (c == null) { // 在父类加载器无法加载时 // 再调用本身的findClass方法来进行类加载 long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }3、破坏双亲委派模型双亲委派机制在历史上主要有三次破坏:第一次破坏双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2面世以前的“远古”时代。由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类 java.lang.ClassLoader则在Java的第一个版本中就已经存在,为了向下兼容旧代码,所以无法以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的 protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在 loadClass()中编写代码。第二次破坏双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,如果有基础类型又要调用回用户的代码,那该怎么办呢?例如我们比较熟悉的JDBC:各个厂商各有不同的JDBC的实现,Java在核心包\lib里定义了对应的SPI,那么这个就毫无疑问由启动类加载器加载器加载。但是各个厂商的实现,是没办法放在核心包里的,只能放在classpath里,只能被应用类加载器加载。那么,问题来了,启动类加载器它就加载不到厂商提供的SPI服务代码。为了解决这个我呢提,引入了一个不太优雅的设计:线程上下文类加载器 (Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为。第三次破坏双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,例如代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为 Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。"简单的事情重复做,重复的事情认真做,认真的事情有创造性地做!"——我是三分恶,可以叫我老三/三分/三哥/三子,一个能文能武的全栈开发,咱们下期见!
0
0
0
浏览量619
开着皮卡写代码

【JVM进阶之路】六:垃圾收集理论和算法

在前面我们了解了虚拟机如何判断对象可回收,接下来我们了解Java虚拟机垃圾收集的一些理论和算法。1、分代收集理论分代收集理论,是基于程序运行对象存活数量和对象年龄之间关系的一套经验法则。它建立在两个分代假说之上:弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。用通俗的话总结:大部分污渍很容易擦干净,多次擦都没擦干净的无责越来越难擦干净。基于这个理论,收集器将Java堆划分出不同的区域,然后将回收对象按照年龄分配到不同的区域存储。具体来讲,就是把Java堆划分为新生代 (Young Generation)和老年代(Old Generation)两个区域,新生代存放存活时间短的对象,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。对于新生代的对象,可以只关注如何保留少量存活而不是去标记那些大量将要被回收的对象;对于老年代,可以降低垃圾收集频率,同时更加关注那些要消亡的对象。为了降低垃圾回收的代价,在新生代和老年代采用了不同的垃圾收集算法。基于分代,产生了一些垃圾收集的类型划分:部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。2、垃圾收集算法2.1、标记-清除算法见名知义,标记-清除(Mark-Sweep)算法分为两个阶段:标记 : 标记出所有需要回收的对象清除:回收所有被标记的对象标记-清除算法比较基础,但是主要存在两个缺点:执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低。内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。标记-清除算法主要用于老年代,因为老年代可回收的对象比较少。2.2、标记-复制算法标记-复制算法解决了标记-清除算法面对大量可回收对象时执行效率低的问题。过程也比较简单:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法存在一个明显的缺点:一部分空间没有使用,存在空间的浪费。新生代垃圾收集主要采用这种算法,因为新生代的存活对象比较少,每次复制的只是少量的存活对象。一般虚拟机的具体实现不会采用1:1的比例划分,以HotSpot为例,HotSpot虚拟机将内存分为一块较大的Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。默认Eden和Survivor的大小比例是8∶1。2.3、标记-整理算法为了降低内存的消耗,引入一种针对性的算法:标记-整理(Mark-Compact)算法。其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。标记-整理算法主要用于老年代,在老年代这种大量对象存活的区域,移动对象是个很大的负担,而且这种对象移动操作必须全程暂停用户应用程序(Stop The World)才能进行。
0
0
0
浏览量631
开着皮卡写代码

【JVM进阶之路】十一:Class文件结构

Java虚拟机和Class文件是Java实现系统无关性的基石。Class文件是JVM实现语言无关性的基石。Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。每一个 Class 文件对应于一个如下所示的 ClassFile 结构体:ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }简单看一下各项的含义:由于 Class 文件结构没有任何分隔符,所以无论是每个数据项的的顺序还是数量,都是严格限定的,哪个字节代表什么含义,长度多少,先后顺序如何,都是不允许改变的。接下来我们来具体学习每项的含义。1、魔数这是基本上每个Java开发人员的第一个Java程序:public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World"); } }我使用的是Idea工具,运行,target目录下会生成对应的class文件,为了查看文件的十六进制信息,我们可以安装一个插件HexView。安装完成之后,选择class文件,右键 HexView,打开后的十六进制如下:第一行中有一串特殊的字符 cafebabe,它就是一个魔数,是 JVM 识别 class 文件的标志,JVM 会在验证阶段检查 class 文件是否以该魔数开头,如果不是则会抛出 ClassFormatError。这段字节很有意思——咖啡宝贝,Java原来不止是咖啡,还是宝贝2、版本号紧跟着魔数后面的四个字节 0000 0031 存储的是 class 文件的版本号:第 5 和第 6 个字节是次版本号(Minor Version),第 7 和第 8 个字节是主版本号(Major Version)。Java的版本号是从 45 开始的,JDK1.1 之后的每个 JDK 大版本发布主版本号向上加1(JDK1.045.3的版本号),高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式未发生变化。0000 0031 对应的十进制为49,是JDK8的内部版本号。3、常量池紧接着主、次版本号之后的是常量池入口。由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不同,这个容量计数是从1而不是0开始的。如图所示,常量池容量为十六进制数0x0022,即十进制的34,这就代表常量池中有33项常量,索引值范围为1~33。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始。常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:被模块导出或者开放的包(Package)类和接口的全限定名(Fully Qualified Name)字段的名称和描述符(Descriptor)方法的名称和描述符方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)这17类常量结构只有一个相同之处,表结构起始的第一位是个u1类型的标志位(tag),代表着当前常量属于哪种常量类型。17种常量类型所代表的具体含义如表所示:类型标志描述CONSTANT_Utf8_info1UTF-8 编码的字符串CONSTANT_Integer_info3整型字面量CONSTANT_Float_info4浮点型字面量CONSTANT_Long_info5长整型型字面量CONSTANT_Double_info6双精度浮点型字面量CONSTANT_Class_info7类或接口的符号引用CONSTANT_String_info8字符串类型字面量CONSTANT_Fieldref_info9字段的符号引用CONSTANT_Methodref_info10类中方法的符号引用CONSTANT_InterfaceMethodref_info11接口中方法的符号引用CONSTANT_NameAndType_info12字段或方法的部分符号引用CONSTANT_MethodHandle_info15表示方法句柄CONSTANT_MethodType_info16表示方法类型CONSTANT_Dynamic_info17表示一个动态计算常量CONSTANT_InvokeDynamic_info18表示一个动态方法调用点CONSTANT_Moudle_info19表示一个模块CONSTANT_Package_info20表示一个模块中开放或者导出的包常量池非常繁琐,17种常量类型各自有着完全独立的数据结构,彼此之间没有什么共性和联系。我们直接看一下常量池中的17种数据类型的结构总表:4、访问标志在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等等。具体的标志位以及标志的含义如表:标志名称标志值含义ACC_PUBLIC0x0001是否为 Public 类型ACC_FINAL0x0010是否被声明为 final,只有类可以设置ACC_SUPER0x0020是否允许使用 invokespecial 字节码指令的新语义ACC_INTERFACE0x0200标志这是一个接口ACC_ABSTRACT0x0400是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假ACC_SYNTHETIC0x1000标志这个类并非由用户代码产生ACC_ANNOTATION0x2000标志这是一个注解ACC_ENUM0x4000标志这是一个枚举access_flags中一共有16个标志位可以使用,当前只定义了其中9个,没有使用到的标志位要求一 律为零。5、类索引、父类索引与接口索引集合这三者为什么放在一起呢?因为这三者用来确定类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了 java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(后的接口顺序从左到右排列在接口索引集合中。6、字段表集合接口索引结束后,接着是字段表(field_info),它用于描述接口或者类中声明的变量——这里的字段(Field)只包括类级变量以及实例级变量,不包括在方法内部声明的局部变量。描述的主要信息包括:  ①、字段的作用域(public,protected,private修饰)  ②、是类级变量还是实例级变量(static修饰)  ③、是否可变(final修饰)  ④、并发可见性(volatile修饰,是否强制从主从读写)  ⑤、是否可序列化(transient修饰)  ⑥、字段数据类型(8种基本数据类型,对象,数组等引用类型)  ⑦、字段名称字段表的结构如下:类型名称数量u2access_flags1u2name_index1u2descriptor_index1u2attributes_count1attribute_infoattributesattributes_countaccess_flags是该字段的的访问标志,它和类中的访问标志很类似,用以描述该字段的权限类型:private、protected、public;并发可见性:volatile;可变性:final;访问标志详情如下图所示:由于Java语法规则的约束,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。7、方法表集合方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项,如表所示:有区别的部分只有方法访问标志 access_flag, 因为volatile关键字和transient关键字不能修饰方法。方法表标志位及其取值如下:8、属性表集合接下来终于到了最后一项:属性表集合。前面提到的Class文件、字段表、方法表都可以携带自己的属性表集合,就是引用的这里。属性表集合中的属性如下所示:与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制宽松一些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。
0
0
0
浏览量577
开着皮卡写代码

【JVM进阶之路】十三:类加载过程

通过前面的学习,我们了解了Class文件的结构,在Class文件中描述的各类信息,最终都需要加载到虚拟机中之后才能被运行和使用。接下来,我们开始学习JVM的类加载。一个类从被加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading),其中验证、准备、解析三个部分统称为连接(Linking)。《Java虚拟机规范》 严格规定了有且只有六种情况必须立即对类进行“初始化”:1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。6)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。这六种场景中的行为称为对一个类型进行主动引用。接下来我们来详细学习Java虚拟机中类加载的全过程,即加载、验证、准备、解析和初始化。1、加载加载是JVM加载的起点,具体什么时候开始加载,《Java虚拟机规范》中并没有进行强制约束,可以交给虚拟机的具体实现来自由把握。在加载过程,JVM要做三件事情:1)通过一个类的全限定名来获取定义此类的二进制字节流。2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了,方法区中的数据存储格式完全由虚拟机实现自行定义,《Java虚拟机规范》未规定此区域的具体数据结构。类型数据妥善安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象, 这个对象将作为程序访问方法区中的类型数据的外部接口。方法区在JDK不同版本的具体实现就不再详细说了。在JDK1.8中,类型数据存储在元空间中。2、验证验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求。验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。文件格式验证第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。需要验证魔数、版本号、常量池常量类型是否支持、指向常量的索引值等等。元数据验证第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求,包括类是否有父类、父类是否继承了final修饰的类、非抽象类是否实现了父类定义的方法、类是否与父类有矛盾等等。字节码验证第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。符号引用验证最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证主要验证类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。3、准备准备阶段是给静态变量分配内存并设置类变量初始值的阶段。在JDK 7及之前,这些变量的内存在方法区(永久代)中分配,在JDK 8及之后,静态变量则会随着Class对象一起存放在Java堆中。4、解析解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。5、初始化类的初始化阶段是类加载过程的最后一个步骤,在这个阶段,会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。准备阶段,变量被赋的是系统要求的零值,在初始化阶段,赋的是代码里编写的值。好了,基本的类加载过程已经了解完了,接下来,我们将学习负责完成加载阶段的类加载器。参考:【1】:《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》
0
0
0
浏览量597
开着皮卡写代码

【JVM进阶之路】五:垃圾回收概述和对象回收判定

1、垃圾收集概述垃圾收集(Garbage Collection,简称GC)简单说,就是要干三件事:哪些内存需要回收?什么时候回收?如何回收?在Java的内存区域中:程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作,所以这几个区域的内存回收是确定的,随着方法结束或者线程结束,内存自然回收。Java堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理。2、回收对象判定现在开始进入垃圾回收的要干的第一件事:哪些内存需要回收?2.1、引用计数算法先来看一种比较古老的方式:引用计数算法(reference counting)。引用计数器的算法是这样的:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。这个方法不是目前的主流判定方式,原因除了需要额外的空间来存储计数器,以及繁琐的更新操作,引用计数法还有一个重大的漏洞,那便是无法处理循环引用对象。举个例子,假设对象 a 与 b 相互引用,除此之外没有其他引用指向 a 或者 b。在这种情况下,a 和 b 实际上已经死了,但由于它们的引用计数器皆不为 0,在引用计数法的心中,这两个对象还活着。因此,这些循环引用对象所占据的空间将不可回收,从而造成了内存泄露。2.2、可达性分析算法目前 Java 虚拟机的主流垃圾回收器采取的是可达性分析算法。这个算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(Gc Root Set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:在虚拟机栈(栈帧中的本地变量表)中引用的对象在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。在本地方法栈中JNI(即通常所说的Native方法)引用的对象。Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。所有被同步锁(synchronized关键字)持有的对象。反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。3、Java中的引用无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和“引用”离不开关系。Java中的引用有四种,分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回 收掉被引用的对象。Object obj =new Object();软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。 Object obj = new Object(); ReferenceQueue queue = new ReferenceQueue(); SoftReference reference = new SoftReference(obj, queue); //强引用对象滞空,保留软引用 obj = null;弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。 Object obj = new Object(); ReferenceQueue queue = new ReferenceQueue(); WeakReference reference = new WeakReference(obj, queue); //强引用对象滞空,保留软引用 obj = null;虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。 Object obj = new Object(); ReferenceQueue queue = new ReferenceQueue(); PhantomReference reference = new PhantomReference(obj, queue); //强引用对象滞空,保留软引用 obj = null;4、不可达!=死亡要注意,及时对象被判定为不可达,也不一定非死不可。举个不恰当的例子,此时的对象就是秋后问斩的死囚,还有伸冤的机会。如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。如果对象在在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己 (this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它就”逃过一劫“;但是如果没有抓住这个机会,那么对象就真的要被回收了。
0
0
0
浏览量1112
开着皮卡写代码

【JVM进阶之路】四:直面内存溢出和内存泄漏

在Java中,和内存相关的问题主要有两种,内存溢出和内存泄漏。内存溢出(Out Of Memory) :就是申请内存时,JVM没有足够的内存空间。通俗说法就是去蹲坑发现坑位满了。内存泄露 (Memory Leak):就是申请了内存,但是没有释放,导致内存空间浪费。通俗说法就是有人占着茅坑不拉屎。1、内存溢出在JVM的几个内存区域中,除了程序计数器外,其他几个运行时区域都有发生内存溢出(OOM)异常的可能。1.1、Java堆溢出Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。我们来看一个代码的例子:/** * VM参数: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError */ public class HeapOOM { static class OOMObject { } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while (true) { list.add(new OOMObject()); } } }接下来,我们来设置一下程序启动时的JVM参数。限制内存大小为20M,不允许扩展,并通过参数-XX:+HeapDumpOnOutOf-MemoryError 让虚拟机Dump出内存堆转储快照。在Idea中设置JVM启动参数如下图:运行一下:Java堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“Java heap space”。 Java堆文件快照文件dump到了java_pid18728.hprof文件。要解决这个内存区域的异常,常规的处理方法是首先通过内存映像分析工具(如JProfiler、Eclipse Memory Analyzer等)对Dump出来的堆转储快照进行分析。看到内存占用信息如下:然后可以查看代码问题如下:常见堆JVM相关参数:-XX:PrintFlagsInitial: 查看所有参数的默认初始值-XX:PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值) -Xms: 初始堆空间内存(默认为物理内存的1/64) -Xmx: 最大堆空间内存(默认为物理内存的1/4) -Xmn: 设置新生代大小(初始值及最大值) -XX:NewRatio: 配置新生代与老年代在堆结构的占比 -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例 -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄(默认15) -XX:+PrintGCDetails:输出详细的GC处理日志 打印GC简要信息:① -XX:+PrintGC ② -verbose:gc -XX:HandlePromotionFailure:是否设置空间分配担保1.2、虚拟机栈和本地方法栈溢出HotSpot虚拟机中将虚拟机栈和本地方法栈合二为一,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss参数来设定。关于虚拟机栈和本地方法栈,有两种异常:如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError异常。1.2.1、StackOverflowErrorHotSpot虚拟机不支持栈的动态扩展,在HotSpot虚拟机中,以下两种情况都会导致StackOverflowError。栈容量过小如下,使用Xss参数减少栈内存容量/** * vm参数:-Xss128k */ public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } } }运行结果:栈帧太大如下,通过一长串变量,来占用局部变量表空间。运行结果:无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候, HotSpot虚拟机抛出的都是StackOverflowError异常。1.2.2、OutOfMemoryError虽然不支持动态扩展栈,但是通过不断建立线程的方式,也可以在HotSpot上产生内存溢出异常。需要注意,这样产生的内存溢出异常和栈空间是否足够并不存在任何直接的关系,主要取决于操作系统本身的内存使用状态。因为操作系统给每个进程的内存时有限的,线程数一多,自然会超过进程的容量。创建线程导致内存溢出异常 :/** * vm参数:-Xss2M */ public class JavaVMStackOOM { private void dontStop() { while (true) { } } public void stackLeakByThread() { while (true) { Thread thread = new Thread(new Runnable() { public void run() { dontStop(); } }); thread.start(); } } public static void main(String[] args) throws Throwable { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } }以上是一段比较有风险的代码,可能会导致系统假死,运行结果如下:1.3、方法区和运行时常量池溢出这里再提一下方法区和运行时常量池的变迁,JDK1.7以后字符串常量池移动到了堆中,JDK1.8在直接内存中划出一块区域元空间来实现方区域。String:intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的 字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,永久代本身内存不限制可能会出现错误:java.lang.OutOfMemoryError: PermGen space1.4、本机直接内存溢出直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致。直接通过反射获取Unsafe实例,通过反射向操作系统申请分配内存:/** * vm参数:-Xmx20M -XX:MaxDirectMemorySize=10M */ public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } } }运行结果:由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况。2、内存泄漏内存回收,简单说就是应该被垃圾回收的对象没有被垃圾回收。在上图中:对象 X 引用对象 Y,X 的生命周期比 Y 的生命周期长,Y生命周期结束的时候,垃圾回收器不会回收对象Y。我们来看几个内存泄漏的例子:静态集合类引起内存泄漏静态集合的生命周期和 JVM 一致,所以静态集合引用的对象不能被释放。public class OOM { static List list = new ArrayList(); public void oomTests(){ Object obj = new Object(); list.add(obj); } }单例模式:和上面的例子原理类似,单例对象在初始化后会以静态变量的方式在 JVM 的整个生命周期中存在。如果单例对象持有外部的引用,那么这个外部对象将不能被 GC 回收,导致内存泄漏。数据连接、IO、Socket等连接创建的连接不再使用时,需要调用 close 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被 GC 回收。 try { Connection conn = null; Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("url", "", ""); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("...."); } catch (Exception e) { }finally { //不关闭连接 } }变量不合理的作用域一个变量的定义作用域大于其使用范围,很可能存在内存泄漏;或不再使用对象没有及时将对象设置为 null,很可能导致内存泄漏的发生。public class Simple { Object object; public void method1(){ object = new Object(); //...其他代码 //由于作用域原因,method1执行完成之后,object 对象所分配的内存不会马上释放 object = null; } }引用了外部类的非静态内部类非静态内部类(或匿名类)的初始化总是需要依赖外部类的实例。默认情况下,每个非静态内部类都包含对其包含类的隐式引用,若在程序中使用这个内部类对象,那么即使在包含类对象超出范围之后,也不会被回收(内部类对象隐式地持有外部类对象的引用,使其成不能被回收)。Hash 值发生改变对象Hash值改变,使用HashMap、HashSet等容器中时候,由于对象修改之后的Hah值和存储进容器时的Hash值不同,会导致无法从容器中单独删除当前对象,造成内存泄露。ThreadLocal 造成的内存泄漏ThreadLocal 可以实现变量的线程隔离,但若使用不当,就可能会引入内存泄漏问题。
0
0
2
浏览量1166
开着皮卡写代码

【JVM进阶之路】七:垃圾收集器盘点

在前面,我们已经了解了JVM的分代收集,知道JVM垃圾收集在新生代主要采用标记-复制算法,在老年代主要采用标记-清除和标记-整理算法。接下来,我们看一看JDK默认虚拟机HotSpot的一些垃圾收集器的实现。1、常见垃圾回收器首先来看一下JDK 11之前全部可用的垃圾收集器。图中列出了七种垃圾收集器,连线表示可以配合使用,所在区域表示它是属于新生代收集器或是老年代收集器。这里还标出了垃圾收集器采用的收集算法,G1收集器比较特殊,整体采用标记-整理算法,局部采用标记-复制算法,后面再细讲。1.1、Serial收集器Serial收集器是最基础、历史最悠久的收集器。如同它的名字(串行),它是一个单线程工作的收集器,使用一个处理器或一条收集线程去完成垃圾收集工作。并且进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束——这就是所谓的“Stop The World”。Serial/Serial Old收集器的运行过程如图:1.2、ParNew收集器ParNew收集器实质上是Serial收集器的多线程并行版本,使用多条线程进行垃圾收集。ParNew收集器的工作过程如图所示:这里值得一提的是Par是Parallel(并行)的缩写,但需要注意的是,这个并行(Parallel)仅仅是描述同一时间多条GC线程协同工作,而不是GC线程和用户线程同时运行。ParNew垃圾收集也是需要Stop The World的。1.3、Parallel Scavenge收集器Parallel Scavenge收集器是一款新生代收集器,基于标记-复制算法实现,也能够并行收集。和ParNew有些类似,但Parallel Scavenge主要关注的是垃圾收集的吞吐量。所谓吞吐量指的是运行用户代码的时间与处理器总消耗时间的比值。这个比例越高,证明垃圾收集占整个程序运行的比例越小。Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:-XX:MaxGCPauseMillis,最大垃圾回收停顿时间。这个参数的原理是空间换时间,收集器会控制新生代的区域大小,从而尽可能保证回收少于这个最大停顿时间。简单的说就是回收的区域越小,那么耗费的时间也越小。 所以这个参数并不是设置得越小越好。设太小的话,新生代空间会太小,从而更频繁的触发GC。-XX:GCTimeRatio,垃圾收集时间与总时间占比。这个是吞吐量的倒数,原理和MaxGCPauseMillis相同。由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”。1.4、Serial Old收集器Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。Serial Old收集器的工作过程如图:1.5、Parallel Old收集器Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。1.6、CMS收集器CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,同样是老年代的收集齐,采用标记-清除算法。CMS收集齐的垃圾收集分为四步:初始标记(CMS initial mark):单线程运行,需要Stop The World,标记GC Roots能直达的对象。并发标记((CMS concurrent mark):无停顿,和用户线程同时运行,从GC Roots直达对象开始遍历整个对象图。重新标记(CMS remark):多线程运行,需要Stop The World,标记并发标记阶段产生对象。并发清除(CMS concurrent sweep):无停顿,和用户线程同时运行,清理掉标记阶段标记的死亡的对象。涉及到了多次标记的过程,这里插入一点三色抽象的知识。三色抽象用来描述对象在垃圾收集过程中的状态。通常白色代表对象未被扫描到,灰色表示对象被扫描到但未被处理,黑色表示对象及其后代已被处理。在CMS的标记和清除过程中就用到了这种抽象,详细的可以查看参考【5】。Concurrent Mark Sweep收集器运行示意图如下:优点:CMS最主要的优点在名字上已经体现出来——并发收集、低停顿。缺点:CMS同样有三个明显的缺点。Mark Sweep算法会导致内存碎片比较多CMS的并发能力比较依赖于CPU资源,并发回收时垃圾收集线程可能会抢占用户线程的资源,导致用户程序性能下降。并发清除阶段,用户线程依然在运行,会产生所谓的理“浮动垃圾”(Floating Garbage),本次垃圾收集无法处理浮动垃圾,必须到下一次垃圾收集才能处理。如果浮动垃圾太多,会触发新的垃圾回收,导致性能降低。1.7、Garbage First收集器Garbage First(简称G1)收集器是垃圾收集器的一个颠覆性的产物,它开创了局部收集的设计思路和基于Region的内存布局形式。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异。以前的收集器分代是划分新生代、老年代、持久代等。G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理。这样就避免了收集整个堆,而是按照若干个Region集进行收集,同时维护一个优先级列表,跟踪各个Region回收的“价值,优先收集价值高的Region。G1收集器的运行过程大致可划分为以下四个步骤:初始标记(initial mark),标记了从GC Root开始直接关联可达的对象。STW(Stop the World)执行。并发标记(concurrent marking),和用户线程并发执行,从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象、最终标记(Remark),STW,标记再并发标记过程中产生的垃圾。筛选回收(Live Data Counting And Evacuation),制定回收计划,选择多个Region 构成回收集,把回收集中Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。需要STW。相比CMS,G1的优点有很多,可以指定最大停顿时间、分Region的内存布局、按收益动态确定回收集。只从内存的角度来看,与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region 之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。2、前沿垃圾回收器2.1、ZGC收集器在JDK 11当中,加入了实验性质的ZGC。它的回收耗时平均不到2毫秒。它是一款低停顿高并发的收集器。与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因。ZGC虽然在JDK 11还处于实验阶段,但由于算法与思想是一个非常大的提升,未来前景相信还是很广阔的。3、垃圾收集器选择3.1、收集器选择权衡垃圾收集器的选择需要权衡的点还是比较多的——例如运行应用的基础设施如何?使用JDK的发行商是什么?等等……这里简单地列一下上面提到的一些收集器的适用场景:Serial :如果应用程序有一个很小的内存空间(大约100 MB)亦或它在没有停顿时间要求的单线程处理器上运行。Parallel:如果优先考虑应用程序的峰值性能,并且没有时间要求要求,或者可以接受1秒或更长的停顿时间。CMS/G1:如果响应时间比吞吐量优先级高,亦或垃圾收集暂停必须保持在大约1秒以内。ZGC:如果响应时间是高优先级的,亦或堆空间比较大。3.1、设置垃圾收集器设置垃圾收集器(组合)的参数如下:新生代老年代JVM 参数IncrementalIncremental-XincgcSerialSerial-XX:+UseSerialGCParallel ScavengeSerial-XX:+UseParallelGC -XX:-UseParallelOldGCParallel NewSerialN/ASerialParallel OldN/AParallel ScavengeParallel Old-XX:+UseParallelGC -XX:+UseParallelOldGCParallel NewParallel OldN/ASerialCMS-XX:-UseParNewGC -XX:+UseConcMarkSweepGCParallel ScavengeCMSN/AParallel NewCMS-XX:+UseParNewGC -XX:+UseConcMarkSweepGCG1-XX:+UseG1GC
0
0
0
浏览量1128
开着皮卡写代码

JVM进阶之路:深入Java虚拟机

逐步引领读者进入Java虚拟机的深奥世界。从Java虚拟机的概览、内存区域、对象模型、内存溢出和泄漏,到垃圾回收的理论、算法、收集器盘点,再到性能监控工具的使用和JVM调优总结,本专栏准确而全面地探讨了Java虚拟机的各个方面。此外,还涵盖了Class文件的结构、字节码指令、类加载过程以及类加载器和机制等重要主题。通过深入的学习和实践,读者将对Java虚拟机的工作原理和性能优化有更全面的理解,为自己的Java应用程序提供更出色的性能和稳定性。
0
0
0
浏览量5215
开着皮卡写代码

RabbitMQ:发布确认模式

1.基本介绍生产者把信道设置成为confirm(确认)模式,一旦信道进入confirm模式,所有在这个信道上面发布的消息都会被指定唯一的一个ID(ID从1开始).一旦消息被投递到所有匹配的队列以后,broker就会发送一个确认给生产者(包含ID),这样使得生产者知道消息已经正确到底目的队列了。如果消息和队列是可持久化的,那么确认消息就会在消息被写入磁盘以后发出,broker回传给生产者的确认消息中delivery-tag包含了确认消息的序列号。2.实现消息可靠传递的三个条件2.1队列持久化生产者发送消息到队列的时候,把durable参数设置为true(表示队列持久化)// 参数1 queue :队列名 // 参数2 durable :是否持久化 // 参数3 exclusive :仅创建者可以使用的私有队列,断开后自动删除 // 参数4 autoDelete : 当所有消费客户端连接断开后,是否自动删除队列 // 参数5 arguments channel.queueDeclare(QUEUE_NAME, true, false, false, null);2.2消息持久化我们需要将消息标记为持久性 - 通过将消息属性(实现基本属性)设置为PERSISTENT_TEXT_PLAIN的值。//交换机名称,队列名称,消息持久化,消息 channel.basicPublish("", "task_queue", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());2.3发布确认队列接收到生产者发送的数据以后,队列把消息保存在磁盘(为了实现持久化),队列会把最终的可靠性传递结果告诉给生产者,这就是发布确认。三种常用的发布确认策略:单个确认发布、批量确认发布、异步确认发布3.发布确认模式RabbitMQ的发布确认模式默认是没有开启的,我们可以通过调用channel.confirmSelect()方法来手动开启发布确认模式。3.1单个确认发布模式单个确认发布模式是一种简单的同步确认发布的方式。也就是说发布一个消息以后,只要确认它被确认发布,才可以继续发布后续的消息。waitForConfirms(long)这一个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认,就会抛出异常。缺点:速度慢,因为如果没有确认消息的话,后面的消息都会被阻塞public class ConfirmMessage { //消息数量 public static final int MSG_CNT=200; public static void main(String[] args) { //调用单个确认发布方法 confirmSingleMessage(); } public static void confirmSingleMessage() { try { //获取信道对象 Channel channel = ConnectUtil.getChannel(); //开启确认发布 channel.confirmSelect(); //声明队列 String queue = UUID.randomUUID().toString(); //队列持久化 channel.queueDeclare(queue, true, false, false, null); //发送消息 long start= System.currentTimeMillis(); for (int i = 0; i < MSG_CNT; i++) { String msg="消息:"+i; //发送消息,消息需要持久化 channel.basicPublish("", queue, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes()); //服务端返回false或者在超时时间内没有返回数据,生产者可以重新发送消息 boolean flag=channel.waitForConfirms(); if (flag){ System.out.println("————————第"+(i+1)+"条消息发送成功————————"); }else { System.out.println("========第"+(i+1)+"条消息发送失败========="); } } //记录结束时间 long end=System.currentTimeMillis(); System.out.println("发布:"+MSG_CNT+"个单独确认消息,耗时:"+(end-start)+"毫秒"); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }3.2批量确认发布模式先发布一批信息然后一起确认可以大大提高吞吐量缺点:当故障发生的时候,我们不知道是哪一个消息出现了问题,我们需要把整个批处理保存在内存中,记录重要的信息后重新发布消息这种方案仍然是同步的方式,会阻塞消息的发布public class ConfirmMessage { //消息数量 public static final int MSG_CNT = 200; public static void main(String[] args) { //调用单个确认发布方法 //confirmSingleMessage();//发布:200个单独确认消息,耗时:192毫秒 confirmBatchMessage(); } public static void confirmSingleMessage() { try { //获取信道对象 Channel channel = ConnectUtil.getChannel(); //开启确认发布 channel.confirmSelect(); //声明队列 String queue = UUID.randomUUID().toString(); //队列持久化 channel.queueDeclare(queue, true, false, false, null); //发送消息 long start = System.currentTimeMillis(); for (int i = 0; i < MSG_CNT; i++) { String msg = "消息:" + i; //发送消息,消息需要持久化 channel.basicPublish("", queue, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes()); //服务端返回false或者在超时时间内没有返回数据,生产者可以重新发送消息 boolean flag = channel.waitForConfirms(); if (flag) { System.out.println("————————第" + (i + 1) + "条消息发送成功————————"); } else { System.out.println("========第" + (i + 1) + "条消息发送失败========="); } } //记录结束时间 long end = System.currentTimeMillis(); System.out.println("发布:" + MSG_CNT + "个单独确认消息,耗时:" + (end - start) + "毫秒"); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void confirmBatchMessage() { try { //获取信道对象 Channel channel = ConnectUtil.getChannel(); //开启确认发布 channel.confirmSelect(); //批量确认消息数量 int batchSize=20; //未确认消息数量 int nackMessageCount=0; //声明队列 String queue = UUID.randomUUID().toString(); //队列持久化 channel.queueDeclare(queue, true, false, false, null); //发送消息 long start = System.currentTimeMillis(); for (int i = 0; i < MSG_CNT; i++) { String msg = "消息:" + i; //发送消息,消息需要持久化 channel.basicPublish("", queue, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes()); //累加未确认的发布数量 nackMessageCount++; //判断的未确认消息数量和批量确认消息的数量是否一致 if (nackMessageCount==batchSize){ //服务端返回false或者在超时时间内没有返回数据,生产者可以重新发送消息 boolean flag = channel.waitForConfirms(); if (flag) { System.out.println("————————第" + (i + 1) + "条消息发送成功————————"); } else { System.out.println("========第" + (i + 1) + "条消息发送失败========="); } //清空未确认发布消息个数 nackMessageCount=0; } } //为了确认剩下的是没有确认的消息,所以要再次进行确认 if (nackMessageCount>0){ //再次重新确认 channel.waitForConfirms(); } //记录结束时间 long end = System.currentTimeMillis(); System.out.println("发布:" + MSG_CNT + "个单独确认消息,耗时:" + (end - start) + "毫秒"); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }3.3异步确认发布模式 //异步消息发布确认 public static void publishMessageAsync() throws Exception { Channel channel = ConnectUtil.getChannel(); //声明队列,此处使用UUID作为队列的名字 String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName, false, false, false, null); //开启发布确认模式 channel.confirmSelect(); //创建ConcurrentSkipListMap集合(跳表集合) ConcurrentSkipListMap<Long, String> concurrentSkipListMap = new ConcurrentSkipListMap<>(); //确认收到消息回调函数 ConfirmCallback ackCallBack = new ConfirmCallback() { @Override public void handle(long deliveryTag, boolean multiple) throws IOException { //判断是否批量异步确认 if (multiple) { //把集合中没有被确认的消息添加到该集合中 ConcurrentNavigableMap<Long, String> confirmed = concurrentSkipListMap.headMap(deliveryTag, true); //清除该部分没有被确认的消息 confirmed.clear(); } else { //只清除当前序列胡的消息 concurrentSkipListMap.remove(deliveryTag); } System.out.println("确认的消息序列序号:" + deliveryTag); } }; //未被确认消息的回调函数 ConfirmCallback nackCallBack = new ConfirmCallback() { @Override public void handle(long deliveryTag, boolean multiple) throws IOException { //获取没有被确认的消息 String msg = concurrentSkipListMap.get(deliveryTag); System.out.println("发布的消息:" + msg + "未被确认,该消息序列号:" + deliveryTag); } }; //添加异步确认监听器 channel.addConfirmListener(ackCallBack, nackCallBack); //记录开始时间 long start = System.currentTimeMillis(); //循环发送消息 for (int i = 0; i < MSG_CNT; i++) { //消息内容 String message = "消息:" + i; //把未确认的消息放到集合中,通过序列号和消息进行关联 // channel.getNextPublishSeqNo(); 获取下一个消息的序列号 concurrentSkipListMap.put(channel.getNextPublishSeqNo(), message); //发送消息 channel.basicPublish("", queueName, null, message.getBytes()); } //记录结束时间 long end = System.currentTimeMillis(); System.out.println("发布"+MSG_CNT+"个批量确认消息,一共耗时:"+(end-start)+"毫秒"); }
0
0
0
浏览量2015
开着皮卡写代码

一文学会 CentOS7 安装配置 Redis

本篇文章编写的初衷,是将本人在 CentOS7 系统下安装配置 Redis 时正确操作步骤分享给需要的博友!经过一系列操作后,完成了 Redis 开机自启的效果。FinalShell 工具的介绍和使用  在 VMware 虚拟机中安装的 CentOS7 系统,通过 FinalShell 工具远程连接 CentOS7 系统,实现同步操作 CenOS7 系统。使用 FinalShell 的目的是将下载好的 redis-6.2.11.tar.gz 安装包上传至 CenOS7 系统中,也是为了便于相关指令的操作,因为虚拟机中安装的 CenOS7 系统的操作界面显示区域太小,不便操作。FinalShell 下载:https://www.aliyundrive.com/s/Ek3MWSJdizxredis-6.2.11.tar.gz 下载:https://www.aliyundrive.com/s/wb9ixVW4Bcd关于redis的安装版本,我们也可以通过下面网址去选择自己需要的版本:http://download.redis.io/releases/FinalShell 使用:通过 FinalShell 工具远程操作 CenOS7 系统,打开 CenOS7 系统,通过 id addr 指令找到本机的IP地址,如下图:然后打开 FinalShell 软件按照如下步骤连接 CenOS7 系统:第一步:打开下图文件夹图标;第二步:点击左上红框的添加标志,选择SSH连接;第三步:按照下图所示填写相关信息,点击确认即可。第四步:确认后,双击 redis-csdn,进入下图界面则连接成功!CentOS7 安装 Redis 步骤1、安装 gcc 依赖由于 redis 是用 C 语言开发,安装之前输入命令 gcc -v 先确认是否安装 gcc 环境,如果没有安装,执行以下命令进行安装: yum install -y gcc 运行执行结果如下:2、上传并解压 redis 安装包点击 FinalShell 的上传按钮,选择存在 Windows 本机中的Redis安装包进行上传,操作步骤如下图:输入 ls 命令,查看是否上传成功,如下图:如下图,输入下面指令,进行解压tar -zxvf redis-6.2.11.tar.gz3、进入 redis 解压目录通过下面命令切换至 redis 解压目录cd redis-6.2.11进入到 redis-6.2.11 目录后,输入make命令进行编译,编译完成后如下图:4、安装并指定安装目录输入下面命令,执行结果如下图:make install PREFIX=/usr/local/redis5、启动服务5.1、前台启动输入下面命令,进入到安装目录下的bin目录中:cd /usr/local/redis/bin/5.2、后台启动从 redis 解压的源码目录中复制 redis.conf 文件 到 redis 的安装目录【1】输入下面命令进入redis的解压目录,再输入 ls 命令查看当前目录中的文件,确认有 redis.conf 文件,再进行下一步;cd redis-6.2.11【2】在 redis 的解压目录中,输入下面指令,将 redis.conf 文件 复制到 redis 的安装目录中;cp redis.conf /usr/local/redis/bin/复制完成后,输入下面命令切换到安装目录下的 bin 目录下cd /usr/local/redis/bin/【3】进入 /usr/local/redis/bin/ 目录下后,先安装 vim 编辑器,再修改 redis.conf 文件配置;可以提前安装一个vim编辑器,这样在打开redis.conf时,对于一些要配置的参数有高亮显示的效果,便于我们更快捷的找到要修改配置的参数;输入下面命令进行安装:yum -y install vimvim安装完成后,输入下面命令,进入 redis.conf 文件中,进行相关配置的修改;vim redis.conf输入 i 进入编辑模式,通过键盘的上下左右键进行相关操作:第一个找到 ,将其改为 ;此步骤是开放外部访问。第二个找到 ,将其改为 ; 此步骤是允许后台运行。这两个配置修改好之后,按 ESC 退出编辑模式;在按 Shift+:组合键,输入wq,敲回车键保存退出即可;最后输入下面命令完成后台启动设置:./redis-server redis.conf6、开机自启设置【1】添加开机启动服务,输入下面命令,进入 redis.service 文件进行编辑:vi /etc/systemd/system/redis.service复制粘贴以下内容:[Unit] Description=redis-server After=network.target [Service] Type=forking ExecStart=/usr/local/redis/bin/redis-server /usr/local/redis/bin/redis.conf PrivateTmp=true [Install] WantedBy=multi-user.target注意:ExecStart配置成自己的路径【2】分别输入下面命令,设置开机启动systemctl daemon-reload systemctl start redis.service systemctl enable redis.service三个命令输入完成后,执行结果如下图:【3】输入下面命令,创建 redis 命令软链接首先输入 cd 返回根目录,如下图:然后输入下面命令,创建软链接ln -s /usr/local/redis/bin/redis-cli /usr/bin/redis7、重启系统,测试Redis如下图,输入 reboot 重启系统,然后等待开机启动后,刷新 FinalShell 工具,重新连接成功,输入 redis 测试是否设置成功,下图显示已设置成功!其它相关操作1、服务操作命令systemctl start redis.service #启动redis服务systemctl stop redis.service #停止redis服务systemctl restart redis.service #重新启动服务systemctl status redis.service #查看服务当前状态systemctl enable redis.service #设置开机自启动systemctl disable redis.service #停止开机自启动2、防火墙操作命令查看防火墙状态:命令:systemctl status firewalld启动防火墙:命令:systemctl start firewalld停止防火墙:命令:systemctl stop firewalld重启防火墙:命令:systemctl restart firewalld设置防火墙开机自启动:命令:systemctl enable firewalld关闭防火墙开机自启动:命令:systemctl disable firewalld开放指定端口:命令:firewall-cmd --zone=public --add-port=端口号/tcp --permanent说明:需要将“端口号”替换为具体的端口号。删除开放的端口:命令:firewall-cmd --zone=public --remove-port=端口号/tcp --permanent说明:需要将“端口号”替换为具体的端口号。查看已开放的端口:命令:firewall-cmd --zone=public --list-ports开放指定服务:命令:firewall-cmd --zone=public --add-service=服务名称 --permanent说明:需要将“服务名称”替换为具体的服务名称,如http、https、ftp等。删除开放的服务:命令:firewall-cmd --zone=public --remove-service=服务名称 --permanent说明:需要将“服务名称”替换为具体的服务名称,如http、https、ftp等。查看已开放的服务:命令:firewall-cmd --zone=public --list-services查看防火墙规则:命令:firewall-cmd --list-all
0
0
0
浏览量128
开着皮卡写代码

RabbitMQ:工作队列模式

1.基本介绍工作队列(又名:任务队列)背后的主要思想是避免立即执行资源密集型任务并等待其完成。相反,我们将任务安排在以后完成。我们将_任务_封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当您运行多个工作线程时,任务将在它们之间共享。这个概念在 Web 应用程序中特别有用,因为在 Web 应用程序中,不可能在较短的 HTTP 请求窗口中处理复杂的任务。在Work Queues工作队列模式中,我们不需要设置交换机(会使用默认的交换机进行消息转换),但是我们需要指定唯一的消息队列来进行消息传递,可以有多个消费者。多个消费者通过轮询的方式来依次接收消息队列中存储的消息,一旦消息被某个消费者接收了,消息队列就会把消息移除,其他消费者就不能接收这条消息了。消费者必须要等消费完一条消息后才可以准备接收下一条消息。对于任务过重或者任务比较多的情况,使用工作队列可以提高任务处理速度。2.轮询发送消息1.如果一个队列中有多个消费者,那么消费者之间对于同一个消息是竞争关系2.对于任务过重或者任务比较多的情况,使用工作队列可以提高任务处理速度,比如发送短信,我们可以部署多个短信服务,只要有一个节点发送成功即可。2.1抽取工具类public class ConnectUtil { public static Connection getConnection() throws IOException, TimeoutException { //1.创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //设置连接参数 //服务器IP地址 factory.setHost("192.168.88.133"); //连接端口 factory.setPort(5672); //设置连接的虚拟机名称 factory.setVirtualHost("/myhost"); //用户名 factory.setUsername("admin"); //密码 factory.setPassword("123456"); //2.创建Connection对象 Connection connection = factory.newConnection(); return connection; } /** * 创建信道对象 * @return * @throws IOException * @throws TimeoutException */ public static Channel getChannel() throws IOException, TimeoutException { Connection connection = getConnection(); Channel channel = connection.createChannel(); return channel; } }2.2 生产者public class Producer { static final String QUEUE_NAME="work_queue"; public static void main(String[] args) { try { Channel channel = ConnectUtil.getChannel(); //声明队列(队列名称,是否持久化,是否独占连接,是否在不适用队列的时候自动删除,队列其他参数) channel.queueDeclare(QUEUE_NAME, true, false, false, null); //发送消息 for (int i = 1; i <= 10; i++) { String msg="hello rabbitmq!"+i; /** * 参数1:交换机名称,不填写交换机名称的话则使用默认的交换机 * 参数2:队列名称(路由key) * 参数3:其他参数 * 参数4:消息内容 */ channel.basicPublish("", QUEUE_NAME, null,msg.getBytes() ); } System.out.println("消息已经发送完毕"); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }2.3消费者消费者1public class Consumer1 { static final String QUEUE_NAME = "work_queue"; public static void main(String[] args) { try { Channel channel = ConnectUtil.getChannel(); //声明队列(队列名称,是否持久化,是否独占连接,是否在不适用队列的时候自动删除,队列其他参数) channel.queueDeclare(QUEUE_NAME, true, false, false, null); //接受消息 DefaultConsumer consumer = new DefaultConsumer(channel) { /** * 消费回调函数,当收到消息以后,会自动执行这个方法 * @param consumerTag 消费者标识 * @param envelope 消息包的内容(比如交换机,路由key,消息id等) * @param properties 属性信息 * @param body 消息数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消息者1接受到的消息:" + new String(body, "UTF-8")); } }; //监听消息(队列名称,是否自动确认消息,消费对象) channel.basicConsume(QUEUE_NAME, true, consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }消费者2public class Consumer2 { static final String QUEUE_NAME = "work_queue"; public static void main(String[] args) { try { Channel channel = ConnectUtil.getChannel(); //声明队列(队列名称,是否持久化,是否独占连接,是否在不适用队列的时候自动删除,队列其他参数) channel.queueDeclare(QUEUE_NAME, true, false, false, null); //接受消息 DefaultConsumer consumer = new DefaultConsumer(channel) { /** * 消费回调函数,当收到消息以后,会自动执行这个方法 * @param consumerTag 消费者标识 * @param envelope 消息包的内容(比如交换机,路由key,消息id等) * @param properties 属性信息 * @param body 消息数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消息者2接受到的消息:" + new String(body, "UTF-8")); } }; //监听消息(队列名称,是否自动确认消息,消费对象) channel.basicConsume(QUEUE_NAME, true, consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }2.4测试为了方便测试,我们需要先启动消费者,然后再启动生产者,不然生产者发送的消息会瞬间被某个消费者消费完3.消息应答3.1消息应答基本介绍我们知道消费者完成一个任务是需要一定的时间的,如果消费者在处理一个长任务的时候,当它只处理一部分但是此时消费者却挂掉了,可能会出现下面的情况:如果说RabbitMQ向消费者传递一条消息以后,不管消费者有没有处理完或者有没有接收到,就马上把消息标记为删除,那么,如果这个时候消费者挂掉了,就会导致丢失当前正在处理的消息,以及后续发送给消费者的消息,因为消费者不能接收到。为了保证消息在发送过程中不会丢失,RabbitMQ引入了消息应答机制,消息应答就是消费者在接收到消息并且处理该消息以后,告诉RabbitMQ它已经处理了,RabbitMQ就可以把这个消息从消息 队列中删除了。3.2消息自动应答消息发送后就马上认为已经传递成功了,这种模式需要在高吞吐量和数据传输安全性方面做权衡。因为如果使用这种模式,如果消息在被接收到之前,消费者那么出现连接或者信道关闭,那么消息就会丢失;不过,对于这种模式来说,消费者那里可以传递过载的消息,没有对传递的消息数量进行限制,这样就可能使得消费者这边因为接收了太多还来不及处理的消息,导致消息积压,最后使得内存耗尽,导致这些消费者线程被操作系统杀死,所以这种模式仅仅适用消费者可以高效并且以某种苏联能够处理这些消息的情况下使用。信息过载:是指社会信息超过了个人或系统所能接受、处理或有效利用的范围,并导致故障的状况。3.3消息手动应答消费者从队列中消费消息,默认采用的是自动应答,自动应答可能导致消息没有完全消费而导致消息失效问题,所以我们要想实现消息消费过程中不丢失,需要把自动应答改为手动应答。而且,使用手动应答可以批量应答减少网络拥堵,下面三个方法可以用于手动应答消息:Channel。basicAck():用于肯定确认,RabbitMQ已经知道消息被消费并且成功处理消息,可以把消息丢弃。Channle.basicNack():用于否定确认Channel.basicReject():用于否定确认,不处理该消息直接拒绝,然后把消息丢弃3.4批量确认(Multiple)批量确认的方法是channel.basicAck(deliverTag,true),参数2标识是否批量确认。如果为true,表示批量确认队列中没有应答的消息。比如channel中传送tag的消息5,6,7,8,当前tag为8,如果参数2为true,那么此时5-8这些还没有被应答的消息都会被确认收到消息应答。如果为false,那么只会应答tag=8的消息。5,6,7这三个消息仍然不会被确认收到消息应答3.5消息自动重新入队如果一个消费者死了(它的通道被关闭,连接被关闭,或者TCP连接丢失)而没有发送一个ack,RabbitMQ就会明白一条消息没有被完全处理,并会重新排队。如果同时有其他消费者在线,它将迅速将其重新交付给另一个消费者。通过这种方式,您可以确保即使消费者偶尔死亡,也不会丢失任何消息。3.6消息手动应答代码生产者public class Producer2 { static final String QUEUE_NAME="ack_work_queue"; public static void main(String[] args) { try { Channel channel = ConnectUtil.getChannel(); //声明队列(队列名称,是否持久化,是否独占连接,是否在不适用队列的时候自动删除,队列其他参数) channel.queueDeclare(QUEUE_NAME, true, false, false, null); //发送消息 for (int i = 1; i <= 10; i++) { String msg="你好,小兔子!"+i; /** * 参数1:交换机名称,不填写交换机名称的话则使用默认的交换机 * 参数2:队列名称(路由key) * 参数3:其他参数 * 参数4:消息内容 */ channel.basicPublish("", QUEUE_NAME, null,msg.getBytes() ); } System.out.println("消息已经发送完毕"); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }消费者1public class Consumer3 { static final String QUEUE_NAME = "ack_work_queue"; public static void main(String[] args) { try { Channel channel = ConnectUtil.getChannel(); //声明队列(队列名称,是否持久化,是否独占连接,是否在不适用队列的时候自动删除,队列其他参数) channel.queueDeclare(QUEUE_NAME, true, false, false, null); System.out.println("消费者1-消费消息的时间比较短。"); //接受消息 DefaultConsumer consumer = new DefaultConsumer(channel) { /** * 消费回调函数,当收到消息以后,会自动执行这个方法 * @param consumerTag 消费者标识 * @param envelope 消息包的内容(比如交换机,路由key,消息id等) * @param properties 属性信息 * @param body 消息数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { //睡眠一秒 SleepUtil.sleep(1); System.out.println("消息者1接受到的消息:" + new String(body, "UTF-8")); //手动确认 //每条消息都有对应的id,表明是第几条消息,false表示不批量 channel.basicAck(envelope.getDeliveryTag(), false); } }; //监听消息(队列名称,是否自动确认消息,消费对象) channel.basicConsume(QUEUE_NAME, false, consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }消费者2public class Consumer4 { static final String QUEUE_NAME = "ack_work_queue"; public static void main(String[] args) { try { Channel channel = ConnectUtil.getChannel(); //声明队列(队列名称,是否持久化,是否独占连接,是否在不适用队列的时候自动删除,队列其他参数) channel.queueDeclare(QUEUE_NAME, true, false, false, null); System.out.println("消费者2-消费消息的时间比较长。"); //接受消息 DefaultConsumer consumer = new DefaultConsumer(channel) { /** * 消费回调函数,当收到消息以后,会自动执行这个方法 * @param consumerTag 消费者标识 * @param envelope 消息包的内容(比如交换机,路由key,消息id等) * @param properties 属性信息 * @param body 消息数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { //睡眠一秒 SleepUtil.sleep(50); System.out.println("消息者2接受到的消息:" + new String(body, "UTF-8")); //手动确认 //每条消息都有对应的id,表明是第几条消息,false表示不批量 channel.basicAck(envelope.getDeliveryTag(), false); } }; //监听消息(队列名称,是否自动确认消息,消费对象) channel.basicConsume(QUEUE_NAME, false, consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }3.7消息手动应答效果第一次测试,两个消费者都睡眠1秒第二次测试,让消费者2睡眠30秒,然后观察两个消费者的消费情况,接着把消费者2停掉,再次观察消费者1控制台打印的消息,发现队列中没有被消费的消息重新进入到队列中,并且被消费者1进行消费4.消息的持久化我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是,如果 RabbitMQ 服务器停止,我们的任务仍将丢失。当 RabbitMQ 退出或崩溃时,它会忘记队列和消息,除非您告诉它不要这样做。要确保消息不会丢失,需要做两件事:我们需要将队列和消息都标记为持久。首先,我们需要确保队列在 RabbitMQ 节点重新启动后仍能存活下来。为此,我们需要将其声明为_持久:_如果我们之前创建的队列是非持久化的,如果RabbitMQ重启的话,该队列就会被删除掉,如果要队列实现持久化需要在声明队列的时候把durable参数设置为持久化;4.1队列持久化**如果之前创建队列的时候,没有设置成持久化,我们需要把原来的队列先删除掉,或者说重新创建一个新的持久化队列,不然会报错。因为RabbitMQ 不允许我们使用不同的参数重新定义现有队列,并且会向任何尝试执行此操作的程序返回错误。但是有一个快速的解决方法 - 让我们声明一个具有不同名称的队列, **// 参数1 queue :队列名 // 参数2 durable :是否持久化 // 参数3 exclusive :仅创建者可以使用的私有队列,断开后自动删除 // 参数4 autoDelete : 当所有消费客户端连接断开后,是否自动删除队列 // 参数5 arguments channel.queueDeclare(QUEUE_NAME, true, false, false, null);4.2消息持久化我们需要将消息标记为持久性 - 通过将消息属性(实现基本属性)设置为PERSISTENT_TEXT_PLAIN的值。//交换机名称,队列名称,消息持久化,消息 channel.basicPublish("", "task_queue", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());将消息标记为持久性并不能完全保证消息不会丢失。尽管它告诉 RabbitMQ 将消息保存到磁盘,但当 RabbitMQ 接受消息但尚未保存消息时,仍有一个较短的时间窗口。另外, RabbitMQ 不会对每条消息都执行 fsync( fsync函数同步内存中所有已修改的文件数据到储存设备。) – 它可能只是保存到缓存中,而不是真正写入磁盘。持久性保证并不强,但对于我们的简单任务队列来说已经足够了。如果您需要更强的保证,则可以使用发布者确认模式。4.3公平调度int prefetchCount = 1; channel.basicQos(prefetchCount);
0
0
0
浏览量2012
开着皮卡写代码

RabbitMQ:死信队列

1.死信队列1.1死信队列基本介绍队列中不能被消费的消息称为死信队列有时候因为特殊原因,可能导致队列中的某些信息无法被消费,而队列中这些不能被消费的消息在后期没有进行处理,就会变成死信队列,死信队列中的消息称为死信。应用场景:未来保证订单业务的消息数据不丢失,我们需要使用到RabbitMQ的死信队列机制,当消息消费发生异常的时候,我们就把消息投入到死信队列中,比如说用户买东西,下单成功后去支付,但是没有在指定时间支付的时候就会自动失效。死信队列,英文缩写:DLX 。DeadLetter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。当消息在一个队列中变成死信后,它能被重新发布到另一个Exchange中,这个Exchange就是DLX1.2消息成为死信的三种情况队列消息数量到达限制;比如队列最大只能存储10条消息,而发了11条消息,根据先进先出,最先发的消息会进入死信队列。消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false。原队列存在消息过期设置,消息到达超时时间未被消费。1.3死信队列结构图通常情况下,消费者是能正常消费消息的,但是出现上面说的三种情况之一,就无法正常消费信息,消息就会进入死信交换机,死信交换机会和死信队列进行绑定,最后由其他消费者来消费死信消息。1.4死信的处理方式死信的产生既然不可避免,那么就需要从实际的业务角度和场景出发,对这些死信进行后续的处理,常见的处理方式大致有下面几种,① 丢弃,如果不是很重要,可以选择丢弃② 记录死信入库,然后做后续的业务分析或处理③ 通过死信队列,由负责监听死信的应用程序进行处理综合来看,更常用的做法是第三种,即通过死信队列,将产生的死信通过程序的配置路由到指定的死信队列,然后应用监听死信队列,对接收到的死信做后续的处理。队列绑定死信交换机:给队列设置参数:x-dead-letter-exchange 和x-dead-letter-routing-key2.TTL消息过期时间2.1基本介绍当消息到达存活时间后,还没有被消费,就会被自动清除。RabbitMQ可以对消息或者队列设置过期时间,队列中的消息过期是成为死信队列的三种原因之一。2.2生产者public class Producer { //正常交换机 public static final String NORMAL_EXCHANGE = "normal_exchange"; //正常队列 public static final String NORMAL_QUEUE = "normal_queue"; public static void main(String[] args) { try { Channel channel = ConnectUtil.getChannel(); //声明交换机 channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT); //声明队列 //channel.queueDeclare(NORMAL_QUEUE, true, false, false, null); //把正常交换机和正常队列进行绑定 //channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "tom"); //设置过期时间 AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build(); //发送消息 for (int i = 0; i < 10; i++) { String message = "消息:" + i; //发送消息 channel.basicPublish(NORMAL_EXCHANGE, "tom", null, message.getBytes()); } } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }2.3消费者1public class Consumer1 { //定义交换机(正常交换机,死信交换机) public static final String NORMAL_EXCHANGE = "normal_exchange"; public static final String DEAD_EXCHANGE = "dead_exchange"; //定义队列(正常队列,死信队列) public static final String NORMAL_QUEUE = "normal_queue"; public static final String DEAD_QUEUE = "dead_queue"; public static void main(String[] args) { try { //创建信道对象 Channel channel = ConnectUtil.getChannel(); //声明交换机 channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT); channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT); //设置正常队列和死信队列进行绑定,key固定不可以改变 Map<String, Object> map = new HashMap<>(); map.put("x-dead-letter-exchange",DEAD_EXCHANGE); map.put("x-dead-letter-routing-key", "jack"); //声明正常队列 channel.queueDeclare(NORMAL_QUEUE,false,false,false,map); //正常交换机绑定正常队列 channel.queueBind(NORMAL_QUEUE,NORMAL_QUEUE,"tom"); //声明死信队列 channel.queueDeclare(DEAD_QUEUE,false,false,false,null); //死信交换机绑定死信队列 channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"jack"); //消费消息 DefaultConsumer consumer = new DefaultConsumer(channel) { /** * 消费回调函数,当收到消息以后,会自动执行这个方法 * @param consumerTag 消费者标识 * @param envelope 消息包的内容(比如交换机,路由key,消息id等) * @param properties 属性信息 * @param body 消息数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消息者1接受到的消息:" + new String(body, "UTF-8")); } }; //监听消息(队列名称,是否自动确认消息,消费对象) channel.basicConsume(NORMAL_QUEUE, true, consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }2.4消费者2public class Consumer2 { //定义交换机(死信交换机) public static final String DEAD_EXCHANGE = "dead_exchange"; //定义队列(死信队列) public static final String DEAD_QUEUE = "dead_queue"; public static void main(String[] args) { try { //创建信道对象 Channel channel = ConnectUtil.getChannel(); //声明死信队列 channel.queueDeclare(DEAD_QUEUE, false, false, false, null); //死信交换机绑定死信队列 channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "jack"); //消费消息 DefaultConsumer consumer = new DefaultConsumer(channel) { /** * 消费回调函数,当收到消息以后,会自动执行这个方法 * @param consumerTag 消费者标识 * @param envelope 消息包的内容(比如交换机,路由key,消息id等) * @param properties 属性信息 * @param body 消息数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消息者2接受到的消息:" + new String(body, "UTF-8")); } }; //监听消息(队列名称,是否自动确认消息,消费对象) channel.basicConsume(DEAD_QUEUE, true, consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }2.5设置TTL的两种方式2.5.1队列设置TTL Map<String, Object> map = new HashMap<>(); //设置队列有效期为10秒 map.put("x-message-ttl",10000); channel.queueDeclare(queueName,durable,exclusive,autoDelete,map);2.5.2消息设置TTL对每条消息设置TTL AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build(); channel.basicPublish(exchangeName,routingKey,mandatory,properties,"msg body".getBytes());2.5.3区别如果设置了队列的TTL属性,那么一旦消息过期,就会被队列丢弃。如果是消息设置了TTL属性,那么即使消息过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,那么已经过期的消息也许还能存活较长时间。如果我们没有设置TTL,就表示消息永远不会过期,如果TTL设置为0,则表示除非此时可以直接投递到消费者,否则该消息会被丢弃。
0
0
0
浏览量2014
开着皮卡写代码

一文总结 Shiro 实战教程

1.权限的管理1.1 什么是权限管理基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。权限管理包括用户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。1.2 什么是身份认证身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。1.3 什么是授权授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的2.什么是shiroApache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.Shiro 是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序—从最小的移动应用程序到最大的web和企业应用程序。Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。3.shiro的核心架构3.1 SubjectSubject即主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。 Subject在shiro中是一个接口,接口中定义了很多认证授权相关的方法,外部程序通过subject进行认证授权,而subject是通过SecurityManager安全管理器进行认证授权3.2 SecurityManagerSecurityManager即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。3.3 AuthenticatorAuthenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。3.4 AuthorizerAuthorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。3.5 RealmRealm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。3.6 SessionManagersessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。3.7 SessionDAOSessionDAO即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。3.8 CacheManagerCacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能。3.9 Cryptography​ Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。4. shiro中的认证4.1 认证身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。4.2 shiro中认证的关键对象Subject:主体访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;Principal:身份信息是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。credential:凭证信息是只有主体自己知道的安全信息,如密码、证书等。4.3 认证流程[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cEb7L6Z6-1680591385485)(Shiro 实战教程.assets/image-20200521204452288.png)]4.4 认证的开发1. 创建项目并引入依赖<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.3</version> </dependency>2. 引入shiro配置文件并加入如下配置[users] mosin=1234 tom=1234[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2zU9GESA-1680591385485)(Shiro 实战教程.assets/image-20220528172213825.png)]3.开发认证代码/** * @author: mosin * @version: v1.0 */ public class ShiroTest { public static void main(String[] args) { //创建默认的安全管理器 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); //创建安全管理器需要的realm对象 IniRealm iniRealm = new IniRealm("classpath:realm.ini"); //安全管理器设置realm对象 defaultSecurityManager.setRealm(iniRealm); //将安全管理器注入安全工具类 用于获取认证的主体 SecurityUtils.setSecurityManager(defaultSecurityManager); //获取认证的主体 Subject subject = SecurityUtils.getSubject(); //创建令牌 UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("mosin", "1234"); try { //认证 通过没有任何的异常 subject.login(usernamePasswordToken); //验证是否通过 boolean authenticated = subject.isAuthenticated(); System.out.println("认证通过:"+authenticated); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误!"); }catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("密码错误!"); } } }DisabledAccountException(帐号被禁用)LockedAccountException(帐号被锁定)ExcessiveAttemptsException(登录失败次数过多)ExpiredCredentialsException(凭证过期)等4.5 自定义Realm上边的程序使用的是Shiro自带的IniRealm,IniRealm从ini配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm。1.shiro提供的Realm2.根据认证源码认证使用的是SimpleAccountRealmSimpleAccountRealm的部分源码中有两个方法一个是 认证 一个是 授权,public class SimpleAccountRealm extends AuthorizingRealm { protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) token; SimpleAccount account = getUser(upToken.getUsername()); if (account != null) { if (account.isLocked()) { throw new LockedAccountException("Account [" + account + "] is locked."); } if (account.isCredentialsExpired()) { String msg = "The credentials for account [" + account + "] are expired"; throw new ExpiredCredentialsException(msg); } } return account; } protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = getUsername(principals); USERS_LOCK.readLock().lock(); try { return this.users.get(username); } finally { USERS_LOCK.readLock().unlock(); } } }3.自定义realm/** * 自定义realm */ public class CustomerRealm extends AuthorizingRealm { //认证方法 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } //授权方法 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal(); if("mosin".equals(principal)){ return new SimpleAuthenticationInfo(principal,"123",this.getName()); } return null; } }4.使用自定义Realm认证4.6 使用MD5和Salt实际应用是将盐和散列后的值存在数据库中,自动realm从数据库取出盐和加密后的值由shiro完成密码校验。/** * 自定义md5+salt realm */ public class CustomerMD5Realm extends AuthorizingRealm { //授权 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } //认证 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal(); //根据用户名查询数据库 if("mosin".equals(principal)){ // 参数1:用户名 参数2:密码 参数3:盐 参数4:自定义realm的名字 System.out.println(this.getName()); return new SimpleAuthenticationInfo(principal, "800d63a19662b2ba95bc2ffa01ab4804", ByteSource.Util.bytes("mosin"),this.getName()); } return null; } }2.使用md5 + salt 认证public class CustomerMD5RealmTest { public static void main(String[] args) { //创建安全管理器 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); //创建自定义MD5Realm对象 CustomerMD5Realm customerMD5Realm = new CustomerMD5Realm(); //创建密码认证匹配器对象 HashedCredentialsMatcher md5 = new HashedCredentialsMatcher("MD5"); //设置散列的次数 md5.setHashIterations(1024); //设置密码认证匹配器对象 customerMD5Realm.setCredentialsMatcher(md5); //设置安全管理器的 认证安全数据源 defaultSecurityManager.setRealm(customerMD5Realm); //设置安全工具类的安全管理器 SecurityUtils.setSecurityManager(defaultSecurityManager); //获取认证的主体 Subject subject = SecurityUtils.getSubject(); //创建令牌 UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("mosi", "12345"); //登录认证 try { subject.login(usernamePasswordToken); System.out.println("认证通过:"+subject.isAuthenticated()); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误"); }catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("密码错误!!!"); } } }5. shiro中的授权5.1 授权授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。5.2 关键对象授权可简单理解为who对what(which)进行How操作:Who,即主体(Subject),主体需要访问系统中的资源。What,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。How,权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。5.3 授权流程5.4 授权方式基于角色的访问控制RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制if(subject.hasRole("admin")){ //操作什么资源 }基于资源的访问控制RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制if(subject.isPermission("user:update:01")){ //资源实例 //对01用户进行修改 } if(subject.isPermission("user:update:*")){ //资源类型 //对01用户进行修改 }5.5 权限字符串​ 权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。例子:用户创建权限:user:create,或user:create:*用户修改实例001的权限:user:update:001用户实例001的所有权限:user:*:0015.6 shiro中授权编程实现方式编程式Subject subject = SecurityUtils.getSubject(); if(subject.hasRole(“admin”)) { //有权限 } else { //无权限 }注解式@RequiresRoles("admin") public void hello() { //有权限 }标签式JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成: <shiro:hasRole name="admin"> <!— 有权限—> </shiro:hasRole> 注意: Thymeleaf 中使用shiro需要额外集成!5.7 开发授权1.realm的实现public class CustomerRealm extends AuthorizingRealm { //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String primaryPrincipal = (String) principals.getPrimaryPrincipal(); System.out.println("primaryPrincipal = " + primaryPrincipal); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.addRole("admin"); simpleAuthorizationInfo.addStringPermission("user:update:*"); simpleAuthorizationInfo.addStringPermission("product:*:*"); return simpleAuthorizationInfo; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal(); if("xiaochen".equals(principal)){ String password = "3c88b338102c1a343bcb88cd3878758e"; String salt = "Q4F%"; return new SimpleAuthenticationInfo(principal,password, ByteSource.Util.bytes(salt),this.getName()); } return null; } }2.授权public class CustomerMD5RealmTest { public static void main(String[] args) { //创建安全管理器 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); //创建自定义MD5Realm对象 CustomerMD5Realm customerMD5Realm = new CustomerMD5Realm(); //创建密码认证匹配器对象 HashedCredentialsMatcher md5 = new HashedCredentialsMatcher("md5"); //设置加密的次数 md5.setHashIterations(1024); //设置密码认证匹配器对象 customerMD5Realm.setCredentialsMatcher(md5); //设置安全管理器的 认证安全数据源 defaultSecurityManager.setRealm(customerMD5Realm); //设置安全工具类的安全管理器 SecurityUtils.setSecurityManager(defaultSecurityManager); //获取认证的主体 Subject subject = SecurityUtils.getSubject(); //创建令牌 UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("mosin", "12345"); //登录认证 try { subject.login(usernamePasswordToken); System.out.println("认证通过:"+subject.isAuthenticated()); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误"); }catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("密码错误!!!"); } //基于角色的控制 //单角色控制 System.out.println("========hasRole=========="); boolean admin = subject.hasRole("admin"); System.out.println("hash admin role:"+admin); //多角色控制 System.out.println("========hasAllRoles=========="); List<String> roles = Arrays.asList("admin", "user"); boolean booleans = subject.hasAllRoles(roles); System.out.println("booleans = " + booleans); // 基于任意角色的控制 System.out.println("========hasRoles=========="); boolean[] booleans1 = subject.hasRoles(roles); for (boolean b : booleans1) { System.out.println("b = " + b); } //基于权限字符串的权限控制 System.out.println("========isPermitted=========="); boolean permitted = subject.isPermitted("user:update:*"); System.out.println("permitted = " + permitted); //分别具有哪些权限 boolean[] permitted1 = subject.isPermitted("user:update:*", "product:update:*"); for (boolean b : permitted1) { System.out.println("b = " + b); } //同时具有哪些权限 boolean permittedAll = subject.isPermittedAll("user:update:*", "product:update:*"); System.out.println("permittedAll = " + permittedAll); } }
0
0
0
浏览量2008
开着皮卡写代码

Shiro整合SpringBoot项目实战

6.0 整合思路6.1 创建springboot项目6.2 引入shiro依赖在pom.xml文件中添加Shiro依赖。可以选择,也可以手动添加Shiro的依赖。 <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.5.3</version> </dependency>6.3 配置shiro环境0.创建配置类在Spring Boot应用程序中,可以通过编写一个Shiro配置类来配置Shiro。在该类中,可以定义Shiro的安全管理器、Realm、过滤器等组件,并将它们注入Spring容器中。1.配置shiroFilterFactoryBeanShiro过滤器是实现基于角色或权限的访问控制的核心组件。可以通过编写一个ShiroFilter配置类来定义Shiro过滤器链,并将其注入到Spring容器中。@Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager){ //创建shiro的filter ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //注入安全管理器 shiroFilterFactoryBean.setSecurityManager(securityManager); return shiroFilterFactoryBean; }2.配置WebSecurityManagerDefaultWebSecurityManager类主要定义了设置subjectDao,获取会话模式,设置会话模式,设置会话管理器,是否是http会话模式等操作,它继承了DefaultSecurityManager类,实现了WebSecurityManager接口@Bean public DefaultWebSecurityManager getSecurityManager(Realm realm){ DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); defaultWebSecurityManager.setRealm(realm); return defaultWebSecurityManager; }3.创建和配置自定义realm在Shiro中,。通过实现自定义的Realm,可以将Shiro连接到应用程序的数据库、LDAP目录或其他数据源中。在自定义Realm中,需要实现认证和授权逻辑,并将其注入到Shiro的安全管理器中。public class CustomerRealm extends AuthorizingRealm { //处理授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } //处理认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { return null; } } //创建自定义realm @Bean public Realm getRealm(){ return new CustomerRealm(); }4.编写主页面index.jsp5.启动项目,访问index.jsp默认在配置好shiro环境后默认环境中没有对项目中任何资源进行权限控制,所有现在项目中所有资源都可以通过路径访问6.加入权限控制修改ShiroFilterFactoryBean配置public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); //配置系统的受限资源 authc //配置系统的公共资源 anon HashMap<String, String> map = new HashMap<>(); map.put("/index.jsp", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); shiroFilterFactoryBean.setLoginUrl("/login.jsp"); return shiroFilterFactoryBean; } /** 代表拦截项目中一切资源 authc 代表shiro中的一个filter的别名,详细内容看文档的shirofilter列表7.重启项目访问查看6.4 常见过滤器注意: shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置控制指定url的权限:6.5 认证实现1. 在login.jsp中开发认证界面<form action="${pageContext.request.contextPath}/user/login" method="post"> 用户名:<input type="text" name="username" > <br/> 密码 : <input type="text" name="password"> <br> <input type="submit" value="登录"> </form>2. 开发controller@Controller @RequestMapping("user") public class UserController { /** * 用来处理身份认证 * @param username * @param password * @return */ @RequestMapping("login") public String login(String username,String password){ //获取主体对象 Subject subject = SecurityUtils.getSubject(); try { subject.login(new UsernamePasswordToken(username,password)); return "redirect:/index.jsp"; } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误!"); }catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("密码错误!"); } return "redirect:/login.jsp"; } } 在认证过程中使用subject.login进行认证3.开发realm中返回静态数据(未连接数据库)@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("=========================="); String principal = (String) token.getPrincipal(); if("mosin".equals(principal)){ return new SimpleAuthenticationInfo(principal,"123",this.getName()); } return null; } }4.启动项目以realm中定义静态数据进行认证认证功能没有md5和随机盐的认证就实现啦6.6 退出认证1.开发页面退出连接2.开发controller@Controller @RequestMapping("user") public class UserController { /** * 退出登录 * */ @RequestMapping("logout") public String logout(){ Subject subject = SecurityUtils.getSubject(); subject.logout();//退出用户 return "redirect:/login.jsp"; } } 3.修改退出连接访问退出路径4.退出之后访问受限资源立即返回认证界面6.7 MD5、Salt的认证实现1.开发数据库注册0.开发注册界面<h1>用户注册</h1> <form action="${pageContext.request.contextPath}/user/register" method="post"> 用户名:<input type="text" name="username" > <br/> 密码 : <input type="text" name="password"> <br> <input type="submit" value="立即注册"> </form>1.创建数据表结构SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for t_user -- ---------------------------- DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user` ( `id` int(6) NOT NULL AUTO_INCREMENT, `username` varchar(40) DEFAULT NULL, `password` varchar(40) DEFAULT NULL, `salt` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; SET FOREIGN_KEY_CHECKS = 1; 2.项目引入依赖<!--mybatis相关依赖--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.2</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> <!--druid--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.19</version> </dependency>3.配置application.properties配置文件server.port=8888 server.servlet.context-path=/shiro spring.application.name=shiro spring.mvc.view.prefix=/ spring.mvc.view.suffix=.jsp #新增配置 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/shiro?characterEncoding=UTF-8 spring.datasource.username=root spring.datasource.password=root mybatis.type-aliases-package=cn.kgc.springboot_jsp_shiro.entity mybatis.mapper-locations=classpath:mapper/*.xml4.创建entity@Data @Accessors(chain = true) @AllArgsConstructor @NoArgsConstructor public class User { private String id; private String username; private String password; private String salt; }5.创建DAO接口@Mapper public interface UserDAO { void save(User user); }6.开发mapper配置文件<insert id="save" parameterType="User" useGeneratedKeys="true" keyProperty="id"> insert into t_user values(#{id},#{username},#{password},#{salt}) </insert>7.开发service接口public interface UserService { //注册用户方法 void register(User user); }8.创建salt工具类public class SaltUtils { /** * 生成salt的静态方法 * @param n * @return */ public static String getSalt(int n){ char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) { char aChar = chars[new Random().nextInt(chars.length)]; sb.append(aChar); } return sb.toString(); } } 9.开发service实现类@Service @Transactional public class UserServiceImpl implements UserService { @Autowired private UserDAO userDAO; @Override public void register(User user) { //处理业务调用dao //1.生成随机盐 String salt = SaltUtils.getSalt(8); //2.将随机盐保存到数据 user.setSalt(salt); //3.明文密码进行md5 + salt + hash散列 Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024); user.setPassword(md5Hash.toHex()); userDAO.save(user); } }10.开发Controller@Controller @RequestMapping("user") public class UserController { @Autowired private UserService userService; /** * 用户注册 */ @RequestMapping("register") public String register(User user) { try { userService.register(user); return "redirect:/login.jsp"; }catch (Exception e){ e.printStackTrace(); return "redirect:/register.jsp"; } } }11.启动项目进行注册2.开发数据库认证0.开发DAO@Mapper public interface UserDAO { void save(User user); //根据身份信息认证的方法 User findByUserName(String username); }1.开发mapper配置文件<select id="findByUserName" parameterType="String" resultType="User"> select id,username,password,salt from t_user where username = #{username} </select>2.开发Service接口public interface UserService { //注册用户方法 void register(User user); //根据用户名查询业务的方法 User findByUserName(String username); }3.开发Service实现类@Service("userService") @Transactional public class UserServiceImpl implements UserService { @Autowired private UserDAO userDAO; @Override public User findByUserName(String username) { return userDAO.findByUserName(username); } }4.开发在工厂中获取bean对象的工具类@Component public class ApplicationContextUtils implements ApplicationContextAware { private static ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; } //根据bean名字获取工厂中指定bean 对象 public static Object getBean(String beanName){ return context.getBean(beanName); } }5.修改自定义realm @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("=========================="); //根据身份信息 String principal = (String) token.getPrincipal(); //在工厂中获取service对象 UserService userService = (UserService) ApplicationContextUtils.getBean("userService"); //根据身份信息查询 User user = userService.findByUserName(principal); if(!ObjectUtils.isEmpty(user)){ //返回数据库信息 return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName()); } return null; } 6.修改ShiroConfig中realm使用凭证匹配器以及hash散列@Bean public Realm getRealm(){ CustomerRealm customerRealm = new CustomerRealm(); //设置hashed凭证匹配器 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); //设置md5加密 credentialsMatcher.setHashAlgorithmName("md5"); //设置散列次数 credentialsMatcher.setHashIterations(1024); customerRealm.setCredentialsMatcher(credentialsMatcher); return customerRealm; }
0
0
0
浏览量2012
开着皮卡写代码

SpringBoot使用Redis实现分布式缓存

springboot使用Redis实现分布式缓存1、环境构建​ 1.1 通过MybatisX工具逆向功能快速初始化一个工程(springboot+mybatis-plus) 1.2 构建controller层测试各模块的功能 1.3 相同的请求没有实现共享数据,需要开启mybatis的二级缓存​ 1.4 springboot环境下开启mybatis-plus的二级缓存1.5编写获取spring工厂的工具类@Component public class ApplicationContextUtils implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public static Object getBean(String beanName){ return applicationContext.getBean(beanName); } }1.6编写Redis缓存类@Slf4j public class RedisCache implements Cache { private final String id; public RedisCache(String id){ this.id = id; } // 操作模块的mapper文件的命名空间 唯一标识符 @Override public String getId() { log.info("id= {}",id); return this.id; } // 将数据写入redis @Override public void putObject(Object key, Object value) { log.info("===============将查询的数据开始写入缓存==============="); RedisTemplate redisTemplate = getRedisTemplate(); redisTemplate.opsForHash().put(id, key.toString(), value); log.info("===============将查询的数据写入缓存完毕==============="); } // 获取缓存中的数据 @Override public Object getObject(Object key) { log.info("============开始从缓存中获取数据============="); RedisTemplate redisTemplate = getRedisTemplate(); log.info("============从缓存中获取数据完毕============="); return redisTemplate.opsForHash().get(id, key.toString()); } // 移除缓存中的数据 @Override public Object removeObject(Object key) { return null; } // 清空缓存 @Override public void clear() { log.info("==========清空缓存============="); RedisTemplate redisTemplate = getRedisTemplate(); redisTemplate.delete(id); } // 获取缓存的数量 @Override public int getSize() { RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate"); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); int size = redisTemplate.opsForHash().size(id).intValue(); return size; } private RedisTemplate getRedisTemplate(){ RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate"); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); return redisTemplate; } }1.7Redis中有关联关系缓存数据的处理@CacheNamespaceRef(DeptMapper.class) // 引用有关联关系的命名空间 public interface EmpMapper extends BaseMapper<Emp> { } 注:以上设置完成后,两个模块会使用相同的key(命名空间)存储数据到缓存中 1.8 Redis中key进行摘要算法DigestUtils.md5DigestAsHex(key.toString().getBytes()) // 通过该操作可以减少key的长度Redis实现主从复制1.准备三台已经安装Redis的虚拟机2.查看三台虚拟机的ip地址3.通过远程连接工具FinalShell连接4.修改从节点配置文件启动三台服务器上的redis后,输入一下命令查看redis主从配置状态info replication修改从节点服务器的配置文件redis.confreplicaof 主机ip 主机redis接口 masterauth 密码修改后重启两个从机,在主机和从机分别输入一下命令查看如下:info replication验证主从架构至此主从架构设置完成Redis集群的构建以上结构的集群构建可以在一台虚拟机环境中进行模拟,首先创建一台已经安装好Redis数据库的虚拟机开启虚拟机并在虚拟机的根路径下创建好7000,7001,7002,7003,7004,7005六个文件夹,之后将redis解压目录下的redis.conf配置文件拷贝到以上几个文件夹中,同时按照以下参数完成配置文件的修改修改配置文件中的参数-port 7000 .... 每个文件修改成不同的端口号 因为是在一台虚拟机中进行的模拟 -bind 0.0.0.0 或者改成本机的ip地址 -cluster-enable yes 开启集群模式 -cluster-config-file nodes-port.conf 集群节点配置文件,可加端口 nodes-7000.conf -cluster-node-timeout 5000 集群节点的超时时间 -appendonly yes 开启AOF持久化机制 -appendonly-aof 持久化文件的名字 修改为不一样的名字 可加端口号 appendonly-7000.aof以上6个文件夹中文件全部修改完毕之后,可以按照以下指令启动全部的redis节点[root@localhost bin]# ./redis-server /7000/redis.conf [root@localhost bin]# ./redis-server /7001/redis.conf [root@localhost bin]# ./redis-server /7002/redis.conf [root@localhost bin]# ./redis-server /7003/redis.conf [root@localhost bin]# ./redis-server /7004/redis.conf [root@localhost bin]# ./redis-server /7005/redis.conf查看redis服务是否已经全部启动成功ps aux|grep redis全部启动成功之后,执行以下指令,将多个节点组合成集群,同时实现主从备份./redis-cli --cluster create 如果有密码可以添加参数 -a 192.168.253.132:7000 192.168.253.132:7001 192.168.253.132:7002 192.168.253.132:7003 192.168.253.132:7004 192.168.253.132:7005 --cluster-replicas 1 主从节点的配比 1:1确认集群的主从从节点信息输入yes,确认主从节点信息后,输出以下信息,表示集群构建成功使用一下指令登录集群中的任意节点实现数据的操作,查看集群是否可正常工作./redis-cli -a cyclone -c -h 192.168.220.11 -p 7001 连接 -a 表示连接密码 没有可省略 -c 表示集群方式进行启动 -h ip 地址 -p 表示端口号如果在springboot项目中连接Redis集群可按照一下方式进行配置redis: cluster: nodes: 192.168.1.1:6379 ,.....
0
0
0
浏览量2013
开着皮卡写代码

RabbitMQ:发布订阅模式

1.订阅模式基本介绍P:生产者,发送消息给交换机C:消费者,接收消息X:交换机,一方面接收生产者发送的消息,另一方面知道怎么处理消息,是否应将其附加到特定队列?是否应将其附加到多个队列中?或者它应该被丢弃。其规则由交换类型定义。Queue:消息队列,接收消息,缓存消息每个消费者都监听自己的队列生产者把消息发送给broker,然后交换机把消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息。2.交换机RabbitMQ 中消息传递模型的核心思想是,生产者从不将任何消息直接发送到队列。实际上,很多时候,生产者甚至不知道消息是否会传递到任何队列。相反,生产者只能将消息发送到_交换机_。交换机的工作是一件非常简单的事情。一方面,它接收来自生产者的消息,另一方面则将它们推送到队列。交换必须确切地知道如何处理它收到的消息。是否应将其附加到特定队列?是否应将其附加到多个队列中?或者它应该被丢弃。其规则由_交换类型_定义。交换机只负责转发消息,并没有存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!交换机类型Fanout:广播,将消息交给所有绑定到交换机的队列Direct:定向,把消息交给符合指定routing key 的队列Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列3.发布订阅模式3.1基本介绍要配置一个fanout类型的交换机,不需要指定对应的路由key,同时会把消息路由到每一个消息队列中,每个消息队列都可以对相同的消息进行存储,然被由各自的消息队列相关联的消费者消费3.2生产者public class Producer { public static String FANOUT_EXCHANGE = " fanout_exchange"; public static String FANOUT_QUEUE_1 = "fanout_queue_1"; public static String FANOUT_QUEUE_2 = "fanout_queue_2"; public static void main(String[] args) { try { Channel channel = ConnectUtil.getChannel(); //声明交换机(交换机名称,交换机类型) channel.exchangeDeclare(FANOUT_EXCHANGE, BuiltinExchangeType.FANOUT); //声明队列 channel.queueDeclare(FANOUT_QUEUE_1,true,false,false,null); channel.queueDeclare(FANOUT_QUEUE_2,true,false,false,null); //把交换机和队列进行绑定 channel.queueBind(FANOUT_QUEUE_1,FANOUT_EXCHANGE,""); channel.queueBind(FANOUT_QUEUE_2,FANOUT_EXCHANGE,""); //发送消息 for (int i = 1; i <=10 ; i++) { String msg="你好,小兔子,发布订阅模式 : "+i; channel.basicPublish(FANOUT_EXCHANGE, "", null, msg.getBytes()); } } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }3.3消费者消费者1public class Consumer1 { public static void main(String[] args) { try { Channel channel = ConnectUtil.getChannel(); channel.queueDeclare(Producer.FANOUT_QUEUE_1, true, false, false, null); channel.exchangeDeclare(Producer.FANOUT_EXCHANGE, BuiltinExchangeType.FANOUT); //把队列和交换机绑定 队列名称,交换机名称,路由key channel.queueBind(Producer.FANOUT_QUEUE_1, Producer.FANOUT_EXCHANGE, ""); //接受消息 DefaultConsumer consumer = new DefaultConsumer(channel) { /** * 消费回调函数,当收到消息以后,会自动执行这个方法 * @param consumerTag 消费者标识 * @param envelope 消息包的内容(比如交换机,路由key,消息id等) * @param properties 属性信息 * @param body 消息数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消息者1接受到的消息:" + new String(body, "UTF-8")); } }; //监听消息(队列名称,是否自动确认消息,消费对象) channel.basicConsume(Producer.FANOUT_QUEUE_1, true, consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }消费者2public class Consumer2 { public static void main(String[] args) { try { Channel channel = ConnectUtil.getChannel(); channel.queueDeclare(Producer.FANOUT_QUEUE_2, true, false, false, null); channel.exchangeDeclare(Producer.FANOUT_EXCHANGE, BuiltinExchangeType.FANOUT); //把队列和交换机绑定 队列名称,交换机名称,路由key channel.queueBind(Producer.FANOUT_QUEUE_2, Producer.FANOUT_EXCHANGE, ""); //接受消息 DefaultConsumer consumer = new DefaultConsumer(channel) { /** * 消费回调函数,当收到消息以后,会自动执行这个方法 * @param consumerTag 消费者标识 * @param envelope 消息包的内容(比如交换机,路由key,消息id等) * @param properties 属性信息 * @param body 消息数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消息者2接受到的消息:" + new String(body, "UTF-8")); } }; //监听消息(队列名称,是否自动确认消息,消费对象) channel.basicConsume(Producer.FANOUT_QUEUE_2, true, consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }3.4测试
0
0
0
浏览量2012
开着皮卡写代码

Spring整合RabbitMQ

1.简单消息模式1.1生产者1.1.1 创建生产者工程 spring-rabbitmq-producer1.1.2引入依赖 <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> <version>2.1.8.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.1.7.RELEASE</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>1.1.3属性配置文件在resources目录下面创建rabbitmq.properties属性配置文件rabbitmq.host=192.168.137.118 rabbitmq.port=5672 rabbitmq.username=admin rabbitmq.password=123456 rabbitmq.virtual-host=/1.1.4spring整合配置文件<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- 加载属性配置文件 --> <context:property-placeholder location="classpath:rabbitmq.properties"></context:property-placeholder> <!-- 定义rabbitmq connectionFactory --> <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}" password="${rabbitmq.password}" virtual-host="${rabbitmq.virtual-host}"/> <!-- 创建rabbit Admin对象,用于管理交换机和队列 --> <rabbit:admin connection-factory="connectionFactory"></rabbit:admin> <!--定义持久化队列,不存在则自动创建;不绑定到交换机则绑定到默认交换机 默认交换机类型为direct,名字为:"",路由键为队列的名称, 默认持久化,默认排他 --> <rabbit:queue id="spring_queue" name="spring_queue" auto-declare="true"></rabbit:queue> <!--创建RabbitTemplate对象,用于操作消息--> <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"></rabbit:template> </beans>1.1.5发送消息@RunWith注解的作用:让测试在spring容器环境下执行,如果测试类没有这个注解,会导致service,dao等注入失败 @RunWith(SpringRunner.class) @ContextConfiguration(locations = "classpath:spring-rabbitmq.xml") public class ProducerTest { @Autowired private RabbitTemplate rabbitTemplate; @Test /** * 只发队列消息 * 默认交换机类型为 direct * 交换机的名称为空,路由键为队列的名称 */ public void queueTest(){ //路由键与队列同名 rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息。"); } }1.2消费者1.2.1创建消费者工程 spring-rabbitmq-consumer1.2.2引入依赖 <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> <version>2.1.8.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.1.7.RELEASE</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>1.2.3属性配置文件在resources目录下面创建rabbitmq.properties属性配置文件rabbitmq.host=192.168.137.118 rabbitmq.port=5672 rabbitmq.username=admin rabbitmq.password=123456 rabbitmq.virtual-host=/1.2.4spring整合配置文件<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- 加载属性配置文件 --> <context:property-placeholder location="classpath:rabbitmq.properties"></context:property-placeholder> <!-- 定义rabbitmq connectionFactory --> <!-- 定义rabbitmq connectionFactory --> <!-- 定义rabbitmq connectionFactory --> <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}" password="${rabbitmq.password}" virtual-host="${rabbitmq.virtual-host}"/> <!--用来处理消息的消费者 --> <bean id="springQueueListener" class="com.zyh.listener.SpringQueueListener"></bean> <!-- 监听器绑定队列,这样队列中来消息就被监听器处理掉 --> <rabbit:listener-container connection-factory="connectionFactory"> <!-- 监听哪一个队列,谁来监听 --> <rabbit:listener ref="springQueueListener" queue-names="spring_queue"></rabbit:listener> </rabbit:listener-container> </beans>1.2.5消息监听器public class SpringQueueListener implements MessageListener { @Override public void onMessage(Message message) { try { byte[] body = message.getBody(); String msg=new String(body,"UTF-8"); MessageProperties messageProperties = message.getMessageProperties(); System.out.println("msg = "+msg); System.out.println("messageProperties = "+ messageProperties); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } }1.2.6测试2.发布订阅模式2.1生产者2.1.1spring整合配置文件 <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~广播;所有队列都能收到消息~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --> <!--定义广播交换机中的持久化队列,不存在则自动创建--> <rabbit:queue id="spring_fanout_queue_1" name="spring_fanout_queue_1" auto-declare="true"/> <!--定义广播交换机中的持久化队列,不存在则自动创建--> <rabbit:queue id="spring_fanout_queue_2" name="spring_fanout_queue_2" auto-declare="true"/> <!--定义广播类型交换机;并绑定上述两个队列--> <rabbit:fanout-exchange id="spring_fanout_exchange" name="spring_fanout_exchange" auto-declare="true"> <rabbit:bindings> <rabbit:binding queue="spring_fanout_queue_1"/> <rabbit:binding queue="spring_fanout_queue_2"/> </rabbit:bindings> </rabbit:fanout-exchange>2.1.2发送消息@RunWith(SpringRunner.class) @ContextConfiguration(locations = "classpath:spring-rabbitmq.xml") public class ProducerTest { @Autowired private RabbitTemplate rabbitTemplate; /** * 发送广播 * 交换机类型为 fanout * 绑定到该交换机的所有队列都能够收到消息 */ @Test public void fanoutTest(){ /** * 参数1:交换机名称 * 参数2:路由键名(广播设置为空) * 参数3:发送的消息内容 */ rabbitTemplate.convertAndSend("spring_fanout_exchange", "", "发送到spring_fanout_exchange交换机的广播消息"); } }2.2消费者2.2.1spring整合配置文件<bean id="fanoutListener1" class="com.zyh.listener.FanoutListener1"/> <bean id="fanoutListener2" class="com.zyh.listener.FanoutListener2"/> <rabbit:listener-container connection-factory="connectionFactory" auto-declare="true"> <rabbit:listener ref="fanoutListener1" queue-names="spring_fanout_queue_1"/> <rabbit:listener ref="fanoutListener2" queue-names="spring_fanout_queue_2"/> </rabbit:listener-container>2.2.2广播监听器1public class FanoutListener1 implements MessageListener { public void onMessage(Message message) { try { String msg = new String(message.getBody(), "utf-8"); System.out.printf("广播监听器1:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n", message.getMessageProperties().getReceivedExchange(), message.getMessageProperties().getReceivedRoutingKey(), message.getMessageProperties().getConsumerQueue(), msg); } catch (Exception e) { e.printStackTrace(); } } }2.2.3广播监听器2public class FanoutListener2 implements MessageListener { public void onMessage(Message message) { try { String msg = new String(message.getBody(), "utf-8"); System.out.printf("广播监听器2:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n", message.getMessageProperties().getReceivedExchange(), message.getMessageProperties().getReceivedRoutingKey(), message.getMessageProperties().getConsumerQueue(), msg); } catch (Exception e) { e.printStackTrace(); } } }3.Topics 通配符模式3.1生产者3.1.1spring整合配置文件<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~通配符;*匹配一个单词,#匹配多个单词 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --> <!--定义广播交换机中的持久化队列,不存在则自动创建--> <rabbit:queue id="spring_topic_queue_star" name="spring_topic_queue_star" auto-declare="true"/> <!--定义广播交换机中的持久化队列,不存在则自动创建--> <rabbit:queue id="spring_topic_queue_well" name="spring_topic_queue_well" auto-declare="true"/> <!--定义广播交换机中的持久化队列,不存在则自动创建--> <rabbit:queue id="spring_topic_queue_well2" name="spring_topic_queue_well2" auto-declare="true"/> <rabbit:topic-exchange id="spring_topic_exchange" name="spring_topic_exchange" auto-declare="true"> <rabbit:bindings> <rabbit:binding pattern="zyh.*" queue="spring_topic_queue_star"/> <rabbit:binding pattern="zyh.#" queue="spring_topic_queue_well"/> <rabbit:binding pattern="yh.#" queue="spring_topic_queue_well2"/> </rabbit:bindings> </rabbit:topic-exchange>3.1.1发送消息@RunWith(SpringRunner.class) @ContextConfiguration(locations = "classpath:spring-rabbitmq.xml") public class ProducerTest { @Autowired private RabbitTemplate rabbitTemplate; /** * 通配符 * 交换机类型为 topic * 匹配路由键的通配符,*表示一个单词,#表示多个单词 * 绑定到该交换机的匹配队列能够收到对应消息 */ @Test public void topicTest(){ /** * 参数1:交换机名称 * 参数2:路由键名 * 参数3:发送的消息内容 */ rabbitTemplate.convertAndSend("spring_topic_exchange", "zyh.bj", "发送到spring_topic_exchange交换机zyh.bj的消息"); rabbitTemplate.convertAndSend("spring_topic_exchange", "zyh.bj.1", "发送到spring_topic_exchange交换机zyh.bj.1的消息"); rabbitTemplate.convertAndSend("spring_topic_exchange", "zyh.bj.2", "发送到spring_topic_exchange交换机zyh.bj.2的消息"); rabbitTemplate.convertAndSend("spring_topic_exchange", "yh.cn", "发送到spring_topic_exchange交换机yh.cn的消息"); } }3.2消费者3.2.1spring整合配置文件 <bean id="topicListenerStar" class="com.zyh.listener.TopicListenerStar"/> <bean id="topicListenerWell" class="com.zyh.listener.TopicListenerWell"/> <bean id="topicListenerWell2" class="com.zyh.listener.TopicListenerWell2"/> <rabbit:listener-container connection-factory="connectionFactory" auto-declare="true"> <rabbit:listener ref="topicListenerStar" queue-names="spring_topic_queue_star"/> <rabbit:listener ref="topicListenerWell" queue-names="spring_topic_queue_well"/> <rabbit:listener ref="topicListenerWell2" queue-names="spring_topic_queue_well2"/> </rabbit:listener-container>3.2.2星号通配符监听器public class TopicListenerStar implements MessageListener { public void onMessage(Message message) { try { String msg = new String(message.getBody(), "utf-8"); System.out.printf("通配符*监听器:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n", message.getMessageProperties().getReceivedExchange(), message.getMessageProperties().getReceivedRoutingKey(), message.getMessageProperties().getConsumerQueue(), msg); } catch (Exception e) { e.printStackTrace(); } } } 3.2.3井号通配符监听器public class TopicListenerWell implements MessageListener { public void onMessage(Message message) { try { String msg = new String(message.getBody(), "utf-8"); System.out.printf("通配符#监听器:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n", message.getMessageProperties().getReceivedExchange(), message.getMessageProperties().getReceivedRoutingKey(), message.getMessageProperties().getConsumerQueue(), msg); } catch (Exception e) { e.printStackTrace(); } } }3.2.4井号通配符监听器2public class TopicListenerWell2 implements MessageListener { public void onMessage(Message message) { try { String msg = new String(message.getBody(), "utf-8"); System.out.printf("通配符#监听器2:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n", message.getMessageProperties().getReceivedExchange(), message.getMessageProperties().getReceivedRoutingKey(), message.getMessageProperties().getConsumerQueue(), msg); } catch (Exception e) { e.printStackTrace(); } } }
0
0
0
浏览量2013
开着皮卡写代码

rabbitmq入门

1.工程搭建1.1步骤分析官网https://www.rabbitmq.com/tutorials/tutorial-one-java.html需求:使用简单模式完成消息传递步骤:① 创建工程(生产者、消费者)② 分别添加依赖③ 编写生产者发送消息④ 编写消费者接收消息1.2创建工程项目创建成功后,把src目录删除然后创建子模块1.3添加依赖 <dependencies> <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.6.0</version> </dependency> </dependencies>2.编写生产者public class Producer { public static void main(String[] args) { try { //创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //主机地址 connectionFactory.setHost("主机地址"); //连接端口;默认为 5672 connectionFactory.setPort(5672); //虚拟主机名称;默认为 / connectionFactory.setVirtualHost("/"); //连接用户名;默认为guest connectionFactory.setUsername("admin"); //连接密码;默认为guest connectionFactory.setPassword("写上自己的密码"); //创建连接 Connection connection = connectionFactory.newConnection(); //创建频道 Channel channel = connection.createChannel(); // 声明(创建)队列 /** * queue 参数1:队列名称 * durable 参数2:是否定义持久化队列,当mq重启之后,还在 * exclusive 参数3:是否独占本次连接 * ① 是否独占,只能有一个消费者监听这个队列 * ② 当connection关闭时,是否删除队列 * autoDelete 参数4:是否在不使用的时候自动删除队列,当没有consumer时,自动删除 * arguments 参数5:队列其它参数 */ channel.queueDeclare("simple_queue", true, false, false, null); // 要发送的信息 String message = "你好;小兔子!"; /** * 参数1:交换机名称,如果没有指定则使用默认Default Exchage * 参数2:路由key,简单模式可以传递队列名称 * 参数3:配置信息 * 参数4:消息内容 */ channel.basicPublish("", "simple_queue", null, message.getBytes()); System.out.println("已发送消息:" + message); // 关闭资源 channel.close(); connection.close(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }运行程序:在浏览器输入服务器地址在执行上述的消息发送之后;可以登录rabbitMQ的管理控制台,可以发现队列和其消息:3.编写消费者public class Consumer { public static void main(String[] args) { try { //1.创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //2. 设置参数 //ip factory.setHost("主机地址"); //端口 默认值 5672 factory.setPort(5672); //虚拟机 默认值/ factory.setVirtualHost("/"); //用户名 factory.setUsername("admin"); //密码 factory.setPassword("写上自己的密码"); //3. 创建连接 Connection Connection connection = factory.newConnection(); //4. 创建Channel Channel channel = connection.createChannel(); //消费方通过信息获取数据,该方法不会自动执行 DefaultConsumer consumer=new DefaultConsumer(channel){ //接收队列中数据的方法 /* 回调方法,当收到消息后,会自动执行该方法 1. consumerTag:标识 2. envelope:获取一些信息,交换机,路由key... 3. properties:配置信息 4. body:数据 */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("consumerTag:"+consumerTag); System.out.println("Exchange:"+envelope.getExchange()); System.out.println("RoutingKey:"+envelope.getRoutingKey()); System.out.println("properties:"+properties); System.out.println("body:"+new String(body)); } } ; //5. 创建队列Queue,通过信道获取队列的数据 String queue_name="simple_queue"; /* basicConsume(String queue, boolean autoAck, Consumer callback) 参数: 1. queue:队列名称 2. autoAck:是否自动确认 ,类似咱们发短信,发送成功会收到一个确认消息(如果自动签收,会删除队列的该条消息) 3. callback:回调对象 */ // 消费者类似一个监听程序,主要是用来监听消息 channel.basicConsume(queue_name,true,consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }运行程序4.入门案例总结上述的入门案例中中其实使用的是如下的简单模式:在上图的模型中,有以下概念:P:生产者,也就是要发送消息的程序C:消费者:消息的接受者,会一直等待消息到来。queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。5.AMQP5.1基本介绍AMQP 一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。RabbitMQ是AMQP协议的Erlang的实现。5.2rabbitmq运转流程在入门案例中:生产者发送消息1.生产者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker;2.声明队列并设置属性;如是否排它,是否持久化,是否自动删除;3.将路由键(空字符串)与队列绑定起来;4.发送消息至RabbitMQ Broker;5.关闭信道;6.关闭连接;消费者接收消息1.消费者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker2.向Broker 请求消费相应队列中的消息,设置相应的回调函数;3.等待Broker投递响应队列中的消息,消费者接收消息;4.确认(ack,自动确认)接收到的消息;5.RabbitMQ从队列中删除相应已经被确认的消息;6.关闭信道;7.关闭连接;
0
0
0
浏览量2011
开着皮卡写代码

消息中间件概述

学习消息队列的原因在电子商务应用中,我们经常需要对庞大的数据进行监控,MQ的使用与日俱增,特别是RabbitMQ在分布式系统中存储转发消息,可以保证数据不丢失,也可以保证高可用性,即集群的时候部分机器宕机可以继续执行。在大型电子商务类网址,比如京东、淘宝等网址有着深入的应用。队列可以消除高并发访问高峰,加快网站的响应速度。在不适用消息队列的情况,用户的请求直接写入数据库,在高并发的情况会对数据库造成巨大的压力,同时也会导致系统响应延迟加剧。消息中间件概述MQ(Message Queue),消息队列(MQ)是一种应用程序对应用程序的通信方法消息队列是一种先进先出的数据结构消息传递:指的是程序直接通过消息发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是拥有远程调用的技术引入消息队列的原因不同进程之间传递消息的时候,两个进程之间耦合度过高,一个进程的改变就会引发另外一个进程的改变。为了让它们不会互相干扰,我们需要在两个进程之间抽离出一个模块,两个进程之间传递的消息通过消息队列来传递。单独修改某一个进程不会影响另外一个。某个进程一下子接受的消息太多了,无法马上处理好,就需要对接受的消息进行排队,所以有了消息队列我们可以把一些不需要即时返回而且又耗费时间的操作提取出来,进行异步处理,可以节省服务器的请求响应时间,从而提高了系统的吞吐量。消息队列应用场景消息中间件主要作用异步处理解耦服务流量削峰应用解耦传统的模式会出现耦合的情况通过中间件模式,可以进行解耦的作用异步处理场景说明:用户注册以后,需要发送注册邮件和注册短信,传统的方式有两种:串行方式和并行的方式。串行方式:把注册信息写到数据库中后,发送注册邮件,再发送注册短信,这三个认为全部完成以后才返回给客户端。但是有个地方要注意,邮件和短信并不是必须的,它只是一个通知,这种方式会导致客户端在等一些没有必要等待的东西。并行方式:把注册信息写到数据库中后,发送邮件的同时,发送短信,这三个认为任务完成后返回给客户端,并行的方式可以提高处理的事件时间虽然并行的方式可以提高效率,但是因为短信和邮件对于我们正常使用网站来说是没有影响的,所以客户端并不需要等到它发送完成以后才显示注册成功,应该是写入数据库中后就可以返回。消息队列:引入消息队列以后,把发送短信,邮件等不是必须的业务逻辑进行异步处理引入消息队列以后,用户的响应时间就等于写入数据库的时间+写入消息队列的时间(可以忽略不计)流量削峰(削峰填谷)流量削峰一般在秒杀活动中应该广泛,因为秒杀活动一般会因为流量过大,导致应用挂掉,为了解决这个问题,我们一般需要加入消息队列传统模式:在下单的时候就会往数据库中添加数据,但是如果并发量过高的话,就可能导致宕机。如果说消息被MQ保存起来,然后系统可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入到数据库中,就不会导致数据库卡死了系统慢慢的按照数据库能够处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。QPS,PV,UV,PR概念QPSQPS:每秒查询率,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。因特网经常用每秒查询率来衡量域名系统服务器的机器的性能QPS=并发量/平均响应时间PV:Page View,网页浏览量。网页被读者调用浏览的次数。网页每次打开或刷新一次页面,记录一次。用户对同一页面的多次访问,访问量累计。UV (Unique Visitor,独立访客访问数)统计1天内访问某站点的用户数(以 cookie 为依据),一台电脑终端为一个访客。PR:PageRank,网页的级别技术,用来标识网页的等级/重要性。级别从1到10.PR越高,说明这个网页越受欢迎AMQP和JMSMQ是消息通信的模型,实现MQ大致有两种主流方式:AMQP、JMSAMQP是一种高级消息队列协议(Advanced Message Queuing Protocol),更准确的说是一种链接协议。这是和JMS的 本质区别,AMQP不和API层进行限定,而是直接定义网络交换的数据格式JMS即Java消息服务(JavaMessage Service)应用程序接口,是一个Java平台关于面向消息中间件(MOM)的API,用在两个应用程序之间,或者分布式系统中发送消息,进行异步通信。二者的区别JMS是定义了统一的接口,来对消息进行操作统一;AMQP是通过规定协议来统一数据交互的格式JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,是跨语言的JMS规定了两种消息模式;二AMQP的消息模式更加丰富。消息队列产品RabbitMQ核心概念 架构原理Broker :接收和分发消息的应用, RabbitMQ Server 就是 Message BrokerVirtual host: 出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似 于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出 多个 vhost ,每个用户在自己的 vhost 创建 exchange / queue 等Connection: publisher / consumer 和 broker 之间的 TCP 连接Channel :如果每一次访问 RabbitMQ 都建立一个 Connection ,在消息量大的时候建立 TCP连接 的开销将是巨大的,效率也较低。 Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯, AMQP method 包含了 channel id 帮助客 户端和 message broker 识别 channel ,所以 channel 之间是完全隔离的。 Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP 连接 的开销Exchange: message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key ,分发消息到 queue 中去。常用的类型有: direct (point-to-point), topic (publish-subscribe) and fanout(multicast)Queue: 消息最终被送到这里等待 consumer 取走Binding : exchange 和 queue 之间的虚拟连接, binding 中可以包含 routing key , Binding 信息被保 存到 exchange 中的查询表中,用于 message 的分发依据
0
0
0
浏览量2020
开着皮卡写代码

SpringBoot 项目整合 Redis 教程详解

Redis 是完全开源的,遵守 BSD 协议,是一个高性能的 key-value 数据库.Redis 与其他 key - value 缓存产品有以下三个特点:Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。Redis支持数据的备份,即master-slave模式的数据备份。Redis 的优势性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s丰富的数据类型 – Redis支持二进制案例的 String, List, Hash, Set 及 zset数据类型操作。原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性Redis 是单线程的,6.0版本开始支持开启多线程。Redis 安装下载地址: https://github.com/tporadowski/redis/releases。解压下载后的压缩文件,解压后文件列表如下:使用cmd窗口打开Redisredis-server.exe redis.windows.conf #加载配置文件启动 注:启动之后,不要关闭窗口,关闭窗口服务停止!安装Redis数据库客户端库相关指令:flushdb 清空当前库 flushall 清空所有库 select 1 切换库key的相关指令Redis 数据类型1.String(字符串)string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。操作指令:2.Hash(哈希)Redis hash 是一个键值(key=>value)对集合。Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。操作指令:3.List(列表)Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。操作指令4.Set(集合)Redis 的 Set 是 string 类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。操作指令:5.ZSet(sorted set:有序集合)Redis ZSet 和 Set 一样也是 String 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。Redis 正是通过分数来为集合中的成员进行从小到大的排序。ZSet 的成员是唯一的,但分数(score)却可以重复。操作指令:SpringBoot 操作 Redis  spring boot data redis中提供了RedisTemplate和StringRedisTemplate,其中StringRedisTemplate是Redistemplate的子类,两个方法基本一致,不同之处主要体现在操作的数据类型不同,RedisTemplate中的两个泛型都是Object,意味着存储的key和value都可以是一个对象,而StringRedisTemplate的两个泛型都是String,意味着StringRedisTemplate的key和value都只能是字符串。引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>SpringBoot 配置 Redisspring: redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接池最大连接数(使用负值表示没有限制) jedis.pool.max-active: 20 # 连接池最大阻塞等待时间(使用负值表示没有限制) jedis.pool.max-wait: -1 # 连接池中的最大空闲连接 jedis.pool.max-idle: 10 # 连接池中的最小空闲连接 jedis.pool.min-idle: 0 # 连接超时时间(毫秒) timeout: 1000RedisTemplate 及其相关方法1.RedisTemplate 介绍  Spring封装了RedisTemplate对象来进行对Redis的各种操作,它支持所有的Redis原生的api。RedisTemplate位于spring-data-redis包下。RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅。2.Redis 5种数据结构操作redisTemplate.opsForValue(); //操作字符串redisTemplate.opsForHash(); //操作hashredisTemplate.opsForList(); //操作listredisTemplate.opsForSet(); //操作setredisTemplate.opsForZSet(); //操作有序set或者:redistempalate.boundValueOpsredistempalate.boundSetOpsredistempalate.boundListOpsredistempalate.boundHashOpsredistempalate.boundZSetOpsopsForXXX和boundXXXOps的区别:XXX为value的类型,前者获取一个operator,但是没有指定操作的对象(key),可以在一个连接(事务)内操作多个key以及对应的value;后者获取了一个指定操作对象(key)的operator,在一个连接(事务)内只能操作这个key对应的value。SpringBootTest 实现Redis数据库增删改查/** * 使用RedisTemplate 操作Redis数据的不同数据类型 */ @SpringBootTest public class Springbootday03ApplicationTests { @Autowired private RedisTemplate<String, String> redisTemplate; /** * String 类型数据操作 */ @Test public void operateString() { //添加值 redisTemplate.opsForValue().set("str", "strValue1"); //添加值 判定是否存在 存在则不添加 Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("str", "strAbsent"); System.out.println("str设置成功:" + aBoolean); //获取值 String str = redisTemplate.opsForValue().get("str"); System.out.println("str = " + str); //更新值 redisTemplate.opsForValue().set("str", "strValue2"); str = redisTemplate.opsForValue().get("str"); System.out.println("newStr = " + str); //删除值 Boolean b = redisTemplate.delete("str"); System.out.println("str删除成功:" + b); } /** * 操作string类型数据 设置过期时间 */ @Test public void operateString2() { redisTemplate.opsForValue().set("str", "strTimeout", 10, TimeUnit.SECONDS); //判定值是否存在 不存在则设置值 同时设置过期时间 Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("str2", "strTimeoutAbsent", 20, TimeUnit.SECONDS); System.out.println("setIfAbsent:" + aBoolean); } /** * 操作hash类型数据 */ @Test public void operateHash() { //添加hash类型数据 key - value redisTemplate.opsForHash().put("hash", "username", "admin"); //修改hash类型数据 redisTemplate.opsForHash().put("hash", "username", "tom"); redisTemplate.opsForHash().put("hash", "password", "123456"); //添加hash类型数据 key - map HashMap<String, String> map = new HashMap<>(); map.put("driverName", "com.mysql.jdbc.Driver"); map.put("url", "jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC"); redisTemplate.opsForHash().putAll("hash", map); //获取hash类型数据 entries Map<Object, Object> hash = redisTemplate.opsForHash().entries("hash"); hash.forEach((key, value) -> { System.out.println(key + "::" + value); }); //获取所有的key Set<Object> keys = redisTemplate.opsForHash().keys("hash"); for (Object key : keys) { System.out.println("key:" + key); } //获取所有value List<Object> values = redisTemplate.opsForHash().values("hash"); values.forEach(value -> System.out.println("value:" + value)); //删除hash类型数据 删除一个 返回删除的个数 Long delete = redisTemplate.opsForHash().delete("hash", "username"); System.out.println("delete = " + delete); //删除hash类型数据 删除多个 返回删除的个数 delete = redisTemplate.opsForHash().delete("hash", "username", "password", "driverName"); System.out.println("delete = " + delete); //删除hash类型数据 删除所有 Boolean delHash = redisTemplate.delete("hash"); System.out.println("delHah:" + delHash); } /** * 操作List类型 有序 可重复 */ @Test public void operateList() { //左压栈 // redisTemplate.opsForList().leftPush("list", "listValue1"); // redisTemplate.opsForList().leftPush("list", "listValue1"); // redisTemplate.opsForList().leftPush("list", "listValue2"); // redisTemplate.opsForList().leftPush("list", "listValue3"); //右压栈 redisTemplate.opsForList().rightPush("list", "listValue0"); redisTemplate.opsForList().rightPush("list", "listValue2"); redisTemplate.opsForList().rightPush("list", "listValue0"); //左出栈 String list1 = redisTemplate.opsForList().leftPop("list"); System.out.println("leftPop list1 = " + list1); //右出栈 String list2 = redisTemplate.opsForList().rightPop("list"); System.out.println("rightPop list2 = " + list2); //获取所有数据 List<String> lists = redisTemplate.opsForList().range("list", 0, redisTemplate.opsForList().size("list") - 1); lists.forEach(list -> System.out.println(list)); //设置指定位置的数据 redisTemplate.opsForList().set("list", 0, "listValue0"); /** * 从存储在键中的列表中删除等于值的元素的第一个计数事件。 * count> 0:删除等于从左到右移动的值的第一个元素; * count< 0:删除等于从右到左移动的值的第一个元素; * count = 0:删除等于value的所有元素。 */ Long remove = redisTemplate.opsForList().remove("list", -1, "listValue0"); System.out.println("remove:" + remove); //删除指定key的list数据 Boolean list = redisTemplate.delete("list"); System.out.println("list集合删除成功:" + list); } /** * 操作Set类型 无序 不可重复 */ @Test public void operateSet() { //设置set值 redisTemplate.opsForSet().add("set", "setValue0"); redisTemplate.opsForSet().add("set", "setValue0"); redisTemplate.opsForSet().add("set", "setValue1"); //判定是否包含 Boolean member = redisTemplate.opsForSet().isMember("set", "setValue0"); System.out.println("isMember:" + member); //删除set中的值 Long remove = redisTemplate.opsForSet().remove("set", "setValue0"); System.out.println("remove = " + remove); //获取set类型值 Set<String> set = redisTemplate.opsForSet().members("set"); set.forEach(str -> { System.out.println("str = " + str); }); } /** * 操作 ZSet 有序 不可重复 */ @Test public void operateZSet() { //存储值 Boolean add = redisTemplate.opsForZSet().add("zset", "zsetValue0", 10); System.out.println("add = " + add); System.out.println("add = " + add); add = redisTemplate.opsForZSet().add("zset", "zsetValue2", 2); System.out.println("add = " + add); //获取值 // Boolean zset = redisTemplate.delete("zset"); // System.out.println("delete zset = " + zset); } }Redis工具类的封装/** * Redis 工具类 * @author mosin * date 2021/11/30 * @version 1.0 */ @Component public final class RedisUtil { private RedisUtil(){}; @Autowired private RedisTemplate<String,String> redisTemplate; //设置值 public void setValue(String key,String value){ redisTemplate.opsForValue().set(key, value); } // 设置值 同时设置有效时间 public void setValue(String key, String value, Long timeOut, TimeUnit timeUnit){ redisTemplate.opsForValue().setIfAbsent(key, value, timeOut, timeUnit); } //设置值 没有则设置 有则不设置 public void setNx(String key,String value){ redisTemplate.opsForValue().setIfAbsent(key, value); } //设置值 没有则设置 同时设置有效时间 有则不设置 public void setNx(String key,String value,long timeOut,TimeUnit timeUnit){ redisTemplate.opsForValue().setIfAbsent(key, value,timeOut,timeUnit); } //删除值 public boolean del(String key){ return redisTemplate.delete(key); } //获取值 public String getValue(String key){ return redisTemplate.opsForValue().get(key); } }Redis 业务实践redis 存储 token,实现非法请求拦截1.编写拦截器@Component public class AdminInterceptor implements HandlerInterceptor { @Autowired private RedisUtil redisUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("拦截器以拦截请求"); //从请求头中获取token 验证用户是否登录 String token = request.getHeader("token"); System.out.println(token); String tokenValue = redisUtil.getValue(token); System.out.println("tokenValue = " + tokenValue); if(tokenValue!=null){ //用户已登录 放行请求 return true; }else{//重定向到登录页面 response.sendRedirect(request.getContextPath()+"/login.jsp"); return false; } } }2.配置拦截器@Configuration public class LoginConfig implements WebMvcConfigurer { @Autowired private AdminInterceptor adminInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { InterceptorRegistration registration = registry.addInterceptor(adminInterceptor); registration.addPathPatterns("/**"); registration.excludePathPatterns("/user/login","/user/register","/login.jsp"); } }3.编写统一返回数据格式类@Data @AllArgsConstructor @NoArgsConstructor @Builder public class JsonResult<T> { private Integer code; private String msg; private Long count; private T data; }4.编写控制器@Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @Autowired private RedisUtil redisUtil; @ResponseBody @RequestMapping("/login") public Object login(User user) throws JsonProcessingException { User usr = User.builder().id(1).name("admin").password("123456").build(); //获取token 放入redis String token = UUID.randomUUID().toString().replace("-", ""); //将user 转为json格式放入 redis ObjectMapper objectMapper = new ObjectMapper(); String s1 = objectMapper.writeValueAsString(usr); //将 token 和用户信息存入 redis redisUtil.setValue(token, s1, 2L, TimeUnit.MINUTES); //将token 存入map集合返回 HashMap<String, String> map = new HashMap<>(); map.put("token", token); return map; } @ResponseBody @RequestMapping("/register") public Object register(User user){ HashMap<String, String> map = new HashMap<>(); map.put("msg", "ok"); return map; } @ResponseBody @RequestMapping("/add") public Object add(User user){ HashMap<String, String> map = new HashMap<>(); map.put("msg", "ok"); return map; } }5.编写业务类和Mapper接口6.使用postman接口测试工具测试接口
0
0
0
浏览量2011
开着皮卡写代码

RabbitMQ:简单模式(Hello World)

基本介绍先来看看RabbitMQ架构图Broker :接收和分发消息的应用, RabbitMQ Server 就是 Message BrokerVirtual host: 出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似 于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出 多个 vhost ,每个用户在自己的 vhost 创建 exchange / queue 等Connection: publisher / consumer 和 broker 之间的 TCP 连接Channel :如果每一次访问 RabbitMQ 都建立一个 Connection ,在消息量大的时候建立 TCP连接 的开销将是巨大的,效率也较低。 Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯, AMQP method 包含了 channel id 帮助客 户端和 message broker 识别 channel ,所以 channel 之间是完全隔离的。 Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP 连接 的开销Exchange: message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key ,分发消息到 queue 中去。常用的类型有: direct (point-to-point), topic (publish-subscribe) and fanout(multicast)Queue: 消息最终被送到这里等待 consumer 取走Binding : exchange 和 queue 之间的虚拟连接, binding 中可以包含 routing key , Binding 信息被保 存到 exchange 中的查询表中,用于 message 的分发依据简单模式生产者我们先创建一个maven工程,然后引入相关依赖<dependencies> <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.16.0</version> <scope>compile</scope> </dependency> </dependencies>public class Producer { //队列名称 static final String QUEUE_NAME = "helo-queue"; public static void main(String[] args) { try { //1.创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //设置连接参数 //服务器IP地址 factory.setHost("192.168.88.133"); //连接端口 factory.setPort(5672); //设置连接的虚拟机名称 factory.setVirtualHost("/myhost"); //用户名 factory.setUsername("admin"); //密码 factory.setPassword("123456"); //2.创建Connection对象 Connection connection = factory.newConnection(); //3.创建信道对象 Channel channel = connection.createChannel(); //4.声明队列(队列名称,是否持久化,是否独占连接,是否在不适用队列的时候自动删除,队列其他参数) channel.queueDeclare(QUEUE_NAME, true, false, false, null); //5.准备发送信息 String msg="hello rabbitmq!!!!!"; /** * 参数1:交换机名称,不填写交换机名称的话则使用默认的交换机 * 参数2:队列名称(路由key) * 参数3:其他参数 * 参数4:消息内容 */ channel.basicPublish("", QUEUE_NAME, null, msg.getBytes()); System.out.println("已经发送消息到队列"); // 关闭资源 channel.close(); connection.close(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }然后我们运行程序,在浏览器输入服务器地址以及对应端口15672,我们可以看到队列以及发送的消息消费者独占队列意思是只有一个连接可以操作改队列public class Consumer { //队列名称 static final String QUEUE_NAME = "helo-queue"; public static void main(String[] args) { try { //1.创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //设置连接参数 //服务器IP地址 factory.setHost("192.168.88.133"); //连接端口 factory.setPort(5672); //设置连接的虚拟机名称 factory.setVirtualHost("/myhost"); //用户名 factory.setUsername("admin"); //密码 factory.setPassword("a87684009."); //2.创建Connection对象 Connection connection = factory.newConnection(); //3.创建信道对象 Channel channel = connection.createChannel(); //4.声明队列(队列名称,是否持久化,是否独占连接,是否在不使用队列的时候自动删除,队列其他参数) channel.queueDeclare(QUEUE_NAME, true, false, false, null); //5.接收消息 DefaultConsumer consumer=new DefaultConsumer(channel){ /** * 消费回调函数,当收到消息以后,会自动执行这个方法 * @param consumerTag 消费者标识 * @param envelope 消息包的内容(比如交换机,路由key,消息id等) * @param properties 属性信息 * @param body 消息数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消息内容body:"+new String(body,"UTF-8")); } }; //监听消息(队列名称,是否自动确认消息,消费对象) channel.basicConsume(QUEUE_NAME,true,consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }消费者不能关闭连接,生产者可以关闭连接简单模式总结P:生产者,也就是发送消息的程序C:消费者,也就是消息的接收着,会一直等待消息的发送,不能关闭连接Queue:消息队列,类似邮箱,可以缓存消息。生产者向其投递消息,消费者从中取出消息。
0
0
0
浏览量2010
开着皮卡写代码

RabbitMQ工作模式

Work queues工作队列模式基本介绍多个消费者共同消费同一个队列中的消息,可以实现快速消费,避免消息积压,但是多个消费者消费队列的消息的时候,是互斥的,同一个消息只能被一个消费者消费,不可以被多个消费者消费。应用:对于一些任务比较多的情况,使用工作队列可以提高任务处理的速度编写代码抽取公共部分,写一个工具类我们知道像连接等操作,其实基本上代码都是一样的,每一次写重复的代码其实没有什么意义,我们可以写一个工具类来封装这些操作。public class ConnectionUtil { public static Connection getConnection() throws Exception { //定义连接工厂 ConnectionFactory factory = new ConnectionFactory(); //设置服务地址 factory.setHost("192.168.137.118"); //端口 factory.setPort(5672); //设置账号信息,用户名、密码、vhost factory.setVirtualHost("/"); factory.setUsername("admin"); factory.setPassword("123456"); // 通过工程获取连接 Connection connection = factory.newConnection(); return connection; }生产者public class Producer { static final String QUEUE_NAME = "work_queue"; public static void main(String[] args) throws Exception { Connection connection = ConnectionUtil.getConnection(); Channel channel = connection.createChannel(); channel.queueDeclare(QUEUE_NAME,true,false,false,null); for (int i = 1; i <= 10; i++) { String body = i+"hello rabbitmq~~~"; channel.basicPublish("",QUEUE_NAME,null,body.getBytes()); } channel.close(); connection.close(); } }消费者1public class Consumer1 { static final String QUEUE_NAME = "work_queue"; public static void main(String[] args) throws Exception { Connection connection = ConnectionUtil.getConnection(); Channel channel = connection.createChannel(); channel.queueDeclare(QUEUE_NAME,true,false,false,null); Consumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("body:"+new String(body)); } }; channel.basicConsume(QUEUE_NAME,true,consumer); } }消费者2代码和上面相同测试我们先启动两个消费者,然后再启动生产者发送消息,到IDEA的两个消费者对应的控制台查看是否竞争性的接收到消息。发布订阅模式订阅模式类型P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)C:消费者,消息的接受者,会一直等待消息到来。Queue:消息队列,接收消息、缓存消息。Exchange:交换机,图中的X。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特定队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:Fanout:广播,将消息交给所有绑定到交换机的队列Direct:定向,把消息交给符合指定routing key 的队列Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列发布订阅模式每个消费者都监听自己的队列生产者把消息发送给broker,然后交换机把消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息。交换机只负责转发消息,并没有存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!代码编写生产者public class Producer { public static void main(String[] args) throws Exception { Connection connection = ConnectionUtil.getConnection(); Channel channel = connection.createChannel(); /* exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments) 参数: 1. exchange:交换机名称 2. type:交换机类型 DIRECT("direct"),:定向 FANOUT("fanout"),:扇形(广播),发送消息到每一个与之绑定队列。 TOPIC("topic"),通配符的方式 HEADERS("headers");参数匹配 3. durable:是否持久化 4. autoDelete:自动删除 5. internal:内部使用。 一般false 6. arguments:参数 */ String exchangeName = "test_fanout"; //5. 创建交换机 channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT,true,false,false,null); //6. 创建队列 String queue1Name = "test_fanout_queue1"; String queue2Name = "test_fanout_queue2"; channel.queueDeclare(queue1Name,true,false,false,null); channel.queueDeclare(queue2Name,true,false,false,null); //7. 绑定队列和交换机 /* queueBind(String queue, String exchange, String routingKey) 参数: 1. queue:队列名称 2. exchange:交换机名称 3. routingKey:路由键,绑定规则 如果交换机的类型为fanout ,routingKey设置为"" */ channel.queueBind(queue1Name,exchangeName,""); channel.queueBind(queue2Name,exchangeName,""); String body = "日志信息:张三调用了findAll方法...日志级别:info..."; //8. 发送消息 channel.basicPublish(exchangeName,"",null,body.getBytes()); //9. 释放资源 channel.close(); connection.close(); } }消费者1public class Consumer1 { public static void main(String[] args) throws Exception { Connection connection = ConnectionUtil.getConnection(); Channel channel = connection.createChannel(); String queue1Name = "test_fanout_queue1"; Consumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("body:"+new String(body)); System.out.println("将日志信息打印到控制台....."); } }; channel.basicConsume(queue1Name,true,consumer); } }消费者2public class Consumer2 { public static void main(String[] args) throws Exception { Connection connection = ConnectionUtil.getConnection(); Channel channel = connection.createChannel(); String queue2Name = "test_fanout_queue2"; Consumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("body:"+new String(body)); System.out.println("将日志信息打印到控制台....."); } }; channel.basicConsume(queue2Name,true,consumer); } }测试启动所有消费者,然后使用生产者发送消息;在每个消费者对应的控制台可以查看到生产者发送的所有消息;达到广播的效果。在执行完测试代码后,其实到RabbitMQ的管理后台找到Exchanges选项卡,点击 fanout_exchange 的交换机,可以查看到如下的绑定:发布订阅模式和工作队列模式的区别工作队列模式不需要定义交换机,而发布订阅模式需要定义交换机发布订阅模式需要设置队列和交换机的绑定,而工作队列模式不需要设置,事实上是因为发布订阅模式中,生产者向交换机发送消息;工作队列模式则是生产者向队列发送消息(底层使用默认交换机)Routing 路由模式基本介绍P:生产者,向交换机发送消息的时候,会指定一个routing keyX:Exchange(交换机),接收生产者的消息,然后把消息传递给和routing key完全匹配的队列C1:消费者,它所在队列指定了需要routing key为error的信息C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息路由模式的特点队列和交换机的绑定是需要指定routing key的,不可以随意绑定消息的发送方向交换机发送消息的时候,也需要指定消息的routing key交换机不再把消息交给每一个绑定的队列,而是根据消息的routing key来进行判断,只有队列的routing key和消息的routing key完全一样才会接收到消息。编写代码生产者public class Producer { public static void main(String[] args) throws Exception { Connection connection = ConnectionUtil.getConnection(); Channel channel = connection.createChannel(); String exchangeName = "test_direct"; // 创建交换机 channel.exchangeDeclare(exchangeName,BuiltinExchangeType.DIRECT,true,false,false,null); // 创建队列 String queue1Name = "test_direct_queue1"; String queue2Name = "test_direct_queue2"; // 声明(创建)队列 channel.queueDeclare(queue1Name,true,false,false,null); channel.queueDeclare(queue2Name,true,false,false,null); // 队列绑定交换机 // 队列1绑定error channel.queueBind(queue1Name,exchangeName,"error"); // 队列2绑定info error warning channel.queueBind(queue2Name,exchangeName,"info"); channel.queueBind(queue2Name,exchangeName,"error"); channel.queueBind(queue2Name,exchangeName,"warning"); String message = "日志信息:张三调用了delete方法.错误了,日志级别warning"; // 发送消息 channel.basicPublish(exchangeName,"warning",null,message.getBytes()); System.out.println(message); channel.close(); connection.close(); } }消费者1public class Consumer1 { public static void main(String[] args) throws Exception { Connection connection = ConnectionUtil.getConnection(); Channel channel = connection.createChannel(); String queue1Name = "test_direct_queue1"; Consumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("body:"+new String(body)); System.out.println("将日志信息打印到控制台....."); } }; channel.basicConsume(queue1Name,true,consumer); } }消费者2public class Consumer2 { public static void main(String[] args) throws Exception { Connection connection = ConnectionUtil.getConnection(); Channel channel = connection.createChannel(); String queue2Name = "test_direct_queue2"; Consumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("body:"+new String(body)); System.out.println("将日志信息存储到数据库....."); } }; channel.basicConsume(queue2Name,true,consumer); } }Topics 通配符模式基本介绍Topic类型与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert通配符规则:#:匹配0个或者多个词*:刚好可以匹配一个词代码生产者public class Producer { public static void main(String[] args) throws Exception { Connection connection = ConnectionUtil.getConnection(); Channel channel = connection.createChannel(); String exchangeName = "test_topic"; channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC,true,false,false,null); String queue1Name = "test_topic_queue1"; String queue2Name = "test_topic_queue2"; channel.queueDeclare(queue1Name,true,false,false,null); channel.queueDeclare(queue2Name,true,false,false,null); // 绑定队列和交换机 /** * 参数: 1. queue:队列名称 2. exchange:交换机名称 3. routingKey:路由键,绑定规则 如果交换机的类型为fanout ,routingKey设置为"" */ // routing key 系统的名称.日志的级别。 //需求: 所有error级别的日志存入数据库,所有order系统的日志存入数据库 channel.queueBind(queue1Name,exchangeName,"#.error"); channel.queueBind(queue1Name,exchangeName,"order.*"); channel.queueBind(queue2Name,exchangeName,"*.*"); String body = "日志信息:张三调用了findAll方法...日志级别:info..."; //发送消息goods.info,goods.error channel.basicPublish(exchangeName,"order.info",null,body.getBytes()); channel.close(); connection.close(); } }消费者1public class Consumer1 { public static void main(String[] args) throws Exception { Connection connection = ConnectionUtil.getConnection(); Channel channel = connection.createChannel(); String queue1Name = "test_topic_queue1"; Consumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("body:"+new String(body)); } }; channel.basicConsume(queue1Name,true,consumer); } }消费者2public class Consumer2 { public static void main(String[] args) throws Exception { Connection connection = ConnectionUtil.getConnection(); Channel channel = connection.createChannel(); String queue2Name = "test_topic_queue2"; Consumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("body:"+new String(body)); } }; channel.basicConsume(queue2Name,true,consumer); } }
0
0
0
浏览量2013
开着皮卡写代码

RabbitMQ:路由模式

1.基本介绍在路由工作模式中,我们需要配置一个类型为direct的交换机,并且需要指定不同的路由键(routing key),把对应的消息从交换机路由到不同的消息队列进行存储,由消费者进行消费。P:生产者,向交换机发送消息的时候,会指定一个routing keyX:Exchange(交换机),接收生产者的消息,然后把消息传递给和routing key完全匹配的队列C1:消费者,它所在队列指定了需要routing key为error的信息C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息路由模式的特点队列和交换机的绑定是需要指定routing key的,不可以随意绑定消息的发送方向交换机发送消息的时候,也需要指定消息的routing key交换机不再把消息交给每一个绑定的队列,而是根据消息的routing key来进行判断,只有队列的routing key和消息的routing key完全一样才会接收到消息。2.生产者public class Producer { public static String DIRECT_EXCHANGE = " direct_exchange"; public static String DIRECT_QUEUE_1 = "direct_queue_1"; public static String DIRECT_QUEUE_2 = "direct_queue_2"; public static void main(String[] args) { try { Channel channel = ConnectUtil.getChannel(); //声明交换机(交换机名称,交换机类型) channel.exchangeDeclare(DIRECT_EXCHANGE, BuiltinExchangeType.DIRECT); //声明队列 channel.queueDeclare(DIRECT_QUEUE_1,true,false,false,null); channel.queueDeclare(DIRECT_QUEUE_2,true,false,false,null); //把交换机和队列1进行绑定 channel.queueBind(DIRECT_QUEUE_1,DIRECT_EXCHANGE,"error"); //把交换机和队列2进行绑定 channel.queueBind(DIRECT_QUEUE_2,DIRECT_EXCHANGE,"info"); channel.queueBind(DIRECT_QUEUE_2,DIRECT_EXCHANGE,"error"); channel.queueBind(DIRECT_QUEUE_2,DIRECT_EXCHANGE,"warning"); //发送消息 String msg="日志信息:调用了xxx方法,日志级别是info"; channel.basicPublish(DIRECT_EXCHANGE,"info",null,msg.getBytes()); System.out.println("消息发送成功"); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } 3.消费者消费者1public class Consumer1 { public static void main(String[] args) { try { //获取信道对象 Channel channel = ConnectUtil.getChannel(); //消费消息 DefaultConsumer consumer=new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消费者1接收到消息:"+new String(body,"UTF-8")); System.out.println("消费者1把日志信息保存到数据库"); } }; channel.basicConsume(Producer.DIRECT_QUEUE_1,true,consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }消费者2public class Consumer2 { public static void main(String[] args) { try { //获取信道对象 Channel channel = ConnectUtil.getChannel(); //消费消息 DefaultConsumer consumer=new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消费者2接收到消息:"+new String(body,"UTF-8")); System.out.println("消费者2把日志信息输出到控制台"); } }; channel.basicConsume(Producer.DIRECT_QUEUE_2,true,consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }4.测试第一次测试,发送日志级别为info的信息第二次测试,发送日志级别为error的信息
0
0
0
浏览量2010
开着皮卡写代码

RabbitMQ:发布确认模式

1.基本介绍生产者把信道设置成为confirm(确认)模式,一旦信道进入confirm模式,所有在这个信道上面发布的消息都会被指定唯一的一个ID(ID从1开始).一旦消息被投递到所有匹配的队列以后,broker就会发送一个确认给生产者(包含ID),这样使得生产者知道消息已经正确到底目的队列了。如果消息和队列是可持久化的,那么确认消息就会在消息被写入磁盘以后发出,broker回传给生产者的确认消息中delivery-tag包含了确认消息的序列号。2.实现消息可靠传递的三个条件2.1队列持久化生产者发送消息到队列的时候,把durable参数设置为true(表示队列持久化) // 参数1 queue :队列名 // 参数2 durable :是否持久化 // 参数3 exclusive :仅创建者可以使用的私有队列,断开后自动删除 // 参数4 autoDelete : 当所有消费客户端连接断开后,是否自动删除队列 // 参数5 arguments channel.queueDeclare(QUEUE_NAME, true, false, false, null);2.2消息持久化我们需要将消息标记为持久性 - 通过将消息属性(实现基本属性)设置为PERSISTENT_TEXT_PLAIN的值。//交换机名称,队列名称,消息持久化,消息 channel.basicPublish("", "task_queue", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());2.3发布确认队列接收到生产者发送的数据以后,队列把消息保存在磁盘(为了实现持久化),队列会把最终的可靠性传递结果告诉给生产者,这就是发布确认。三种常用的发布确认策略:单个确认发布、批量确认发布、异步确认发布3.发布确认模式RabbitMQ的发布确认模式默认是没有开启的,我们可以通过调用channel.confirmSelect()方法来手动开启发布确认模式。3.1单个确认发布模式单个确认发布模式是一种简单的同步确认发布的方式。也就是说发布一个消息以后,只要确认它被确认发布,才可以继续发布后续的消息。waitForConfirms(long)这一个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认,就会抛出异常。缺点:速度慢,因为如果没有确认消息的话,后面的消息都会被阻塞public class ConfirmMessage { //消息数量 public static final int MSG_CNT=200; public static void main(String[] args) { //调用单个确认发布方法 confirmSingleMessage(); } public static void confirmSingleMessage() { try { //获取信道对象 Channel channel = ConnectUtil.getChannel(); //开启确认发布 channel.confirmSelect(); //声明队列 String queue = UUID.randomUUID().toString(); //队列持久化 channel.queueDeclare(queue, true, false, false, null); //发送消息 long start= System.currentTimeMillis(); for (int i = 0; i < MSG_CNT; i++) { String msg="消息:"+i; //发送消息,消息需要持久化 channel.basicPublish("", queue, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes()); //服务端返回false或者在超时时间内没有返回数据,生产者可以重新发送消息 boolean flag=channel.waitForConfirms(); if (flag){ System.out.println("————————第"+(i+1)+"条消息发送成功————————"); }else { System.out.println("========第"+(i+1)+"条消息发送失败========="); } } //记录结束时间 long end=System.currentTimeMillis(); System.out.println("发布:"+MSG_CNT+"个单独确认消息,耗时:"+(end-start)+"毫秒"); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }3.2批量确认发布模式先发布一批信息然后一起确认可以大大提高吞吐量缺点:当故障发生的时候,我们不知道是哪一个消息出现了问题,我们需要把整个批处理保存在内存中,记录重要的信息后重新发布消息这种方案仍然是同步的方式,会阻塞消息的发布public class ConfirmMessage { //消息数量 public static final int MSG_CNT = 200; public static void main(String[] args) { //调用单个确认发布方法 //confirmSingleMessage();//发布:200个单独确认消息,耗时:192毫秒 confirmBatchMessage(); } public static void confirmSingleMessage() { try { //获取信道对象 Channel channel = ConnectUtil.getChannel(); //开启确认发布 channel.confirmSelect(); //声明队列 String queue = UUID.randomUUID().toString(); //队列持久化 channel.queueDeclare(queue, true, false, false, null); //发送消息 long start = System.currentTimeMillis(); for (int i = 0; i < MSG_CNT; i++) { String msg = "消息:" + i; //发送消息,消息需要持久化 channel.basicPublish("", queue, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes()); //服务端返回false或者在超时时间内没有返回数据,生产者可以重新发送消息 boolean flag = channel.waitForConfirms(); if (flag) { System.out.println("————————第" + (i + 1) + "条消息发送成功————————"); } else { System.out.println("========第" + (i + 1) + "条消息发送失败========="); } } //记录结束时间 long end = System.currentTimeMillis(); System.out.println("发布:" + MSG_CNT + "个单独确认消息,耗时:" + (end - start) + "毫秒"); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void confirmBatchMessage() { try { //获取信道对象 Channel channel = ConnectUtil.getChannel(); //开启确认发布 channel.confirmSelect(); //批量确认消息数量 int batchSize=20; //未确认消息数量 int nackMessageCount=0; //声明队列 String queue = UUID.randomUUID().toString(); //队列持久化 channel.queueDeclare(queue, true, false, false, null); //发送消息 long start = System.currentTimeMillis(); for (int i = 0; i < MSG_CNT; i++) { String msg = "消息:" + i; //发送消息,消息需要持久化 channel.basicPublish("", queue, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes()); //累加未确认的发布数量 nackMessageCount++; //判断的未确认消息数量和批量确认消息的数量是否一致 if (nackMessageCount==batchSize){ //服务端返回false或者在超时时间内没有返回数据,生产者可以重新发送消息 boolean flag = channel.waitForConfirms(); if (flag) { System.out.println("————————第" + (i + 1) + "条消息发送成功————————"); } else { System.out.println("========第" + (i + 1) + "条消息发送失败========="); } //清空未确认发布消息个数 nackMessageCount=0; } } //为了确认剩下的是没有确认的消息,所以要再次进行确认 if (nackMessageCount>0){ //再次重新确认 channel.waitForConfirms(); } //记录结束时间 long end = System.currentTimeMillis(); System.out.println("发布:" + MSG_CNT + "个单独确认消息,耗时:" + (end - start) + "毫秒"); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }3.3异步确认发布模式 //异步消息发布确认 public static void publishMessageAsync() throws Exception { Channel channel = ConnectUtil.getChannel(); //声明队列,此处使用UUID作为队列的名字 String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName, false, false, false, null); //开启发布确认模式 channel.confirmSelect(); //创建ConcurrentSkipListMap集合(跳表集合) ConcurrentSkipListMap<Long, String> concurrentSkipListMap = new ConcurrentSkipListMap<>(); //确认收到消息回调函数 ConfirmCallback ackCallBack = new ConfirmCallback() { @Override public void handle(long deliveryTag, boolean multiple) throws IOException { //判断是否批量异步确认 if (multiple) { //把集合中没有被确认的消息添加到该集合中 ConcurrentNavigableMap<Long, String> confirmed = concurrentSkipListMap.headMap(deliveryTag, true); //清除该部分没有被确认的消息 confirmed.clear(); } else { //只清除当前序列胡的消息 concurrentSkipListMap.remove(deliveryTag); } System.out.println("确认的消息序列序号:" + deliveryTag); } }; //未被确认消息的回调函数 ConfirmCallback nackCallBack = new ConfirmCallback() { @Override public void handle(long deliveryTag, boolean multiple) throws IOException { //获取没有被确认的消息 String msg = concurrentSkipListMap.get(deliveryTag); System.out.println("发布的消息:" + msg + "未被确认,该消息序列号:" + deliveryTag); } }; //添加异步确认监听器 channel.addConfirmListener(ackCallBack, nackCallBack); //记录开始时间 long start = System.currentTimeMillis(); //循环发送消息 for (int i = 0; i < MSG_CNT; i++) { //消息内容 String message = "消息:" + i; //把未确认的消息放到集合中,通过序列号和消息进行关联 // channel.getNextPublishSeqNo(); 获取下一个消息的序列号 concurrentSkipListMap.put(channel.getNextPublishSeqNo(), message); //发送消息 channel.basicPublish("", queueName, null, message.getBytes()); } //记录结束时间 long end = System.currentTimeMillis(); System.out.println("发布"+MSG_CNT+"个批量确认消息,一共耗时:"+(end-start)+"毫秒"); }
0
0
0
浏览量2012
开着皮卡写代码

Spring Boot 整合 Swagger 教程详解

Spring Boot 是一个基于 Spring 框架的轻量级开源框架,它的出现极大地简化了 Spring 应用的搭建和开发。在开发过程中,接口文档是非常重要的一环,它不仅方便开发者查看和理解接口的功能和参数,还能帮助前后端开发协同工作,提高开发效率。本文将介绍如何在 Spring Boot 中使用 Swagger 来实现接口文档的自动生成。一、关于 Swagger  Swagger 是一个 RESTful 接口文档的规范和工具集,它的目标是统一 RESTful 接口文档的格式和规范。在开发过程中,接口文档是非常重要的一环,它不仅方便开发者查看和理解接口的功能和参数,还能帮助前后端开发协同工作,提高开发效率。在 Spring Boot 中,我们可以通过集成 Swagger 来实现接口文档的自动生成。Swagger 通过注解来描述接口,然后根据这些注解自动生成接口文档。二、Swagger 的安装1、下载 Swagger  Swagger 的官方网站是 https://swagger.io/,我们可以在这里下载最新版本的 Swagger。2、安装 Swagger  安装 Swagger 非常简单,只需要将下载的 Swagger 解压到指定目录即可。在解压后的目录中,我们可以找到 swagger-ui.html 页面,这个页面就是 Swagger 的 UI 界面。三、Swagger 的使用1、编写接口  在编写接口时,我们需要使用 Swagger 的注解来描述接口信息。常用的注解包括:@Api:用于描述接口的类或接口@ApiOperation:用于描述接口的方法@ApiParam:用于描述接口的参数@ApiModel:用于描述数据模型@ApiModelProperty:用于描述数据模型的属性  例如,我们编写一个简单的接口:@RestController @Api(tags = "用户接口") public class UserController { @GetMapping("/user/{id}") @ApiOperation(value = "根据 ID 获取用户信息") public User getUserById(@ApiParam(value = "用户 ID", required = true) @PathVariable Long id) { // 根据 ID 查询用户信息 } } 在上面的代码中,@Api表示该类是一个用户接口,@ApiOperation 表示该方法是获取用户信息的接口,@ApiParam 表示该参数是用户 ID,@PathVariable 表示该参数是路径参数。2、启用 Swagger  在 Spring Boot 中,我们可以通过添加 Swagger 相关的依赖来启用 Swagger。我们可以在 pom.xml 文件中添加以下依赖:<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> 在 Spring Boot 中,我们还需要添加配置类来配置 Swagger。配置类的代码如下:@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("com.example.demo.controller")) .paths(PathSelectors.any()) .build() .apiInfo(apiInfo()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("接口文档") .description("接口文档") .version("1.0.0") .build(); } } 在上面的代码中,@Configuration 表示该类是一个配置类,@EnableSwagger2 表示启用 Swagger。在 api() 方法中,我们通过 `select() 方法配置扫描的包路径,paths() 方法配置接口的访问路径,apiInfo() 方法配置接口文档的相关信息。3、查看接口文档 启动 Spring Boot 应用后,我们可以在浏览器中访问 http://localhost:8080/swagger-ui.html 来查看接口文档。在 Swagger UI 页面中,我们可以看到所有的接口信息,包括接口名称、请求方式、请求路径、请求参数、响应参数等。四、Swagger 的高级使用1、描述数据模型 我们可以使用 @ApiModel 和 @ApiModelProperty 注解来描述数据模型和属性。例如,我们可以编写一个 User 类,并在类上使用 @ApiModel 和@ApiModelProperty 注解来描述该数据模型:@ApiModel(description = "用户信息") public class User { @ApiModelProperty(value = "用户 ID", example ="1") private Long id; @ApiModelProperty(value = "用户名", example = "张三") private String username; @ApiModelProperty(value = "密码", example = "123456") private String password; // 省略 getter 和 setter 方法 }在上面的代码中,@ApiModel 表示该类是一个数据模型,@ApiModelProperty 表示该属性是数据模型的一个属性,value 属性表示属性的描述,example 属性表示属性的示例值。2、描述枚举类型  我们可以使用 @ApiModel 和 @ApiModelProperty 注解来描述枚举类型。例如,我们可以编写一个 Gender 枚举类型,并在枚举值上使用 @ApiModelProperty 注解来描述该枚举值:@ApiModel(description = "性别") public enum Gender { @ApiModelProperty(value = "男") MALE, @ApiModelProperty(value = "女") FEMALE; }在上面的代码中,@ApiModel 表示该枚举类型是一个描述性别的枚举类型,@ApiModelProperty 表示该枚举值是描述男性的枚举值或描述女性的枚举值。3、描述响应参数  我们可以使用 @ApiResponses 和 @ApiResponse 注解来描述接口的响应参数。例如,我们可以编写一个 getUserById() 方法,并在方法上使用 @ApiResponses 和 @ApiResponse 注解来描述该方法的响应参数:@GetMapping("/user/{id}") @ApiOperation(value = "根据 ID 获取用户信息") @ApiResponses({ @ApiResponse(code = 200, message = "请求成功", response = User.class), @ApiResponse(code = 404, message = "用户不存在") }) public User getUserById(@ApiParam(value = "用户 ID", required = true) @PathVariable Long id) { // 根据 ID 查询用户信息 }在上面的代码中,@ApiResponses 表示该方法的响应参数,@ApiResponse 表示该响应参数的描述,code 属性表示响应码,message 属性表示响应消息,response 属性表示响应的数据模型。五、Swagger 的进阶使用1、配置全局参数  我们可以在配置类中使用 globalOperationParameters() 方法来配置全局参数。例如,我们可以配置一个全局的 Authorization 参数,用于授权:@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("com.example.demo.controller")) .paths(PathSelectors.any()) .build() .globalOperationParameters(Arrays.asList( new ParameterBuilder() .name("Authorization") .description("授权") .modelRef(new ModelRef("string")) .parameterType("header") .required(false) .build() )) .apiInfo(apiInfo()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("接口文档") .description("接口文档") .version("1.0.0") .build(); } }  在上面的代码中,我们通过 globalOperationParameters() 方法来配置一个全局的 Authorization 参数,用于授权。2、配置安全协议  我们可以在配置类中使用 securitySchemes() 方法来配置安全协议。例如,我们可以配置一个 Bearer Token 安全协议:@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("com.example.demo.controller")) .paths(PathSelectors.any()) .build() .securitySchemes(Arrays.asList( new ApiKey("Bearer", "Authorization", "header") )) .apiInfo(apiInfo()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("接口文档") .description("接口文档") .version("1.0.0") .build(); } } 在上面的代码中,我们通过 securitySchemes() 方法来配置一个 Bearer Token 安全协议。3、配置安全上下文  我们可以在配置类中使用 securityContexts() 方法来配置安全上下文。例如,我们可以配置一个安全上下文,用于在 Swagger UI 中显示认证按钮:@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("com.example.demo.controller")) .paths(PathSelectors.any()) .build() .securitySchemes(Arrays.asList( new ApiKey("Bearer", "Authorization", "header") )) .securityContexts(Collections.singletonList( SecurityContext.builder() .securityReferences(Collections.singletonList( new SecurityReference("Bearer", new AuthorizationScope[0]) )) .build() )) .apiInfo(apiInfo()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("接口文档") .description("接口文档") .version("1.0.0") .build(); } } 在上面的代码中,我们通过 securityContexts() 方法来配置一个安全上下文,用于在 Swagger UI 中显示认证按钮。4、配置忽略参数  在接口中,有些参数可能是敏感信息,我们不希望在接口文档中显示。我们可以使用 @ApiIgnore 注解来忽略这些参数。例如,我们可以在 User 类中使用 @ApiIgnore 注解来忽略密码参数:@ApiModel(description = "用户信息") public class User { @ApiModelProperty(value = "用户 ID", example = "1") private Long id; @ApiModelProperty(value = "用户名", example = "张三") private String username; @ApiModelProperty(hidden = true) @ApiIgnore private String password; // 省略 getter 和 setter 方法 } 在上面的代码中,@ApiModelProperty(hidden = true) 表示该参数是隐藏的,@ApiIgnore 表示忽略该参数。六、总结  通过集成 Swagger,我们可以方便地生成接口文档,使得前后端开发协同更加高效。在使用 Swagger 时,我们需要注意以下几点:使用注解来描述接口信息,包括接口名称、请求方式、请求路径、请求参数、响应参数等;在配置类中配置 Swagger,包括扫描的包路径、接口文档信息、全局参数、安全协议、安全上下文等;描述数据模型、枚举类型、响应参数等信息,方便开发者查看和理解接口的功能和参数;在需要时使用 @ApiIgnore 注解来忽略敏感参数的显示。最后,需要注意的是,Swagger 只是一种规范和工具集,它并不能取代单元测试和集成测试等其他测试方式。在开发过程中,我们需要综合使用各种测试方式,保证软件的质量和稳定性。
0
0
0
浏览量2008
开着皮卡写代码

SpringBoot 项目的创建与启动

Spring Boot是什么众所周知 Spring 应用需要进行大量的配置,各种 XML 配置和注解配置让人眼花缭乱,且极容易出错,因此 Spring 一度被称为“配置地狱”。为了简化 Spring 应用的搭建和开发过程,Pivotal 团队在 Spring 基础上提供了一套全新的开源的框架,它就是 Spring Boot。Spring Boot 具有 Spring 一切优秀特性,Spring 能做的事,Spring Boot 都可以做,而且使用更加简单,功能更加丰富,性能更加稳定而健壮。随着近些年来微服务技术的流行,Spring Boot 也成为了时下炙手可热的技术。Spring Boot 的特点Spring Boot 具有以下特点:1. 独立运行的 Spring 项目Spring Boot 可以以 jar 包的形式独立运行,Spring Boot 项目只需通过命令“ java–jar xx.jar” 即可运行。2. 内嵌 Servlet 容器Spring Boot 使用嵌入式的 Servlet 容器(例如 Tomcat、Jetty 或者 Undertow 等),应用无需打成 WAR 包3. 提供 starter 简化 Maven 配置Spring Boot 提供了一系列的“starter”项目对象模型(POMS)来简化 Maven 配置。4. 提供了大量的自动配置Spring Boot 提供了大量的默认自动配置,来简化项目的开发,开发人员也通过配置文件修改默认配置。5. 自带应用监控Spring Boot 可以对正在运行的项目提供监控。6. 无代码生成和 xml 配置Spring Boot 不需要任何 xml 配置即可实现 Spring 的所有配置。配置开发环境在使用 Spring Boot 进行开发之前,第一件事就是配置好开发环境。工欲善其事,必先利其器,IDE(集成开发环境)的选择相当重要,目前市面上有很多优秀的 IDE 开发工具,例如 IntelliJ IDEA、Spring Tools、Visual Studio Code 和 Eclipse 等等,那么我们该如何选择呢?这里我们极力推荐大家使用 IntelliJ IDEA,因为相比于与其他 IDE,IntelliJ IDEA 对 Spring Boot 提供了更好的支持。Spring Boot 版本及其环境配置要求如下表创建 SpringBoot 项目1、Spring Initializr 创建 SpringBoot 项目第一步: 打开IDEA,如下图打开 File—>New—>Project ,然后进入下一步。第二步: 打开Project后,按照下图步骤创建一个 Empty Project 项目,项目名称为 springboot_csdn,然后进入下一步。第三步: 空项目创建好之后,右击项目名称,新建一个 Module,进入下一步。第四步: 打开 Spring Initializr,在设置对话框中,Server URL 默认是 start.spring.io,所以用此步骤进行创建 springboot 项目时必须联网。且创建的本质,是将官网创建的步骤以 IDEA 图形化界面的方式创建。start.spring.io 是国外的一个网站,当运行速度较慢的时候,我们使用国内的阿里云网站:start.aliyun.com。输入项目的 GroupId、ArtifactId 等内容,注意 Type 为 Maven,packaging 为 jar,Java version 切换为 8(默认为 11),最后点击下方的 Next 按钮,进行下一步;第五步: 到此界面,其实无需过多更改,可以选择 Spring Boot 2.7.9 的版本。后面项目创建后根据实际情况还能修改,然后点击 Create,一个springboot 项目便创建完成。注意:如果创建完成后,pom.xml文件不是蓝色的m显示时,右击文件,添加到maven管理,添加后,打开Maven点击更新即可。2、Maven 创建 SpringBoot 项目第一种方式创建 SpringBoot 项目的前提是需要在联网的情况下,当没有网的时候,我们就不能用上面的方式来创建 SpringBoot 项目了,这时我们就需要使用 Maven 创建 SpringBoot 项目,此方式创建的前提是本地仓库已经缓存之前创建 SpringBoot 项目所需要的相关依赖。创建步骤如下:第一步: 按照之前创建 Maven 的步骤进行创建。第二步: 返回 IntelliJ IDEA 工作区,会发现 Maven 项目创建完成,其目录结构如图所示。第三步: 在该 Maven 项目的 pom.xml 中添加以下配置,导入 Spring Boot 相关的依赖<!--继承springboot的父工程--> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.5</version> </parent> <!--引入web依赖 spring+springmvc--> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <!-- 项目打jar包必要插件--> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>第四步: 在 cn.kgc.springboot包下,创建一个名为 HelloWorldApplication 主程序,用来启动 Spring Boot 应用,代码如下<!-- 注解说明: 该注解是组合注解 @SpringBootConfiguration 用来自动配置spring相关环境 spring+springmvc @EnableAutoConfiguration 开启自动配置 核心注解 @ComponentScan 组件扫描 只能扫描当前包和子包 --> @SpringBootApplication public class HelloWorldApplication { public static void main(String[] args) { SpringApplication.run(HelloWorldApplication.class, args); } }启动 Spring Boot默认情况下,Spring Boot 项目会创建一个名为 ***Application 的主程序启动类 ,该类中使用了一个组合注解 @SpringBootApplication,用来开启 Spring Boot 的自动配置,另外该启动类中包含一个 main() 方法,用来启动该项目。直接运行启动类 HelloworldApplication 中的 main() 方法,便可以启动该项目,结果如下图注意:Spring Boot 内部集成了 Tomcat,不需要人为手动配置 Tomcat,开发者只需要关注具体的业务逻辑即可。为了能比较的清楚的看到效果,我们在 cn.csdn.springboot 包下又创建一个 controller 包,并在该包内创建一个名为 UserController 的 Controller,代码如下package cn.kgc.springboot01.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("user") public class UserController { @RequestMapping("show") private String Show(){ System.out.println("springboot01展示了!"); return "springboot01展示了!"; } }重启 Spring Boot 项目,然后在地址栏访问 “http://localhost:8080/user/show”,结果如下图SpringBoot 设置端口号上面我们项目运行的默认端口号是 8080,如果我们想更改端口号,用如下方式实现即可:1、如下图,打开 resources 目录下的 application.properties 文件,在文件中添加 “server.port=8888” ,8888即是最新的端口号。运行效果如下:2、application.properties 做设置,会增加代码的冗余,因此,我们通常使用 yml 格式的文件进行配置设置;将 application.properties 改为application.yml ,内容修改如下:server: port: 9999运行效果如下:
0
0
0
浏览量2009
开着皮卡写代码

rabbitmq安装

下载链接:https://pan.baidu.com/s/1S31oVSB-ki_FjQZinsIvjw?pwd=1111提取码:11111.下载Erlang的rpm包RabbitMQ是Erlang语言编写,所以Erang环境必须要有,注:Erlang环境一定要与RabbitMQ版本匹配:https://www.rabbitmq.com/which-erlang.htmlErlang下载地址:https://www.rabbitmq.com/releases/erlang/(根据自身需求及匹配关系,下载对应rpm包)https://dl.bintray.com/rabbitmq-erlang/rpm/erlang/21/el/7/x86_64/erlang-21.3.8.9-1.el7.x86_64.rpm2.下载socat的rpm包rabbitmq安装依赖于socat,所以需要下载socat。socat下载地址:http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm3.下载RabbitMQ的rpm包RabbitMQ下载地址:https://www.rabbitmq.com/download.html(根据自身需求及匹配关系,下载对应rpm包)rabbitmq-server-3.8.1-1.el7.noarch.rpm安装1.安装Erlang、Socat、RabbitMQ①rpm -ivh erlang-21.3.8.9-1.el7.x86_64.rpm②rpm -ivh socat-1.7.3.2-1.el6.lux.x86_64.rpm在安装rabbitmq之前需要先安装socat,否则,报错。可以采用yum安装方式:yum install socat,我们这里采用rpm安装方式③rpm -ivh rabbitmq-server-3.8.1-1.el7.noarch.rpm/usr/lib/rabbitmq/bin/上传到opt目录中2.启用管理插件rabbitmq-plugins enable rabbitmq_management3.启动RabbitMQ需要先把防火墙给关闭,然后再启动服务systemctl start rabbitmq-server.service4.查看进程ps -ef | grep rabbitmq测试关闭防火墙:systemctl stop firewalld.service在web浏览器中输入地址:http://虚拟机ip:15672/输入默认账号密码: guest : guest,guest用户默认不允许远程连接。增加自定义账号添加管理员账号密码:rabbitmqctl add_user admin admin分配账号角色:rabbitmqctl set_user_tags admin administrator修改密码:rabbitmqctl change_password admin 密码查看用户列表:rabbitmqctl list_users使用新账号登录,成功界面管理界面标签页介绍overview:概览connections:无论生产者还是消费者,都需要与RabbitMQ建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道。Exchanges:交换机,用来实现消息的路由Queues:队列,即消息队列,消息存放在队列中,等待消费,消费后被移除队列。端口:5672:rabbitMq的编程语言客户端连接端口15672:rabbitMq管理界面端口25672:rabbitMq集群的端口卸载rpm -qa | grep rabbitmqrpm -e rabbitmq-server管理界面添加用户如果不使用guest,我们也可以自己创建一个用户:创建Virtual Hosts设置权限
0
0
0
浏览量2015
开着皮卡写代码

RabbitMQ:发布确认高级

1.发布确认1.1发布确认机制方案1.2全局配置文件在application.properties全局配置文件中添加spring.rabbitmq.publish-confirm-type属性,这个属性有以下几种值none:禁用发布确认模式(默认)0correlated:发布消息成功到交换机后会触发回调方法simple:有两种效果第一种效果是和correlated一样会触发回调方法第二种效果是在发布消息成功以后使用rabbitTemplate调用waitForConfirms或者waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判单下一步的逻辑waitForConfirmsOrDie方法如果返回false则会关闭信道,那么接下来就无法发送消息到broker# RabbitMQ/配置 #服务器地址 spring.rabbitmq.host=192.168.88.136 #服务端口号 spring.rabbitmq.port=5672 #虚拟主机名称 spring.rabbitmq.virtual-host=/myhost #用户名 spring.rabbitmq.username=admin #密码 spring.rabbitmq.password=123456 #设置生产者发布确认模式 spring.rabbitmq.publisher-confirm-type=correlated1.3配置类package com.zyh.config; import org.springframework.amqp.core.*; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author zengyihong * @create 2022--10--06 10:06 */ @Configuration public class ConfirmConfig { //确认交换机 public static final String CONFIRM_EXCHANGE = "confirm_exchange"; //确认队列 public static final String CONFIRM_QUEUE = "confirm_queue"; //路由key public static final String CONFIRM_ROUTING_KEY = "key1"; /** * 声明确认交换机 * * @return */ @Bean public DirectExchange confirmExchange() { return new DirectExchange(CONFIRM_EXCHANGE); } /** * 声明确认队列 * * @return */ @Bean public Queue confirmQueue() { return QueueBuilder.durable(CONFIRM_QUEUE).build(); } /** * 把确认交换机和确认队列进行绑定 * @param queue * @param exchange * @return */ @Bean public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,@Qualifier("confirmExchange") DirectExchange exchange){ return BindingBuilder.bind(queue).to(exchange).with(CONFIRM_ROUTING_KEY); } }1.4生产者package com.zyh.controller; import com.zyh.config.ConfirmConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.Date; /** * @author zengyihong * @create 2022--10--06 10:15 */ @Slf4j @RestController @RequestMapping("/confirm") public class ConfirmController { @Resource private RabbitTemplate rabbitTemplate; /** * 生产者发送消息 * * @param message */ @GetMapping("/sendConfirmMessage/{message}") public void sendMessage(@PathVariable String message) { log.info("生产者发送消息:{}",message); rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE, ConfirmConfig.CONFIRM_ROUTING_KEY, message); } }1.5消费者package com.zyh.consumer; import com.zyh.config.ConfirmConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; import java.io.UnsupportedEncodingException; /** * @author zengyihong * @create 2022--10--06 10:20 */ @Slf4j @Component public class ConfirmConsumer { @RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE) public void receiveConfirmMessage(Message message) { try { //获取消息 String msg = new String(message.getBody(),"UTF-8"); //记录日志 log.info("消费者接收到确认队列中的消息:{}"+msg); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } }1.6测试正常运行结果如图所示,如果rabbitmq出现故障的话,那么结果是不会显示出来的,我们可以通过回调接口来监测运行结果1.7回调接口package com.zyh.config; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.Resource; /** * @author zengyihong * @create 2022--10--06 10:36 */ @Slf4j @Component public class MyCallBack implements RabbitTemplate.ConfirmCallback { @Resource private RabbitTemplate rabbitTemplate; //依赖注入rabbitTemplate之后再设置它的回调对象 @PostConstruct public void init() { //把当前类MyCallBack实现类注入到RabbitTemplate中确认回调接口中 rabbitTemplate.setConfirmCallback(this); } /** * 不管交换机有没有接收到消息,都会执行这个回调方法 * @param correlationData * @param ack * @param cause */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { //获取消息id String id = correlationData != null ? correlationData.getId() : ""; //判断交换机是否接收到消息 if (ack) { log.info("交换机已经收到id为{}的消息", id); } else { log.info("交换机还没有收到id为{}的消息,原因是{}", id, cause); } } }1.8改写生产者代码package com.zyh.controller; import com.zyh.config.ConfirmConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * @author zengyihong * @create 2022--10--06 10:15 */ @Slf4j @RestController @RequestMapping("/confirm") public class ConfirmController { @Resource private RabbitTemplate rabbitTemplate; /** * 生产者发送消息 * * @param message */ @GetMapping("/sendConfirmMessage/{message}") public void sendMessage(@PathVariable String message) { //指定消息id为1的数据 CorrelationData correlationData1 = new CorrelationData("1"); rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE, ConfirmConfig.CONFIRM_ROUTING_KEY, message,correlationData1); CorrelationData correlationData2 = new CorrelationData("2"); //key2是一个不存在的路由key rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE, "key2", message,correlationData2); log.info("生产者发送消息:{}",message); } }1.9测试交换机收到两条信息,但是消费者只能消费一条消息,因为第二条消息的路由key和交换机的binding key不一样,也没有其他队列可以接收这条消息,所以就被丢弃了。2.回退消息2.1Mandatory参数如果我们仅仅开启了生产者确认机制,那么当交换机接收到消息以后,会直接给生产者发送确认消息,但是如果发现消息不可以路由,就会直接把消息丢弃,此时消费者接收不到消息,而且这个时候生产者也不知道消息被丢弃了,这样就导致消息丢失。我们可以通过设置mandatory参数,使得消息在传递过程中出现不可到达的目的地的时候可以把消息返回给生产者2.2在全局配置文件中开启回退消息# RabbitMQ/配置 #服务器地址 spring.rabbitmq.host=192.168.88.136 #服务端口号 spring.rabbitmq.port=5672 #虚拟主机名称 spring.rabbitmq.virtual-host=/myhost #用户名 spring.rabbitmq.username=admin #密码 spring.rabbitmq.password=123456 #设置生产者发布确认模式 spring.rabbitmq.publisher-confirm-type=correlated #开启消息回退 spring.rabbitmq.publisher-returns=true2.3生产者package com.zyh.controller; import com.zyh.config.ConfirmConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * @author zengyihong * @create 2022--10--06 10:15 */ @Slf4j @RestController @RequestMapping("/confirm") public class ConfirmController { @Resource private RabbitTemplate rabbitTemplate; /** * 生产者发送消息 * * @param message */ @GetMapping("/sendConfirmMessage/{message}") public void sendMessage(@PathVariable String message) { //指定消息id为1的数据 CorrelationData correlationData1 = new CorrelationData("1"); rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE, ConfirmConfig.CONFIRM_ROUTING_KEY, message,correlationData1); CorrelationData correlationData2 = new CorrelationData("2"); //key2是一个不存在的路由key rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE, "key2", message,correlationData2); log.info("生产者发送消息:{}",message); } }2.4回调接口package com.zyh.config; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.ReturnedMessage; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.Resource; /** * @author zengyihong * @create 2022--10--06 10:36 */ @Slf4j @Component public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback { @Resource private RabbitTemplate rabbitTemplate; //依赖注入rabbitTemplate之后再设置它的回调对象 @PostConstruct public void init() { //把当前类MyCallBack实现类注入到RabbitTemplate中确认回调接口中 rabbitTemplate.setConfirmCallback(this); //把当前类MyCallBack实现类注入到RabbitTemplate中消息回退接口中 rabbitTemplate.setReturnsCallback(this); } /** * 不管交换机有没有接收到消息,都会执行这个回调方法 * @param correlationData * @param ack * @param cause */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { //获取消息id String id = correlationData != null ? correlationData.getId() : ""; //判断交换机是否接收到消息 if (ack) { log.info("交换机已经收到id为{}的消息", id); } else { log.info("交换机还没有收到id为{}的消息,原因是{}", id, cause); } } @Override public void returnedMessage(ReturnedMessage returnedMessage) { log.error("消息{}----->被交换机{}退回,退回原因:{},路由key:{}", new String(returnedMessage.getMessage().getBody()), returnedMessage.getExchange(), returnedMessage.getReplyText(), returnedMessage.getRoutingKey()); } }2.5消费者package com.zyh.consumer; import com.zyh.config.ConfirmConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; import java.io.UnsupportedEncodingException; /** * @author zengyihong * @create 2022--10--06 10:20 */ @Slf4j @Component public class ConfirmConsumer { @RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE) public void receiveConfirmMessage(Message message) { try { //获取消息 String msg = new String(message.getBody(),"UTF-8"); //记录日志 log.info("消费者接收到确认队列中的消息:{}",msg); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } }2.6测试3.备份交换机Rabbitmq——备份交换机
0
0
0
浏览量2016
开着皮卡写代码

SpringBoot + layui 框架实现一周免登陆功能

要实现一周免登录功能,您可以使用Spring Boot和Layui框架配合完成。以下是一种可能的实现方式:创建一个名为User的实体类,用于表示用户信息,其中包含用户的用户名和密码等字段,以及用于标记用户是否选择一周免登陆的rememberMe字段。@Entity @Table(name = "users") public class User implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String username; @Column(nullable = false) private String password; private boolean rememberMe; // Getters and Setters // ... } 创建一个名为UserRepository的接口,用于对User实体进行数据库操作。@Repository public interface UserRepository extends JpaRepository<User, Long> { User findByUsername(String username); }创建一个名为UserService的服务类,用于处理用户相关的业务逻辑。在这个类中,添加一个方法用于验证用户的登录,并根据用户是否选择一周免登陆来设置相关的Cookie。@Service public class UserService { private UserRepository userRepository; @Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } public boolean login(String username, String password, boolean rememberMe, HttpServletResponse response) { User user = userRepository.findByUsername(username); if (user != null && user.getPassword().equals(password)) { if (rememberMe) { // 设置一周免登陆的Cookie,有效期为7天 Cookie cookie = new Cookie("rememberMe", "true"); cookie.setMaxAge(7 * 24 * 60 * 60); // 7天的秒数 cookie.setPath("/"); response.addCookie(cookie); user.setRememberMe(true); userRepository.save(user); } return true; } return false; } }创建一个名为LoginController的控制器类,用于处理用户登录的请求。@Controller public class LoginController { private UserService userService; @Autowired public void setUserService(UserService userService) { this.userService = userService; } @RequestMapping("/login") public String login(String username, String password, boolean rememberMe, HttpServletResponse response) { if (userService.login(username, password, rememberMe, response)) { return "redirect:/home"; // 登录成功后跳转到主页 } return "redirect:/login?error"; // 登录失败跳转回登录页面,并带上错误参数 } }在对应的登录页面中使用Layui框架的表单组件,将用户名、密码和记住我选项组织成一个表单,并向LoginController的登录请求发送POST请求。<!DOCTYPE html> <html> <head> <title>登录</title> <!-- 引入Layui的相关资源 --> <link rel="stylesheet" href="https://cdn.staticfile.org/layui/2.5.4/css/layui.min.css"> <script src="https://cdn.staticfile.org/layui/2.5.4/layui.min.js"></script> </head> <body> <div class="layui-container"> <form class="layui-form" action="/login" method="post"> <div class="layui-form-item"> <label class="layui-form-label">用户名</label> <div class="layui-input-block"> <input type="text" name="username" lay-verify="required" autocomplete="off" placeholder="请输入用户名" class="layui-input"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">密码</label> <div class="layui-input-block"> <input type="password" name="password" lay-verify="required" autocomplete="off" placeholder="请输入密码" class="layui-input"> </div> </div> <div class="layui-form-item"> <div class="layui-input-block"> <input type="checkbox" name="rememberMe" title="记住我" lay-skin="primary"> <button class="layui-btn" lay-submit lay-filter="formDemo">登录</button> </div> </div> </form> </div> <script> layui.use(['form'], function() { var form = layui.form; // 表单验证 form.verify({ required: function(value, item) { if(value.length < 1) { return '该项不能为空'; } } }); // 监听表单提交 form.on('submit(formDemo)', function(data) { // 获取表单数据并提交 var username = data.field.username; var password = data.field.password; var rememberMe = data.field.rememberMe === 'on'; // 发送AJAX请求提交登录表单 $.ajax({ url: '/login', type: 'POST', data: { username: username, password: password, rememberMe: rememberMe }, success: function(res) { // 登录成功后的逻辑处理 if (res.success) { window.location.href = '/home'; } else { layer.msg(res.message, {icon: 2}); } }, error: function() { layer.msg('服务器错误', {icon: 2}); } }); return false; // 阻止表单提交 }); }); </script> </body> </html> 在上述代码中,我们使用了Layui的form模块进行表单的验证和提交。form.verify()函数用于定义表单字段的验证规则,这里只提供了一个required规则作为示例。form.on(‘submit(formDemo)’, function(data) {})函数用于监听表单提交事件,并在提交时使用AJAX发送登录请求。提交成功后,根据服务器的响应进行相应的处理。
0
0
0
浏览量2024
开着皮卡写代码

RabbitMQ:Topics主题/通配符模式

1.基本介绍Topic类型与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert通配符规则:#:匹配0个或者多个词*:刚好可以匹配一个词2.生产者public class Producer { public static String TOPIC_EXCHANGE = "topic_exchange"; public static String TOPIC_QUEUE_1 = "topic_queue_1"; public static String TOPIC_QUEUE_2 = "topic_queue_2"; public static void main(String[] args) { try { Channel channel = ConnectUtil.getChannel(); //声明交换机(交换机名称,交换机类型) channel.exchangeDeclare(TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC); //声明队列 channel.queueDeclare(TOPIC_QUEUE_1,true,false,false,null); channel.queueDeclare(TOPIC_QUEUE_2,true,false,false,null); //把交换机和队列1进行绑定 channel.queueBind(TOPIC_QUEUE_1,TOPIC_EXCHANGE,"#.error"); //把交换机和队列2进行绑定 channel.queueBind(TOPIC_QUEUE_2,TOPIC_EXCHANGE,"order.*"); channel.queueBind(TOPIC_QUEUE_2,TOPIC_EXCHANGE,"*.orange.*"); channel.queueBind(TOPIC_QUEUE_2,TOPIC_EXCHANGE,"*.*"); //发送消息 String msg="日志信息:调用了xxx方法,日志级别是error"; channel.basicPublish(TOPIC_EXCHANGE,"error",null,msg.getBytes()); System.out.println("消息发送成功"); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }3.消费者消费者1 public class Consumer1 { public static void main(String[] args) { try { //获取信道对象 Channel channel = ConnectUtil.getChannel(); //消费消息 DefaultConsumer consumer=new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消费者1接收到消息:"+new String(body,"UTF-8")); System.out.println("消费者1把日志信息保存到数据库"); } }; channel.basicConsume(Producer.TOPIC_QUEUE_1,true,consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }消费者2public class Consumer2 { public static void main(String[] args) { try { //获取信道对象 Channel channel = ConnectUtil.getChannel(); //消费消息 DefaultConsumer consumer=new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("消费者2接收到消息:"+new String(body,"UTF-8")); System.out.println("消费者2把日志信息保存到数据库"); } }; channel.basicConsume(Producer.TOPIC_QUEUE_2,true,consumer); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }4.测试
0
0
0
浏览量2011
开着皮卡写代码

一文吃透SpringBoot整合mybatis-plus(保姆式教程)

首先创建一个 SpringBoot 项目,具体创建步骤可以参见我的上一篇博文:SpringBoot 项目的创建与启动。手动整合 mybatis-plus 详解1、引入依赖在 pom.xml 文件中添加相关依赖,代码如下: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.12</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>cn.kgc</groupId> <artifactId>springboot04</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot04</name> <description>springboot04</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--引入mybatis-plus依赖--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <!--引入mysql依赖--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--引入druid连接池依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.9</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>2、创建基本目录结构在新创建的springboot项目中的cn.kgc.springboot04包下创建如下图所示的目录结构,再在resources目录下创建mapper目录。3、配置 application.yml将下图中 application.properties 文件改为 application.yml 风格的文件在application.yml文件中添加相关配置,配置代码如下:server: port: 8888 spring: #配置数据源 datasource: driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql:///java2218?serverTimezone=UTC&useSSL=false&characterEncoding=UTF-8 username: root password: huanghuang #配置mybatis-plus mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true type-aliases-package: cn.kgc.springboot04.entity mapper-locations: classpath:mapper/*.xml4、在 entity 包下创建实体类创建一个实体类 Admin,代码如下:package cn.kgc.springboot04.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @TableName("admin") @Accessors(chain = true) public class Admin { @TableId(type = IdType.AUTO) private long adminId; @TableField("adminName") private String adminName; private long adminPassword; @TableField(exist = false) private String sex; }@TableName(“admin”):指定实体类对应数据库中表的名字,Admin类默认对应的表名为admin,当类名不为Admin时,添加此注解可解决此问题。@Accessors(chain = true) :通过链式调用完成对象创建;如:Admin admin = new Admin().setAdminName(“小明”).setAdminPassword(123456);@TableId(type = IdType.AUTO) :指定主键自增策略,如果数据库为给主键添加自增属性,通过此注解可以添加自增功能。@TableField(“adminName”):当实体类的属性名与数据库的字段名不一致时,使用此注解可以指定属性名对应数据库中的哪个字段对应。@TableField(exist = false):指定当前属性在数据库中不存在对应的字段 忽略该字段的操作。5、创建 Mapper 接口创建 AdminMapper 接口,使其继承 BaseMapper 类实现ORM操作,代码如下:package cn.kgc.springboot04.mapper; import cn.kgc.springboot04.entity.Admin; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface AdminMapper extends BaseMapper<Admin> { }其中,BaseMapper提供了常用的CRUD、分页、批量操作等方法。6、创建 Mapper.xml 文件创建 AdminMapper.xml 文件,使其的 namespace 为 AdminMapper 接口的路径地址,代码如下:<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="cn.kgc.springboot04.mapper.AdminMapper"> </mapper>7、创建 Service 接口创建 AdminService 接口,使其继承 IService 类,代码如下:package cn.kgc.springboot04.service; import cn.kgc.springboot04.entity.Admin; import com.baomidou.mybatisplus.extension.service.IService; public interface AdminService extends IService<Admin> { }8、创建 ServiceImpl 实现类创建 AdminServiceImpl 实现类,使其继承 ServiceImpl 类以及继承 AdminService 接口,代码如下:package cn.kgc.springboot04.service.impl; import cn.kgc.springboot04.entity.Admin; import cn.kgc.springboot04.mapper.AdminMapper; import cn.kgc.springboot04.service.AdminService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; @Service public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements AdminService { }9、创建 Controller 控制类创建 AdminController 控制类,代码如下: package cn.kgc.springboot04.controller; import cn.kgc.springboot04.entity.Admin; import cn.kgc.springboot04.service.AdminService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("admin") public class AdminController { @Autowired AdminService adminService; //查询所有管理员 @RequestMapping("list") public List<Admin> getList(){ return adminService.list(); } //增加或者修改一条数据 @RequestMapping("savaOrUpdate") public String insertOne(Admin admin){ boolean save = adminService.saveOrUpdate(admin); return ""+save; } //删除一条数据 @RequestMapping("delete") public String deleteOne(Integer id){ boolean b = adminService.removeById(id); return ""+b; } }10、测试下面,我们就一一测试不同接口的运行效果吧!【1】查询所有数据:在浏览器输入(http://localhost:8888/admin/list )测试结果如下:【2】插入一条数据:在浏览器输入(http://localhost:8888/admin/savaOrUpdate?adminname=赵敏&adminpassword=123456)测试结果如下:添加数据后,数据库数据显示如下:【3】修改一条数据:在浏览器输入(http://localhost:8888/admin/savaOrUpdate?adminname=赵敏&adminpassword=888888&adminid=1006)测试结果如下:修改数据后,数据库数据显示如下:【4】删除一条数据:在浏览器输入(http://localhost:8888/admin/delete?id=1006)测试结果如下:如下图,删除数据后,数据库的这条记录便不存在了。自动整合 mybatis-plus 详解1、引入依赖在 pom.xml 文件中添加相关依赖,代码如下: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.12</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>cn.kgc</groupId> <artifactId>springboot05</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot05</name> <description>springboot05</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--引入mybatis-plus依赖--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <!--引入mysql依赖--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--引入druid连接池依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.9</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>2、配置 application.yml将下图中 application.properties 文件改为 application.yml 风格的文件在application.yml文件中添加相关配置,配置代码如下:server: port: 9999 spring: #配置数据源 datasource: driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql:///java2218?serverTimezone=UTC&useSSL=false&characterEncoding=UTF-8 username: root password: huanghuang #配置mybatis-plus mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true type-aliases-package: cn.kgc.springboot04.entity mapper-locations: classpath:mapper/*.xml3、自动整合配置【1】IDEA 连接 数据库如下图,打开IDEA 右侧工具栏的 Database ,点击+按钮,选择 Data Source后,选择你使用的数据库类型,我这边使用的是MySQL,所以我选择MySQL,然后进入下一步;【2】添加数据库到IDEA中按照下图所示,填入数据库用户名和密码,然后填写需要添加的数据库名称,点击 Apply 和 OK 后,进入下一步。如果java2218数据库中没有数据,可以点击此处的刷新按钮即可。【3】选择数据表进行自动创建第一步:如下图,选择你需要自动创建的表,可以选择多个表,然后右击选择 MybatisX-Generator,进入下一步;第二步:如下图配置 module path、base package 和 relative package,然后进入下一步;第三步:如下图配置,选择你安装的 MyBatis-Plus 版本,我安装的是 MyBatis-Plus 3版本,因此选择此项,然后选择 Lombok ,点击 Finish 完成创建点击完成后,自动生成的文件目录如下图所示:由图可知,通过次步操作,我们已经自动创建了entity层、 mapper 层和 service 层,大大提高了我们编写的效率。4、手动创建 Controller 层和测试Controller 层的业务和测试如 【手动整合 mybatis-plus 详解】中的第9、10步一样。
0
0
0
浏览量2011
开着皮卡写代码

SpringBoot 整合 JSP和MyBatis

💖 Spring Boot starter入门  传统的 Spring 项目想要运行,不仅需要导入各种依赖,还要对各种 XML 配置文件进行配置,十分繁琐,但 Spring Boot 项目在创建完成后,即使不编写任何代码,不进行任何配置也能够直接运行,这都要归功于 Spring Boot 的 starter 机制  Spring Boot 将日常企业应用研发中的各种场景都抽取出来,做成一个个的 starter(启动器),starter 中整合了该场景下各种可能用到的依赖,用户只需要在 Maven 中引入 starter 依赖,SpringBoot 就能自动扫描到要加载的信息并启动相应的默认配置。starter 提供了大量的自动配置,让用户摆脱了处理各种依赖和配置的困扰。所有这些 starter 都遵循着约定成俗的默认配置,并允许用户调整这些配置,即遵循“约定大于配置”的原则  并不是所有的 starter 都是由 Spring Boot 官方提供的,也有部分 starter 是第三方技术厂商提供的,例如 druid-spring-boot-starter 和 mybatis-spring-boot-starter 等等。当然也存在个别第三方技术,Spring Boot 官方没提供 starter,第三方技术厂商也没有提供 starter  以 spring-boot-starter-web 为例,它能够为提供 Web 开发场景所需要的几乎所有依赖,因此在使用 Spring Boot 开发 Web 项目时,只需要引入该 Starter 即可,而不需要额外导入 Web 服务器和其他的 Web 依赖 <!--SpringBoot父项目依赖管理--> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.13</version> <relativePath/> </parent> <dependencies> <!--导入 spring-boot-starter-web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> ... </dependencies> </project>您可能会发现一个问题,即在以上 pom.xml 的配置中,引入依赖 spring-boot-starter-web 时,并没有指明其版本(version),但在依赖树中,我们却看到所有的依赖都具有版本信息,那么这些版本信息是在哪里控制的呢?  其实,这些版本信息是由 spring-boot-starter-parent(版本仲裁中心) 统一控制的。spring-boot-starter-parent   spring-boot-starter-parent 是所有 Spring Boot 项目的父级依赖,它被称为 Spring Boot 的版本仲裁中心,可以对项目内的部分常用依赖进行统一管理。 <!--SpringBoot父项目依赖管理--> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.5</version> </parent>Spring Boot 项目可以通过继承 spring-boot-starter-parent 来获得一些合理的默认配置,它主要提供了以下特性:默认 JDK 版本(Java 8)默认字符集(UTF-8)依赖管理功能资源过滤默认插件配置识别 application.properties 和 application.yml 类型的配置文件💖 SpringBoot基本设置6.1 SpringBoot设置端口号server: port: 8989 #配置端口6.2 SpringBoot设置项目名server: servlet: context-path: /springboot #配置项目的虚拟路径(根路径) 项目名使用/开头6.3 SpringBoot配置文件的拆分spring: profiles: active: dev #开发环境 6.4 SpringBoot开启日志# 显示sql logging: level: cn.kgc.springboot.mapper: debug #开启日志 6.5 SpringBoot实现热部署  在web项目的开放过程中,通常我们每次修改完代码都需要重新启动项目,实现重新部署。但是随着项目越来越庞大,每次启动都是一个费时的事情。所以,热部署是一个非常构建的技术,有了热部署,可以极大提高我们的开发效率spring-boot-devtools实现热部署1.引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency>2.配置maven插件<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> <!-- 如果devtools不生效 请设置该属性 --> </configuration> </plugin> </plugins> </build>3.配置application.yml文件devtools: restart: enabled: true #开启热部署4.修改idea设置File-Settings-Compiler 勾选 Build Project automatically5.设置Registryctrl + shift + alt + / ,选择Registry,勾选Compiler autoMake allow when app running6.6 SpringBoot开启分页查询1.引入依赖:<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.12</version> </dependency> ---------------------------------------------- <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.10</version> </dependency>2.配置application.yml文件(可以不配置使用默认)# pageHelper分页配置 pagehelper: helper-dialect: mysql #数据库类型 reasonable: true #分页合理化 page<1 查询第一页 page >pageNumber 查询最后一页 support-methods-arguments: true # 支持mapper接口传递参数开启分页 params: count=countSql #用于从对象中根据属性名取值3.编写控制器,业务层,持久化层进行测试@Override public PageInfo<User> getPage(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<User> userList = userMapper.selectList(); PageInfo<User> pageInfo = new PageInfo<>(userList); return pageInfo; }💖 springBoot对象管理管理对象spring中管理对象的方式:1.使用xml配置文件,在文件中使用bean标签设置对象的管理<bean id="" class="xxx.xxx.xxx"></bean>2.使用注解 @Component @Controller @Service @Repositorspringboot管理对象的方式:1.使用配置方式创建对象  springboot支持使用 @Configuration 注解添加在配置类上,该类作为一个配置类,相当于spring的配置文件,该注解只能使用在类上。在配置类中方法上使用 @Bean 注解,相当于spring配置文件中的bean标签@Configuration public class MyConfiguration{ @Bean public User getUser(){ return new User(); } @Bean public Student getStudent(){ return new Student(); } }2.使用原始的 spring 注解 @Component @Controller @Service @Repository属性注入spring 原始的注入方式1.set方式2.constructor3.自动注入name: tom age: 30 price: 23.5 sex: true birth: 2021/11/28 12:12:12 array: 12,13,15,17 list: 李四,lisi,tom map: "{'aa':30,'bb':'lisi'}" # 注入map使用json格式 取值的个数#{${map}}对象的注入object: name: lisi age: 20 birth: 2021/11/24@Controller @RequestMapping("/inject2") @ConfigurationProperties("object") public class InjectController2 { private String name; private Integer age; private Date birth; public void setName(String name) { this.name = name; } public void setAge(Integer age) { this.age = age; } public void setBirth(Date birth) { this.birth = birth; } @RequestMapping("/inject") @ResponseBody public String inject(){ System.out.println(name); System.out.println(age); System.out.println(birth); return "ok"; } }注意:添加一下依赖,可以再写yaml文件时提示,消除红色的警告提示。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>💖 springBoot整合JSP引入依赖<!-- 标准标签库--> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- 让springboot内置的tomcat具有解析jsp的能力--> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency>配置视图解析器spring: mvc: view: prefix: / suffix: .jsp设置不重启项目 刷新jsp页面server: servlet: jsp: init-parameters: development: true # 修改jsp页面无需重新启动项目集成后无法访问jsp页面解决方案1.添加插件<build> <resources> <!--注册webapp目录为资源目录--> <resource> <directory>src/main/webapp</directory> <targetPath>META-INF/resources</targetPath> <includes> <include>**/*.*</include> </includes> </resource> </resources> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>2.设置工作目录3.插件启动💖 SpringBoot整合MyBatis引入依赖<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency>编写配置文件spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver username: root password: root url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&characterEncoding=utf-8mybatis配置mybatis: mapper-locations: classpath:mapper/*.xml #设置mapper文件的位置 type-aliases-package: cn.kgc.springboot.entity # 起别名 configuration: map-underscore-to-camel-case: true #开启驼峰命名设置dao接口的扫描1.在入口类上使用注解,完成扫描@SpringBootApplication @MapperScan("cn.kgc.springboot.dao") //扫描dao接口所在的包 同时生成代理对象 注入spring容器 public class Springday02Application { public static void main(String[] args) { SpringApplication.run(Springday02Application.class, args); } }
0
0
0
浏览量2008
开着皮卡写代码

消息中间件原理与实践

全面解析消息中间件的核心概念和应用,深入了解消息如何应用消息中间件构建可靠、高效的分布式系统
0
0
0
浏览量2216
开着皮卡写代码

SpringBoot 框架从入门到精通

从Spring Boot框架的基础入门一直深入到精通阶段,将学到如何使用Spring Boot快速构建、部署和维护Java应用程序,深入了解框架的核心原理和最佳实践
0
0
0
浏览量2142
开着皮卡写代码

quartz基本使用

前言众所周知,Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,在众多的中小项目中有着广泛的使用(本人所在的项目也有部分使用),Quartz的一个特点就是,引用简单,能和既有的框架做快速的整合,不管是基于spring的项目还是springboot的项目,甚至是简单的web项目,都可以快速的引入,加上学习成本低,对于大多数小伙伴来说,是个不错的选择Quartz基本概念Quartz 是一个完全由 Java 编写的开源作业调度框架,为在 Java 应用程序中进行作业调度提供了简单却强大的机制Quartz 可以与 J2EE 与 J2SE 应用程序相结合也可以单独使用Quartz 允许程序开发人员根据时间的间隔来调度作业Quartz 实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联Quartz 核心概念在正式开始编写代码之前,有必要了解一些关于Quartz的几个核心概念,有助于我们对于代码的编写和理解Job 表示一个工作,要执行的具体内容。此接口中只有一个方法,如下:void execute(JobExecutionContext context)JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略Trigger 代表一个调度参数的配置,什么时候去调Scheduler 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了Quartz的运行环境Quartz 可以运行嵌入在另一个独立式应用程序Quartz 可以在应用程序服务器(或 servlet 容器)内被实例化,并且参与 XA 事务Quartz 可以作为一个独立的程序运行(其自己的 Java 虚拟机内),可以通过 RMI 使用Quartz 可以被实例化,作为独立的项目集群(负载平衡和故障转移功能),用于作业的执行下面让我们正式开始编码吧1、引入quartz依赖 <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--quartz相关依赖--> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz-jobs</artifactId> <version>2.3.1</version> </dependency> </dependencies> 按照上面的核心概念中提到的,其实我们定义一个job在quartz中非常简单,大概2个步骤,第一一个实现Job接口的类,第二需要一个类,用于组装job,JobDetail,Trigger,然后利用调度器Scheduler将他们整合在一起就可以了,里面的细节就是如何运用API的过程,那么先看第一个例子吧简单job类import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import java.time.LocalTime; public class MyJob implements Job { @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println("我正在执行任务:" + LocalTime.now()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } import org.quartz.*; import org.quartz.impl.StdSchedulerFactory; public class Test1 { public static void main(String[] args) throws SchedulerException { //定义Scheduler对象 Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.start(); //定义JobDetail对象 JobDetail jobDetail = JobBuilder.newJob(MyJob.class).withIdentity("jodDetail1", "group1").build(); //定义Trigger对象 SimpleTrigger trigger = TriggerBuilder.newTrigger() .startNow() .withSchedule( SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()) .build(); //使用scheduler将JobDetail和Trigger进行组装 scheduler.scheduleJob(jobDetail,trigger); try { Thread.sleep(60000); } catch (InterruptedException e) { e.printStackTrace(); } scheduler.shutdown(); } } 关于这段代码有几处需要简单说明下JobDetail -> withIdentity ,为每一个job指定一个唯一的身份标识,最好带有一定的业务含义Trigger -> withSchedule ,在这个模块中,定义运行的job的详细参数,比如schedule类型,有simpleSchedule简单类型,还有cronSchedule类型,withIntervalInSeconds任务执行间隔时间,这里面的API比较多,定义的时间策略也很多,有兴趣的同学可以点出来瞧瞧本段代码的意思就是间隔5秒执行一次job下面运行下这段代码,看下效果job参数传递试想有这么一种场景,每次执行job时,需要从jobDetail中给job传递一些参数,以便执行时使用该怎么做呢?在jobDetail的对象构造过程中,提供了usingJobData的方法,里面有多种类型可供选择,最常见的像key,value类似map的结构比如我们在jobDetail中传递一个name=jike的值,简单改造后如下:public static void main(String[] args) throws SchedulerException { Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.start(); JobDetail jobDetail = JobBuilder.newJob(MyJob2.class) .withIdentity("jodDetail1", "group1") .usingJobData("name","jike") .build(); SimpleTrigger trigger = TriggerBuilder.newTrigger() .startNow() .withSchedule( SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()) .build(); scheduler.scheduleJob(jobDetail,trigger); try { Thread.sleep(60000); } catch (InterruptedException e) { e.printStackTrace(); } scheduler.shutdown(); } 参数放进去了以后,job中怎么接收呢?quartz提供了大概3种方式进行参数的传递,第一种,属性的set/get方法public class MyJob2 implements Job { @Getter@Setter private String name; @Override public void execute(JobExecutionContext context) throws JobExecutionException { //JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); //System.out.println("name is :" + jobDataMap.getString("name")); System.out.println("name is :" + name); } } 运行上面的代码第二种,从excute的context中获取public class MyJob2 implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); System.out.println("name is :" + jobDataMap.getString("name")); } }还有一种场景,就是我希望这一次job执行完毕之后,参数也随着job的执行不断的改变,这时就需要@PersistJobDataAfterExecution 这个注解排上用场了,看下面的这段代码,第一次在jobDetail中向job传递了一个count的参数,后面我们希望每次job执行完毕后count数值递增public class Test3 { public static void main(String[] args) throws SchedulerException { Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.start(); int count = new Random().nextInt(10); JobDetail jobDetail = JobBuilder.newJob(MyJob3.class) .withIdentity("jodDetail1", "group1") .usingJobData("count",count) .build(); SimpleTrigger trigger = TriggerBuilder.newTrigger() .startNow() .withSchedule( SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()) .build(); scheduler.scheduleJob(jobDetail,trigger); try { Thread.sleep(60000); } catch (InterruptedException e) { e.printStackTrace(); } scheduler.shutdown(); } } @PersistJobDataAfterExecution public class MyJob3 implements Job { @Setter@Getter private int count; @Override public void execute(JobExecutionContext context) throws JobExecutionException { count++; context.getJobDetail().getJobDataMap().put("count",count); LocalTime now = LocalTime.now(); System.out.println("current time is :" + now.toString() + ",the cout is:" + count); } } 简单来说,@PersistJobDataAfterExecution这个注解具有存储参数的功能trigger优先级在使用trigger的时候,有这么一个问题不容忽略,就是当项目中有多个地方的job配置了同一时间点触发,但我们知道,quartz的执行是需要额外占用系统资源的,也就是每次job的执行需要系统提供新的线程来执行,往往在开发过程中,为了不至于让job的运行占用过多的线程资源,我们将quartz的线程开销数设置为一个固定的值,设置也比较简单,只需要在resource目录下,提供一个quartz.properties的文件进行简单的配置即可,job在启动的时候,会去读取这个配置文件中的内容org.quartz.scheduler.instanceName=myScheduler #配置quartz的线程总数,这里配置为1,放大问题出现的可能性 org.quartz.threadPool.threadCount=1 org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore org.quartz.jobStore.misfire.Threshold=1000 这样以来,在上面的场景下,多个任务要在同一时间点触而且我们希望某个job的业务先执行怎么办呢?quartz在trigger的配置中提供了优先级的API,现在假如有两个job,均在10秒之后触发,那么我们可以通过设置withPriority的值来达到我们的目的(注意:前提是线程资源不够的情况下,优先级才会生效)public class Test4 { public static void main(String[] args) throws SchedulerException { Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.start(); JobDetail jobDetail1 = JobBuilder.newJob(MyJob4.class).withIdentity("jodDetail1", "group1").build(); JobDetail jobDetail2 = JobBuilder.newJob(MyJob4.class).withIdentity("jobDetail2", "group2").build(); Date date = DateBuilder.futureDate(10, DateBuilder.IntervalUnit.SECOND); SimpleTrigger trigger1 = TriggerBuilder.newTrigger() // .startNow() .startAt(date) .withPriority(2) .usingJobData("msg","trigger1触发") .withSchedule( SimpleScheduleBuilder .simpleSchedule() .withIntervalInSeconds(5) .repeatForever()) .build(); SimpleTrigger trigger2 = TriggerBuilder.newTrigger() //.startNow() .startAt(date) .withPriority(9) .usingJobData("msg","trigger2触发") .withSchedule( SimpleScheduleBuilder .simpleSchedule() .withIntervalInSeconds(5) .repeatForever()) .build(); scheduler.scheduleJob(jobDetail1,trigger1); scheduler.scheduleJob(jobDetail2,trigger2); try { Thread.sleep(60000); } catch (InterruptedException e) { e.printStackTrace(); } scheduler.shutdown(); } } public class MyJob4 implements Job { @Setter@Getter private String msg; @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println("我正在执行任务:" + LocalTime.now() +",msg is:" + msg); } } 简单改造一下代码之后,运行上面的程序,通过控制台打印结果可以发现具有更高优先级的trigger的任务,在资源有限的情况下会更先执行,cronSchedule相比simpleSchedule,在项目开发中cronSchedule的使用可能更多,cronSchedule的方式支持丰富的cron表达式,即我们熟悉的基于各种时间类型的定时任务,比如每隔5秒执行,就可以使用我们熟悉的 “0/5 * * * * ?”使用CronTrigger,可以指定具体的时间表,例如“每周五中午”或“每个工作日和上午9:30”,甚至“每周一至周五上午9:00至10点之间每5分钟”和1月份的星期五“。在CronScheduleBuilder对象的API中,提供了几种常用的和cron相关的方法,比如最常用的支持cron时间表达式的字符串格式的方法请参考如下示例代码:public class Test6 { public static void main(String[] args) throws SchedulerException { Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.start(); JobDetail jobDetail = JobBuilder.newJob(MyJob6.class).withIdentity("jodDetail1", "group1").build(); CronTrigger cronTrigger = TriggerBuilder.newTrigger() .startNow() .usingJobData("msg", "trigger1触发") .withSchedule( CronScheduleBuilder.cronSchedule("0/5 * * * * ?") ) .build(); scheduler.scheduleJob(jobDetail, cronTrigger); try { Thread.sleep(60000); } catch (InterruptedException e) { e.printStackTrace(); } scheduler.shutdown(); } } public class MyJob6 implements Job { @Setter@Getter private String msg; @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println("我正在执行任务:" + LocalTime.now() +",msg is:" + msg); } } 运行效果如下:misfire机制在quartz中,有这么一种场景不能忽视,设想当你的任务间隔执行时间是5秒,但实际上,在job执行业务逻辑过程中却执行了9秒甚至更长的时间,那么对于quartz来说,这两次间隔的任务该怎么处理呢?因为人家原本希望的是每5秒一次的,现在你的业务执行时间打破了5秒的机制,要怎么处理呢?在quartz中,针对cron类型的trigger,提供了下面3种处理方式,分别是:withMisfireHandlingInstructionDoNothing 不触发立即执行,等待下次Cron触发频率到达时刻开始按照Cron频率依次执行withMisfireHandlingInstructionIgnoreMisfires 以错过的第一个频率时间立刻开始执行,做错过的所有频率周期后,当下一次触发频率发生时间大于当前时间后,再按照正常的Cron频率依次执行withMisfireHandlingInstructionFireAndProceed 以当前时间为触发频率立刻触发一次执行,后按照Cron频率依次执行大概是什么意思呢?我们先用程序简单模拟下效果吧,比如使用第一种来看一下实际效果怎样的?withMisfireHandlingInstructionDoNothingpublic class Test7 { public static void main(String[] args) throws SchedulerException { Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.start(); JobDetail jobDetail1 = JobBuilder.newJob(Job7.class).withIdentity("jodDetail1", "group1").build(); CronTrigger cronTrigger = TriggerBuilder.newTrigger() .startNow() .usingJobData("msg", "trigger1触发") .withPriority(2) .withSchedule( CronScheduleBuilder.cronSchedule("0/5 * * * * ?") .withMisfireHandlingInstructionDoNothing() ) .build(); scheduler.scheduleJob(jobDetail1, cronTrigger); try { Thread.sleep(600000); } catch (InterruptedException e) { e.printStackTrace(); } scheduler.shutdown(); } } public class Job7 implements Job { @Setter @Getter private String msg; @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println("我正在执行任务:" + LocalTime.now() +",msg is:" + msg); try { Thread.sleep(7000); } catch (InterruptedException e) { e.printStackTrace(); } } }运行程序,对照上述关于这几种misFire的解释,能够想象出下面的效果吗?
0
0
0
浏览量616
开着皮卡写代码

单点登录方案设计

前言单点登录在任何稍成规模的分布式系统,或者sass,或者中台型的架构中,都是必不可少的,单点登录主要达到的目的是:一处登录,处处登录这里主要提2种实际生产环境下比较常用的2种业务场景,第一种,产品自身的单点登录需求,比如像下面这张图:这张图反映的是一些类似sass系统或者业务中台类似的体系架构,一个系统的各个产品均能通过一个统一的登录入口进入,然后由各个产品应用图标,再进入各自的模块产品第二种业务场景是,当涉及到和第三方应用进行对接的时候,第三方系统希望共享与本系统的部分用户信息,通俗来讲就是,本系统的某些产品对接了第三方平台的应用,客户端希望能够在本系统和外部系统之间来回切换,而不用重复在2个平台之间进行登录,以下图进行理解:本篇以上面的2个需求点入手,从业务的角度引申到实际方案的实现进行一些探讨和经验的分享平台内部单点登录实现方案1、cookie实现方案这是一种最简单的单点登录实现方式,是使用cookie作为媒介,存放用户会话凭证简而言之,用户登录父应用之后,应用返回一个cookie(或加密的cookie),当用户访问子应用的时候,携带上这个cookie,授权应用拿到cookie并进行校验,校验通过则允许该用户访问当前应用cookie信息通常可以在浏览器中进行查看,如果后端没有做任何的处理,通常以jsessionid进行展示我们知道,对于sass平台或类似的系统,父应用或者顶级应用在登录成功后,会进入主域名,比如taobao.com,然后再从主域名进入各个子系统,即二级域名下,如果是同域的情况下,cookie的会话信息是可以共享(或者传递)的,这样的话,当主域名的用户关键信息存至cookie后,其他子域下的系统就可以拿到当前cookie的信息进行解析并获取用户信息cookie方案可使登录后的用户在各个应用之间正常的访问,当然,在实际生产中,同一个用户在各个应用之间的切换与访问也是有前提条件的,那就是权限体系的认证,即至少保证当前用户拥有要访问应用的权限在小编过往的项目中,sass平台有一个统一用户系统,所有访问系统的用户均需在用户的体系下存在,然后在用户系统中进行对使用其他应用的访问权限进行统一配置(赋权)之后,才能进行各个应用之间的切换,从具体的实现来说,主要分为如下步骤:1.用户中心注册用户(或管理员添加)2.其他应用访问权限配置(菜单,资源授权等)3.用户登录统一认证系统4.各应用通过用户中心统一dubbo接口返回值做进一步业务处理5.校验是否切换至不同应用2、token实现方案cookie方案在实际实施的时候,被很多人吐槽的点有2个,容易被截取(不够安全),不够轻量级,就这2点,就足够成为分布式应用下如何高效进行会话传输被舍弃的因素了,因此在本人经历的项目以及从主流的解决方案下,token方案开始越来越被很多互联网公司接受token的实现相对简单,前后端交互方便,存储的安全性可以根据采用的加密技术提升安全性,主流的实现像:springsecurity , jwt+shiro 等,都是不错的实现思路具体来说,token的实现方案和cookie的实现方案本质不同点在于会话信息的传输上有所不同,其基础的架构体系并没有太大的差别,但token解决这个问题更灵活的地方在于,可以根据业务需要定制化加密方案可以在数据库层面或者其他数据存储上面存储可以灵活的做会话的自动续期总的来说,业务层面的操作和上面cookie方案中的没有本质差别,只体现在具体的实现上,这样以来,用户中心的总体业务层的规划可按如下理解关于token的具体落地实现方案,有兴趣的同学可以参考我之前的2篇文章:springboot+shiro实现安全认证以及基于springcloud实现安全认证总结一下,以上两种实现方案适合初具中台规模或者类sass系统的平台,拥有独立的用户中心体系,需要通过独立的用户中心进行其他应用的资源配置,认证,授权等,在这种架构下,采用单点登录具有它存在的价值和意义,毕竟单独把用户中心拆分成独立的微服务,是需要投入一定的技术和人力成本的与第三方平台对接时单点登录实现方案当sass系统做大了之后,比如像支付宝,很多其他第三方应用为了对接支付宝的支付体系,举例来说,当我们在美团下单时,提示我们选择支付宝支付的时候,需要跳转到支付宝的一个认证页面进行认证,只有认证通过之后,才能进行支付这里涉及到一个用户信息的转换问题,可以理解为认证中心,这个认证中心具备用户认证,以及分发凭证的功能,认证中心的实现我们无从得知,但是可以参考springsecurity的认证功能实现,和oauth2那一套大体类似我们这里探讨的是一种更通用的基于sass平台或者中台模式的实现,即假定通过我们自身的sass平台跳转到第三方平台的应用系统,或者由第三方应用系统跳转到我们自身的sass系统,该如何实现的问题方案一:第三方应用作为自身平台的一个应用注册第三方系统作为一个应用,集成到自身的sass平台。用户以自身平台为入口登录并跳转至第三方系统同域当自身平台与第三方系统同域时,第三方系统能够直接通过Cookie获取本平台用户会话信息(token),通过调用本平台提供的相关dubbo接口即可获取用户信息,进而同步用户至第三方系统并实现免登录同域情况下,第三方应用可以拿到本平台的cookie信息,拿到之后,进一步调用本平台提供的dubbo接口获取用户的数据,一般来说,这种情况下,第三方系统为了更好的管理用户数据,需要和本平台的用户信息进行适配,对第三方系统来说,只需要自己的系统用户表中做一个映射即可达到目的非同域当自身sass平台与第三方系统非同域时,第三方系统就无法通过浏览器Cookie信息获取自身sass平台用户会话信息,这时候可以借助中间件,通过中间件传递用户信息在上一步的基础上,仍然将第三方应用注册到本平台第三方平台需要监听特定的消息队列本平台用户登录成功后,向消息队列推送当前用户会话信息第三方平台从消息队列解析用户信息,执行自动登录方案二:将本平台作为一个应用注册到第三方系统此种方案需要提供本平台用户登录的URL给第三方系统,同样需要考虑同域和非同域的情况同域需要根据情况实现特殊单点登录SPI服务当本平台与第三方系统同域时,第三方系统将会话信息(识别用户即可)写入浏览器Cookie,随即跳转至本平台登录页面,特殊单点登录SPI服务(需要实现)读取Cookie中的用户信息,对用户进行同步、免登录等。通常这种方式涉及到2个系统的对接,需要对方的开发人员提供一些配置信息,比如本系统提供登录的URL,而第三方系统提供登录回调的URL,这样的话在本系统完成登录的逻辑之后才知道调回的路径但是本人经历的项目中是这么做的,提供一个基于spi实现的一个验证token的jar包,这个jar包可立即为一个简单的工程,该工程实现了一个关键的接口,在该接口中实现的逻辑是,校验token,解析出用户信息,然后执行自动登录,并跳转url很明显,这个jar包是作为连接本系统和第三方系统用的非同域当本平台与第三方系统非同域时,第三方系统无法将会话信息写入浏览器Cookie,这时候需要实现中间件部署在与本平台同域环境中,第三方系统先跳转至中间件页面,通过中间件将用户信息写入Cookie,最后跳转至UYUN平台登录页面,特殊单点登录SPI服务(需要实现)读取Cookie中的用户信息,对用户进行同步、免登录等。图中的中间件可理解为一个需要通过消息中间件实现用户信息传递的一个工程,该工程部署在和本平台同域的环境上方案三:CAS单点登录这种方案的实现网上可以参考的资料非常多,也是当下比较简单的一种实现,开发者只需要对cas证书做配置,这里不再赘述了
0
0
0
浏览量2020
开着皮卡写代码

Java会话技术之 —— cookie与session

前言说到cookie与session,想必大家都不陌生,写过单机模式下的登录业务逻辑的应该多少都会接触到cookie与session,对于cookie和session,很多同学第一反应就是cookie是存储再客户端浏览器的,而session是放在服务端的对网上一大堆的关于对比cookie与session技术的,对于面试来讲,临时突击加以记忆还是可以的,但说到具体的使用以及原理,还是有必要对其做一些深入的理解和探讨首先我们通过一个具体的小案例来看看cookie与session的由来业务场景假如在单机模式下,系统需要先进行登录,然后才能访问其他的资源(接口)1、环境准备快速搭建一个springboot工程,提供两个接口,登录接口和获取用户信息接口@RestController public class LoginController { @GetMapping("/login") public String login(HttpSession session,HttpServletRequest request,HttpServletResponse response, String username, String password) { if(username.equals("admin") && password.equals("123456")){ session.setAttribute("login_info",username); return "login success:" + username; } return "login fail:" + username; } @GetMapping("/info") public String getLoginInfo(HttpSession session,HttpServletRequest request,HttpServletResponse response) { return "info:" + session.getAttribute("login_info"); } }首先调第一个登录接口:http://localhost:8081/login?username=admin&password=123456接着调第二个获取用户信息接口:在此,我们注意到,通过F12观察到cookie那一栏中,在登陆成功后会产生一个 :jsessionid = B8F3038B28A83185F50BEB116CB97805而session与cookie的由来就要从这里说起2、cookie与session产生的背景上图是一个非常简单的客户端通过浏览器访问服务端的示意图,由于浏览器发起请求是无状态的,即请求不会直接保存客户端的会话信息,但是服务端为了安全考虑以及记录客户端请求的会话信息,于是需要一种中间介质,可理解为缓存空间,于是各自保存一下本次的会话信息,而对于双方来说,只需要一个凭证用于标识对方的身份,这个标识就是上面的JSESSIONID对于客户端来说,保存的位置就是浏览器中看到的cookie,而服务端来说,就是session,那么对于上面的两个接口可以拆成下面两步:用户登录成功后,服务端保存JESSIONID至session,同时浏览器将JESSIONID保存至cookie,这一步通过浏览器的接口请求的header信息也可以看到,可理解为,客户端登录时顺便携带了这个JESSIONID作为凭证告知服务端,这个就是双方确认身份的凭证2、再次调用获取用户信息接口时,客户端在请求头中携带上面的JSESSIONID,服务端从session中解析这个JSESSIONID的值,解析到了,就从session中取出来,这里不管是通过断点还是浏览器的请求header都可以反映出这个信息3、清掉cookie,再次发起调用用户信息接口时,这一次 /info接口携带新的cookie,服务端拿到这个值去session中找并不存在,因此返回了null4、更换浏览器,在调用了登录接口之后,通过另一个浏览器访问,无法获取到用户的信息,这说明cookie具有禁止跨域的操作5、重新启动后端服务,在浏览器没有做变更的状态下,再次访问获取用户信息接口,发现未能获取到用户的信息,说明服务端保存session信息是存在于tomcat的进程中6、使用上述相同的工程再启动一个相同的服务,以端口区分,然后8081端口依次访问登录,获取用户信息,8082获取用户信息(同一个浏览器)很明显,8081和8082是两个服务对应着不同的进程,8082拿着8081的JSESSIONID去请求第二个服务的用户信息肯定是请求不到结果的从第六步的演示,我们开始思考一个问题,同一个服务部署多份的情况下(集群部署),如何才能获取到任一台服务上的资源呢?于是就产生了分布式会话技术最后再简单总结下Cookie(Session)的技术点Cookie属于客户端(浏览器)技术,session属于服务端(tomcat)技术各自记录JSSESIONID的值,作为双方互认的凭证,可用于登录校验中(通过header头中携带cookie的信息,服务端解析Cookie进行前置校验)Cookie具有不可跨域性服务端的session信息的生命周期随着容器的销毁而销毁Cookie可以在浏览器进行修改、删除Cookie可以设置有效期(过期时间)本篇通过案例演示说明了cookie与session的使用和原理,由于从cookie的原理衍生出了众多的分布式会话解决方案,但是其核心原理都是从这里出发,希望能够深入理解,本篇到此结束,最后感谢观看!
0
0
0
浏览量1857
开着皮卡写代码

Java会话技术之 —— Spring Session

前言在上一篇我们聊到了会话技术的基础原理中session和cookie的使用,基于cookie和session可以实现客户端(浏览器)和服务端的会话存储,从请求的无状态变为一定程度的有状态,在文章最后,通过一个简单的演示,看到这样一种现象,即在分布式环境下,假如客户端第一次携带着JSESSINID访问了A服务器的某个接口,再次访问B服务器相同服务的相同接口时,却发现获取到的JSESSINID值为null很明显,在分布式环境下,基于单机模式下的session和cookie的值是无法跨进程互通,于是我们想,是否可以通过某种方式将JSESSINID或者说一个客户端与服务端的交互凭证存放在某个存储介质中呢?答案是肯定的在Java中,提供了多种对于此问题的解决方案,目前使用较多也比较成熟的方案像spring-session,token + redis ,JWT,oauth2等,都可以实现在分布式环境下session共享问题,当然这些方案并不是相互隔离的,可以组合搭配,甚至可以基于某一种做定制化的方案都可以下面来探讨下小而美的分布式session共享的解决方案的spring-sessionspring-session基本原理spring-session提供了一种扩展存储分布式会话session的解决方案,即通过引入spring-session的相关依赖,可以将会话的session信息存放到指定的存储介质中,可以是mongodb,redis,mysql等,实现自由灵活的存取下面演示基于redis存储session信息实现分布式会话问题的解决1、添加基础依赖 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.1.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.2.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.18</version> </dependency> </dependencies> 2、配置ymlserver: port: 8081 spring: #redis基础配置 ################################### redis: port: 6379 host: localhost database: 1 #session配置 ################################### session: store-type: redis timeout: 3600s 可以看到,spring-session提供了丰富的session存储方式,可以根据自己的情况自由选择3、编写登录接口和获取登录信息接口@RestController public class LoginController { @GetMapping("/login") public String login(HttpSession session,HttpServletRequest request,HttpServletResponse response, String username, String password) { if(username.equals("admin") && password.equals("123456")){ session.setAttribute("login_info",username); return "login success:" + username; } return "login fail:" + username; } @GetMapping("/info") public String getLoginInfo(HttpSession session,HttpServletRequest request,HttpServletResponse response) { return "info:" + session.getAttribute("login_info"); } }仍然按照之前的测试方式,通过浏览器观察下session的信息,调用接口:http://localhost:8081/login?username=admin&password=123456这时候发现,cookie一栏不再有JSESSIONID,而是一个SESSION,说明使用了spring-sesion的方式之后,会话信息的存储方式发生了改变再通过redis的客户端工具观察下redis库中的session存储情况,发现这里多出来了很多和session相关的信息,比如expires表示会话的过期时间等(具体的过期时间可以从TTL的时间看出来)这时,再去访问获取会话信息接口:既然看到session的信息存储到了redis,即在过期时间范围内持久化了,就算重启,也可以访问,即重启后,再次访问上面的接口仍然可以看到 info:admin重启后,再次访问,依然可以访问到会话的信息,下面再启动一个相同的服务,使用端口进行区分启动成功后,访问:http://localhost:8082/info,发现也可以成功访问到相同的session信息,这样就简单实现了在分布式环境下会话共享的问题一个扩展配置在使用redis存储会话信息时候,默认情况下,保存会话信息的key前缀是以spring开头的,在实际开发过程中,我们希望key带有一定的业务标识,比如redis中存储的会话信息,希望和登录用户的ID有关联,就可以使用下面的这个配置:session.redis.namespace即存储在redis中的session的key就可以按照自定义的方式存储了,删除Session信息重启下项目再次测试,可以看到,可以的前置就是自定义的了而实际在开发过程中,更通用的做法是,将保存在redis中的会话信息的key和登录用户的ID进行关联,即在用户登录成功后,保存到redis中,后续在会话有效的时间范围内,用户再次访问时候,通过网关或者通用拦截器校验用户的会话信息即可,着在spring-session中该如何实现呢?这里留下一个小小的疑问,有兴趣的同学可以深入探究下
0
0
0
浏览量2013
开着皮卡写代码

【微服务】API治理发展历史与未来趋势

一、前言API的出现和广泛使用让跨系统,跨应用之间的连接交互更紧密,也更加便捷。随着网络基础设施、软硬件等环境臻于完善,互联网在近些年得到飞速发展。不管是我们熟悉的各类app,微信小程序,还是各类行业生态系统,智能AI等工具,站在API的肩膀上,在各自赛道实现行业价值的同时,也在跨界融合与交互中展现了强劲的生命力。既然API的作用这么大,它究竟是如何治理的呢?为了尽可能发挥API价值,如何在生产实践中进行治理呢?未来,如何更好的治理我们的API,以期在复杂多变的环境中立于不败之地呢?  二、API治理的价值和意义2.1 API治理概念狭义的API治理是指企业制定并采取一定的适合自身发展现状的管理策略和组织方式,对应用程序编程接口(API)进行集约化的有效管理和控制的过程。2.2 API治理价值和意义互联网企业的应用系统或软件产品,都是为用户提供使用价值,为企业产生利润和商业价值而存在,API作为产品的底层基础设施,可以说就是企业最大的一笔宝贵的无形资产。总结来说,API治理的价值主要体现在如下几个方面;2.2.1 提升团队协同效率规范的API管理让团队在业务交互协作中省时省力,甚至可以减少很多不必要的沟通成本。2.2.2 降低产品运维成本当API管理日臻完善,一旦产品出现线上故障时需要进行问题排查定位,比如界面上出现某个交互动作异常,就能方便的追溯出现问题的API接口,这种情况在复杂的微服务调用链路中进行问题定位具有重要的意义。2.2.3 识别和降低系统的外部风险完善的API管理可以有效识别外部潜在的风险,比如安全漏洞、合规性查验、恶意攻击、性能低效,从而避免给系统带来不必要的损失。2.2.4 提供更多的拓展性完善的API治理,可以让系统在面临复杂多变的外部环境因素中始终留有宽裕的可拓展性,具体来说,如果你的API在设计之初,就能按照主流的编码规范进行设计和开发,比如职责单一、分层抽象、可复用性强等原则,在未来对接口做更多的定制开发、集成第三方组件、甚至跨语言调用等场景下,就具备更好的拓展性。三、API生命周期管理在谈API治理之前,需要先搞清楚一件事,那就是API的生命周期如何?搞清这个问题的意义在于,只有了解了API的生命周期,以及生命周期的各个阶段要做的事情,才能指导API治理工作的开展,从而有更清晰的方向。大体来说,API的生命周期主要分为如下几个阶段。3.1 规划阶段任何涉及API开发的需求都是从规划和设计开始的。在这个阶段,业务人员,产品经理,开发人员等需要明确需求,然后初步确定API应该公开哪些服务和功能,并输出为功能和非功能的全面需求文档。当完成API需求的梳理后,开发团队将根据行业规范的命名规则,体系结构和要使用的特定协议,做出符合实际需求的设计决策。在这个阶段,将会产出一份API设计规范的文档,该规范描述了接口支持的方法、参数、请求类型等操作以及任何技术约束。3.2 开发阶段有了API开发的规范文档,接下来就是开发编码,开发将会根据API设计规范在规定的时间内完成相关API的研发工作,在这个阶段,可能在研发过程中进行微调,比如接口的出入参,参数校验等规则,将会结合实际情况进行适当的调整。3.3 测试阶段一般来说,当API研发完毕,并完成相关的交互联调后,就交付测试,测试人员将结合测试用例进行API测试,功能测试,性能测试,验收测试等,测试的过程,也是进一步暴露API问题的过程,只有通过测试的API才是有意义的产物。3.4 部署与实施阶段经过测试的产品,将投产运行。也可以说,当一个API稳定且安全,就可以投入生产了。在投产之后,用户使用系统过程中,将会更深层次的检验API的安全性、稳定性、性能指标等因素,同时,用户可能会对现有的API提出新的要求,比如当前的API返回参数不满足生产需求,就需要在后续的迭代中继续开发以满足用户的实际需要。从这个角度讲,API产生之后也并非静止状态,而是处于不断的迭代、优化与完善的循环中。3.5 稳定或退役阶段当系统经过较长周期的持续迭代后,核心的API已经非常稳定了,但是这些API已经在为用户提供使用价值,这样的API就处于稳定阶段,这样的API只需要做好维护,或者对其进行新的商业价值的挖掘。当然,随着业务的发展和时间的推移,某些API已经不再使用,或者被其他的API替代,这样的API就可以考虑对其进行废弃或退休。四、API治理发展历史在早期,API的治理的重要意义并不被很多团队所意识到。一方面是早期的软件架构相对简单,另一方面,受制于互联网技术的发展,当时的软件产品的使用量,系统的业务体量也远远达不到今天的规模,所以API的治理过程也是随着互联网的发展脚步,经历了一个相对漫长的发展历史。4.1 粗放的技术实现阶段最初,技术团队对于API的定位是快速实现业务需求,满足用户交付,以期快速实现商业价值为目的,对于API管理本身并不是很重视,所以经历过早期软件开发阶段的伙伴可以看到一个有趣的现象就是,只要能实现功能,你可以采用webservice实现,基于http形式的实现,甚至是符合soap规范的实现等都允许,这种百花齐放的状况让程序员感到很苦恼,因为他们在与外部厂商或其他系统对接的时候将不得不重新对里面涉及到的各种不同的技术。这种状况的存在,也让团队在进行技术标准化的进程中带来了很多麻烦和挑战。4.2 制定与完善API标准阶段随着技术的发展,尤其是持续交付带来的技术革新,让更多的技术团队意识到一套完善的API管理规范在交付实施过程中对提升效率的重要性。典型的场景就是,交付时,客户需要系统的各类文档,操作手册等,这种情况下,业务团队将倒逼技术团队开始认真重视对API规范的管理。于是,越来越多的互联网公司和技术团队开始在开发之初就对API相关的各类标准规范进行梳理和完善,在这个阶段,可以看到市场上陆续出现各类API管理的小工具,在线协作工具,甚至是一些自动化生成API管理文档的各类插件。有了这些相对规范的API标准文档,交付的效率高了,开发团队在协作、日常开发运维等工作中也更加高效。4.3 统一API标准阶段在API管理规范在行业内的认知越来越完善,并且在越来越多的开发团队得到广泛的推广应用时,如何统一API标准成了势在必行的事情。在API发展的过程中,截止到当下,API的风格经历了多个阶段,比如大家熟知的soap协议规范,以及目前主流的restful风格的规范等,这些不同类型API的成型,是经过众多互联网企业的大量生产实践与探索经验的结果。可以肯定的是,这种应用实践的历程,为充分发掘API的使用价甚至是商业价值提供了宝贵的经验。同时,统一标准的制定,为跨语言,跨组件,跨平台等更多全新的技术形态提供了基础的技术储备。比如大家熟悉的微服务,正是得益于API的各类标准的完善,包括协议栈、参数规范、请求类型、数据响应等多维度的统一,使得微服务架构的大规模生产实施变得容易。当然,这也为主流API风格的微服务应用进行容器化、云上部署等提供了基础。4.4 API周边生态培育阶段伴随着云计算、大数据、人工智能等技术的发展,API在各类应用系统中承载的角色和地位也变得越来越重要,尤其是业务体量和用户数量爆发式的增长,API作为系统核心数据资源的承载入口,在各类业务场景下发挥着不可替代的作用。比如当系统API资源足够丰富的时候,可以合理开放API资源给第三方、友商、跨行业平台等外部使用。再比如,针对API接口的链路追踪与监控,当应用系统的复杂性越来越高,API的调用网络也越加复杂的时候,如何对API进行监控是很多公司亟待解决的问题,这也就产生了一大批提供API监测的开源产品和厂商,为API的生态家族提供了有力的支撑与补充。4.5 API与网关融合阶段微服务治理方案的稳步推进和大规模的使用,让很多人看到互联网项目的微服务化在“三高”项目(高并发,高性能,高可用)演进过程中的潜力,与此同时,“三高”指标对应用系统提出了更高的要求。这几年,随着可观测性思想的普及,人们对于API在运行过程中隐藏的各类指标信息,期望以一种更加直观的方式去了解,去掌控。把用户的需求转为通俗的解释就是说,怎么能够实时了解系统中使用的API调用情况如何?占用的服务器资源如何?经历了怎样的调用过程?调用频次如何,那些API被调用的很频繁?核心API调用时长...这一系列问题汇聚起来就是希望对API的治理有一个可视化的窗口,以方便对API集中治理。在这样的大背景下,API网关就应运而生了。那么API网关与微服务的发展有何关系呢?可以这么说,正是微服务治理解决方案的日渐成熟推动了API网关的不断完善。API网关的诞生,让API治理维度范围不断扩大,从早期Nginx网关仅仅满足API的路由转发,到后来微服务网关可以对API各种细腻度的治理,比如安全校验,链路日志埋点,统一认证,黑白名单处理,批量正则拦截等丰富的可定制化功能。而容器化,云原生的兴起,让API网关也进一步升级,这也诞生出了更多符合云原生部署架构的API网关,而这些云原生网关的发展也在微服务网关的基础上对API的治理有了更灵活的伸展性,以及补充了更多有关API治理的内涵和外延。可以说,不管是微服务网关,还是云原生网关,它们的出现和发展,让原来静态化的API具备了动态的能力,因为网关的存在让API暴露在一个可视化的世界中,不管是开发、测试还是生产部署以及后续的运维监控,从此API的一举一动尽收眼底,对API的治理将进入一个正向的有价值的循环中。4.6 API工具化与产品化阶段当系统API资源的使用覆盖到产品研发的各个层次人员之后,下一步就是如何将API进行工具化和产品化,简单来说就是,对一个API使用者,系统的API资源如何调用?API列表在哪里?如何快速找到我期望的API?目标API的结果是否能够满足客户的交付需求?现有的API是否能够打通与第三方系统对接的某个需求...这一系列的需求,尤其是对于那些平台化的产品显得格外突出,为什么这么说呢?因为平台化的产品背后,可能是几十几百,甚至成千上万个服务单元在协同,共同支撑整个平台业务的正常运转。在这样规模的平台下,各服务单元之间的协作可以说离不开API的互相调用,当某个服务单元的开发人员需要对接另一个服务单元的业务时,怎么能快速找到符合要求的API呢?基于这样的需求背景,API协同开发与交互成了很多开发团队的痛点。于是,各类API管理工具产品也应运而生,比如大家熟悉的postman,或在线API调用的小工具等,有了这些API管理工具的支持,业务团队的在API层面的协作中变得容易,同时,团队内各种角色的人员也可以基于这些可视化的API管理工具高效开展自身的工作。更有意义的事情是,API管理产品让企业的API资源有了一个类似git这样集中托管的“仓库”,换言之,API管理产品让企业的API资源有了栖身之所,从而为企业、为团队的高效协作提供了更多的基础。五、API治理未来趋势ChatGPT 的诞生相信让大很多人看到了API提供即时、高效、近乎准确的各类查询服务的强大能力。在大数据、人工智能、物联网等逐步迈向快车道的同时,API在背后赋予的价值和意义也必将越来越重要。从互联网的发展路径来看,发展到今天,服务化的应用大致经历了传统单体服务,微服务治理的大规模应用,再到今天容器化的探索与治理,可以说,其核心就是围绕如何建立更高效、更便捷、更稳健以及更加体系化的API治理环境在努力。当前,目之所及的是,跨应用、跨语言、跨平台、跨行业、多场景的交互与协作逐渐成为常态化的现象,也就是对于各个API服务的提供商来说,如何基于客户需求场景和体验,在既有产品API能力的基础上,并能借助大数据、生成式AI的能力,快速构筑产品多维度,多场景下解决方案的API能力将成为很多互联网公司重点布局的方向。基于此,在不久的未来,API的治理方向可以从三个层次来看:底层将依托于企业强大的数据整合能力、计算能力、模型建造能力、与生成式人工智能整合能力,持续丰富API的多维度提供数据价值的能力,形成丰富的API资源库;中层将结合产品自身、外部环境、客户定制化需求等抽象出更更富的API使用场景,以高效的交付、对接能力以应对更复杂的外部场景;最顶层,则以商业化的视角,将企业的API资源以工具化、产品化的能力对外输出,打造和运营符合产品定位的API生态圈,并加快在跨界整合、跨行业融合、跨平台交互等使用场景中API商业价值的变现能力。
0
0
0
浏览量2013
开着皮卡写代码

tomcat基础架构剖析

前言关于tomcat,可以从很多个维度去分析,作为一款优秀的JavaEE容器,从架构设计,线程模型,设计模式等诸多方面可以成为我们日产工作的借鉴,打算通过几个小节的深入研究探讨一下tomcat中的核心内容本篇从tomcat的总体架构出发,了解一下其优秀的架构设计一次请求的完整过程不管是使用springmvc,springboot还是servlet,一个请求从客户端发起到最终收到响应,大致经历的过程如下:容器接收到请求解析请求参数,并包装为容器的请求,根据请求路径匹配映射的servlet容器servlet容器根据请求参数处理请求并返回结果当然,这只是一个非常粗的步骤,在springmvc框架中,将这么几步拆分开去,又可以细化到更细腻度的步骤,但springmvc或者springboot,底层也是基于tomcat原生的servlet的思想,因此有必要先了解一下tomcat是如何处理一个完整的请求的理解上面的这幅图首先我们可以知道,tomcat为了解耦,一个请求到达HTTP服务器,并不直接调用Servlet,而是把请求交给Servlet容器来处理,因此可以说,了解了servlet的工作原理,就基本上掌握了tomcat的工作原理了servlet服务器请求处理流程当客户请求某个资源时,HTTP服务器会用一个ServletRequest对象把客户的请求信息封 装,然后调用Servlet容器的service方法,Servlet容器拿到请求后,根据请求的URL 和Servlet的映射关系,找到相应的Servlet,如果Servlet还没有被加载,就用反射机制创 建这个Servlet,并调用Servlet的init方法来完成初始化,接着调用Servlet的service方法 来处理请求,把ServletResponse对象返回给HTTP服务器,HTTP服务器会把响应发送给 客户端以上就是servlet容器的整个工作过程,但是到达servlet容器并处理请求,整个过程是由许多个小的步骤,各个组件的配合共同完成的,让我们分别来做了解Tomcat整体架构我们知道如果要设计一个系统,首先是要了解需求,我们已经了解了Tomcat要实现两个 核心功能1) 处理Socket连接,负责网络字节流与Request和Response对象的转化2) 加载和管理Servlet,以及具体处理Request请求因此Tomcat设计了两个核心组件连接器(Connector)和容器(Container)来分别做这 两件事情。连接器负责对外交流,容器负责内部处理。从大的方向来看,连接器和容器可以将tomcat从逻辑结构上进行一次划分连接器 - CoyoteCoyote 是Tomcat的连接器框架的名称 , 是Tomcat服务器提供的供客户端访问的外部接 口。客户端通过Coyote与服务器建立连接、发送请求并接受响应,对应于tomcat源码中的Connector类Coyote 封装了底层的网络通信(Socket 请求及响应处理),为Catalina 容器提供了统一 的接口,使Catalina 容器与具体的请求协议及IO操作方式完全解耦。Coyote 将Socket 输 入转换封装为 Request 对象,交由Catalina 容器进行处理,处理请求完成后, Catalina 通 过Coyote 提供的Response 对象将结果写入输出流,可以说,Coyote提供了一个类似中间人的角色,承接着socket的http请求到httpServletRequest请求的转化传递,在tomcat源码中CoyoteAdapter的实现,后续章节中会加以说明Coyote 作为独立的模块,只负责具体协议和IO的相关操作, 与Servlet 规范实现没有直 接关系,因此即便是 Request 和 Response 对象也并未实现Servlet规范对应的接口, 而 是在Catalina 中将他们进一步封装为ServletRequest 和 ServletResponse在研究tomcat的过程中,个人认为最好是从server.xml文件看起,这个文件的层次结构可以说很大一部分程度上反映了tomcat逻辑上的各个组件的结构,主要是这个文件的层次感太强了,组件之间的嵌套关系一眼便知,下面贴出了tomcat8X版本的srver.xml的配置文件,去掉了注释<?xml version="1.0" encoding="UTF-8"?> <Server port="8005" shutdown="SHUTDOWN"> <Listener className="org.apache.catalina.startup.VersionLoggerListener" /> <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" /> <!-- Prevent memory leaks due to use of particular java/javax APIs--> <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /> <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" /> <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" /> <GlobalNamingResources> <Resource name="UserDatabase" auth="Container" type="org.apache.catalina.UserDatabase" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" pathname="conf/tomcat-users.xml" /> </GlobalNamingResources> <Service name="Catalina"> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /> <Engine name="Catalina" defaultHost="localhost"> <Realm className="org.apache.catalina.realm.LockOutRealm"> <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/> </Realm> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t "%r" %s %b" /> </Host> </Engine> </Service> </Server> 想要搞清楚tomcat的各种流程图或者架构图,对该文件的各个关键标签的含义,有必要进行理论上上的掌握连接器组件在上面的一张图中,我们简单了解了tomcat主要可以分为连接器(connector)和容器(Container),那么它们各自的组件下,又由那些小的组件构成呢?从server.xml也可以看出来,一个容器可能对接多个连接器。但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作Service组件。这里注意,Service本身没有做什 么事情,只是在连接器和容器外面多包了一层,把它们组装在一起。Tomcat内可能有多个Service,这样的设计也是出于灵活性的考虑。通过在Tomcat中配置多个 Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。这就解释了我们可以在webapps下部署多个不同的war包了吧,因为它们归属于不同的 service啊连接器具体的组件连接器中的各个组件的作用如下:EndPointEndPoint : Coyote 通信端点,即通信监听的接口,是具体Socket接收和发送处理 器,是对传输层的抽象,因此EndPoint用来实现TCP/IP协议的Tomcat 并没有EndPoint 接口,而是提供了一个抽象类AbstractEndpoint , 里面定 义了两个内部类:Acceptor和SocketProcessor。Acceptor用于监听Socket连接请求。 SocketProcessor用于处理接收到的Socket请求,它实现Runnable接口,在Run方法里 调用协议处理组件Processor进行处理。为了提高处理能力,SocketProcessor被提交到 线程池来执行。而这个线程池叫作执行器(Executor),我在后面的专栏会详细介绍 Tomcat如何扩展原生的Java线程池ProcessorCoyote 协议处理接口 ,如果说EndPoint是用来实现TCP/IP协议的,那么 Processor用来实现HTTP协议,Processor接收来自EndPoint的Socket,读取字节流解 析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理, Processor是对应用层协议的抽象ProtocolHandlerCoyote 协议接口, 通过Endpoint 和 Processor , 实现针对具体协 议的处理能力。Tomcat 按照协议和I/O 提供了6个实现类 : AjpNioProtocol , AjpAprProtocol, AjpNio2Protocol , Http11NioProtocol ,Http11Nio2Protocol , Http11AprProtocol。我们在配置tomcat/conf/server.xml 时 , 至少要指定具体的 ProtocolHandler , 当然也可以指定协议名称 , 如 : HTTP/1.1 ,如果安装了APR,那么 将使用Http11AprProtocol , 否则使用 Http11NioProtocolAdapter由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request类 来“存放”这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。 但是这个Request对象不是标准的ServletRequest,也就意味着,不能用Tomcat Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter,这是 适配器模式的经典运用,连接器调用CoyoteAdapter的Sevice方法,传入的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调用容 器的Service方法容器 - CatalinaTomcat是一个由一系列可配置的组件构成的Web容器,而Catalina是Tomcat的servlet容 器,从server.xml中基本上可以猜到,servlet容器包含的主要组件Catalina 是Servlet 容器实现,包含了之前讲到的所有的容器组件,它通过松耦合的方式集成 Coyote,以完成按照请求协议进行数据读写。同时,它还包括我们的启动入口、Shell程 序等Tomcat 本质上就是一款 Servlet 容器, 因此Catalina 才是 Tomcat 的核心 , 其他模块 都是为Catalina 提供支撑的。 比如 : 通过Coyote 模块提供链接通信,Jasper 模块提供 JSP引擎,Naming 提供JNDI 服务,Juli 提供日志服务,这些从server.xml文件中可以找到对应的标签如上图所示,Catalina负责管理Server,而Server表示着整个服务器。Server下面有多个 服务Service,每个服务都包含着多个连接器组件Connector(Coyote 实现)和一个容器 组件Container。在Tomcat 启动的时候, 会初始化一个Catalina的实例Catalina 各个组件的职责:Catalina:负责解析Tomcat的配置文件 , 以此来创建服务器Server组件,并根据 命令来对其进行管理Server:服务器表示整个Catalina Servlet容器以及其它组件,负责组装并启动 Servlet引擎,Tomcat连接器。Server通过实现Lifecycle接口,提供了 一种优雅的启动和关闭整个系统的方式Service:服务是Server内部的组件,一个Server包含多个Service。它将若干个 Connector组件绑定到一个Container(Engine)上Connector:连接器,处理与客户端的通信,它负责接收客户请求,然后转给相关 的容器处理,最后向客户返回响应结果Container:容器,负责处理用户的servlet请求,并返回对象给web用户的模块Engine:表示整个Catalina的Servlet引擎,用来管理多个虚拟站点,一个Service 最多只能有一个Engine,但是一个引擎可包含多个HostHost:代表一个虚拟主机,或者说一个站点,可以给Tomcat配置多个虚拟主 机地址,而一个虚拟主机下可包含多个ContextContext:表示一个Web应用程序, 一个Web应用可包含多个WrapperWrapper:表示一个Servlet,Wrapper 作为容器中的最底层,不能包含子容器也可以再通过Tomcat的server.xml配置文件来加深对Tomcat容器的理解。Tomcat 采用了组件化的设计,它的构成组件都是可配置的,其中最外层的是Server,其他组件 按照一定的格式要求配置在这个顶层容器中<Server > <Service > <Connector /> <Connector /> <Engine> <Host> <Context></Context> </Host> </Engine> </Service> </Server> 容器通过 Pipeline-Valve 责任链,对请求一次处理,invoke 处理方法,每个容器都有一个 Pipeline,触发第一个 Valve,这个容器的 valve 都会被调到,不同容器之间通过 Pipeline 的 getBasic 方法,负责调用下层容器的第一个 Valve当然,如果想更深入的了解这些图的内涵,最好还是需要通过源码的方式进行调试,那样才能更深刻的了解其背后的原理
0
0
0
浏览量1464
开着皮卡写代码

tomcat启动流程分析

前言tomcat究竟在启动的时候做了哪些事情呢?从直观上讲,当tomcat启动完毕后,部署在tomcat里的项目,就可以通过外部的http形式进行访问了,但有个疑问是,为什么可以访问呢?其内部都做了哪些准备工作呢?本篇通过源码来了解一下tomcat启动的过程吧通过上一篇的分析,我们初步了解了tomcat内部的基本构成,包括的基本组件,这些组件的配合构成了tomcat的逻辑上的架构从大的方面划分,tomcat在启动过程中,主要完成了2件事情,第一初始化容器组件,第二启动相关的线程等待读写事件的接入tomcat 的IO模型从tomcat8之后,tomcat默认使用的是NIO,即异步IO,我们知道,在tomcat8之前的版本中有大多使用同步IO,同步IO和异步IO相比,在高并发的请求处理场景中性能相差非常大,这个得益于NIO的底层特殊的线程处理机制,即reactor模型reactor模型简单解释来说,reactor模型将一个请求的处理过程划分成不同的步骤,在bio中,一个请求过来了,tomcat提供一个线程处理,如果请求没有处理完毕,这个线程将一直阻塞,但通过NIO的方式,将工作线程和接收请求的线程分开,并通过非阻塞的IO去处理,就大大提升了整体的性能和吞吐量小编从实际的压测结果来看,使用tomcat7的BIO的方式,和tomcat8的NIO的方式在相同条件下进行压测,tomcat8的吞吐量比tomcat7要多出1.5倍,这只是在本地环境模拟的效果omcat8默认使用的是NIO的方式,在server.xml中可以找到下面的这段配置,即在HTTP/1.1的情况下<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />我们可以修改protocol属性使用NIO2 (异步非阻塞)<Connector port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" connectionTimeout="20000" redirectPort="8443" /> 简单了解了tomcat8的IO模型,我们正式来看tomcat的启动流程,初始化各组件+线程就绪,即init+start关于init过程回忆上一篇我们谈到的关于tomcat的基本组件的构成,见下面这张图其实不难理解,tomcat在启动过程中,也是按照server.xml中各个组件标签的顺序依次进行初始化的(各个组件的作用见上一篇)其大概的步骤如下:1.调用 bin/startup.bat (在linux 目录下 , 需要调用 bin/startup.sh) , 在startup.bat 脚本中, 调用了catalina.bat2.在catalina.bat 脚本文件中,调用了BootStrap 中的main方法3.在BootStrap 的main 方法中调用了 init 方法 , 来创建Catalina 及 初始化类加载器4.在BootStrap 的main 方法中调用了 load 方法 , 在其中又调用了Catalina的load方法5.在Catalina 的load 方法中 , 需要进行一些初始化的工作, 并需要构造Digester 对象, 用 于解析 XML6.然后在调用后续组件的初始化操作(见时序图中的组件init过程)7.加载Tomcat的配置文件,初始化容器组件 ,监听对应的端口号, 准备接受客户端请求源码分析,从程序中的Bootstrap入手,我们找到main方法在学习源码之前,我们有必要先简单了解一下时序图中主要组件的类结构,由于所有的组件均存在初始化、启动、停止等生命周期方法,拥有生命周期管理的特 性, 所以Tomcat在设计的时候, 基于生命周期管理抽象成了一个接口 Lifecycle ,而组 件 Server、Service、Container、Executor、Connector 组件 , 都实现了一个生命周期 的接口,从而具有了以下生命周期中的核心方法:init():初始化组件start():启动组件stop():停止组件destroy():销毁组件我们不妨随意找一个组件,像connector来说,其顶级接口还是Lifecycle各组件的默认实现上面我们提到的Server、Service、Engine、Host、Context都是接口。当前对于 Endpoint组件来说,在Tomcat中没有对应的Endpoint 接口, 但是有一个抽象类 AbstractEndpoint ,其下有三个实现类: NioEndpoint、 Nio2Endpoint、AprEndpoint , 这三个实现类,分别对应于前面讲解链接器 Coyote 时, 提到的链接器支持的三种IO模型:NIO,NIO2,APR , Tomcat8.5版本中,默认采 用的是 NioEndpointServerServiceEngineHostContext从各个组件的类的结构图也可以看出,它们都纳管于Lifecycle的生命周期的过程源码启动入口Bootstrap ->main()从main方法入手,可以分为2步,第一步init()的过程,即初始化上述时序图中各个组件的过程第二步,start()过程,调其reactor涉及到的各个线程,下面通过断点简单看其中几个组件的init()过程吧调起catalina的getServer().init()方法调起server的init()在initInternal的init方法中,可以看到像globalNamingResources这样的方法初始化,其实就是在依次解析server.xml中的标签来到本方法最后一段,看到调用了service的init()方法调起service的init()方法,该方法中要初始化的组件比较多,对应于server.xml中的各个层次的标签,举例来说,一个service里面可以配置多个connector,那么就需要在这里进行初始化跳过中间的engine,host和context,我们最后直接来到connector的初始化调起AbstractProtocol的init()方法最终来到endpoint的init()这段代码中,我们似乎看到了socket的痕迹,在前一篇提到,EndPoint是 Coyote的通信端点,即通信监听的接口,是具体Socket接收和发送处理 器是对传输层的抽象,因此EndPoint用来实现TCP/IP协议的如果继续点进bind方法,可以看到其实就是在创建并绑定相应的连接信息那么到这里,初始化的工作就基本完成了,具体的各个组件中初始化代码块和要完成的详细工作,有兴趣的同学可以依次步骤点进去一探究竟下面的start()过程按照时序图,参照上面的过程我们仍然走一遍流程,最终走到AbstractProtocol的start()方法,protected final void startAcceptorThreads() { int count = getAcceptorThreadCount(); acceptors = new Acceptor[count]; for (int i = 0; i < count; i++) { acceptors[i] = createAcceptor(); String threadName = getName() + "-Acceptor-" + i; acceptors[i].setThreadName(threadName); Thread t = new Thread(acceptors[i], threadName); t.setPriority(getAcceptorThreadPriority()); t.setDaemon(getDaemon()); t.start(); } } 来到这里,其实很容易看出来,Acceptor属于一个线程类,继续进入,最终来到NioEndPoint的Acceptor方法中,protected class Acceptor extends AbstractEndpoint.Acceptor { @Override public void run() { int errorDelay = 0; // Loop until we receive a shutdown command while (running) { // Loop if endpoint is paused while (paused && running) { state = AcceptorState.PAUSED; try { Thread.sleep(50); } catch (InterruptedException e) { // Ignore } } if (!running) { break; } state = AcceptorState.RUNNING; try { //if we have reached max connections, wait countUpOrAwaitConnection(); SocketChannel socket = null; try { // Accept the next incoming connection from the server // socket socket = serverSock.accept(); } catch (IOException ioe) { // We didn't get a socket countDownConnection(); if (running) { // Introduce delay if necessary errorDelay = handleExceptionWithDelay(errorDelay); // re-throw throw ioe; } else { break; } } // Successful accept, reset the error delay errorDelay = 0; // Configure the socket if (running && !paused) { // setSocketOptions() will hand the socket off to // an appropriate processor if successful if (!setSocketOptions(socket)) { closeSocket(socket); } } else { closeSocket(socket); } } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString("endpoint.accept.fail"), t); } } state = AcceptorState.ENDED; } 在run方法中我们注意到下面的这句,翻译过来,就是等待请求的连接,总结一下,到这里tomcat通过初始化和start的过程,完成加载容器依赖的各个组件的初始化,并启动IO线程,等待外部请求的接入,下一步,就是具体的请求过来之后如何进行处理的过程,在后续的文章中会继续提到,本篇到此结束,最后感谢观看!
0
0
0
浏览量2007
开着皮卡写代码

elastic-job控制台部署与使用

前言elastic-job官方提供了对于后台应用中的job管控台,可以比较方便的对正在运行的分布式job做一些基本的管理,比如修改job配置参数,启停,废除任务等,下面让我们将控制台部署起来看看效果如何1、git下载elasticjob源码git地址:https://github.com/apache/shardingsphere-elasticjob通过git命令将项目下载到服务器或者本地,2、进入项目根目录,执行编译打包命令mvn clean install -Dmaven.test.skip=true经过漫长的打包,终于将所需的包打好了3、将控制台的jar包拷贝到本地并启动windows环境下,进入bin目录后,可以直接通过start.bat进行启动启动成功后,默认的段开是8899,浏览器访问:localhost:8899,可以看到如下界面管控台使用1、配置zk注册中心通过之前的内容我们知道,es-job依赖zk,因此首先我们需要将zk的配置信息添加进去2、job操作后台启动服务,以上一篇的simpleJob为例,当job开始运行之后,在控制台的作业操作一栏,可以看到任务的相关参数信息已经展示在列表上面了,对应着程序中job的配置参数基于此,我们可以对当前运行中的job做相应的操作详情列出当前job对应的分片项信息,比如当前的这个job设置了2个分片,由于只启动了一个示例,而且在同一台机器,因此IP相同,进程号也一样点击失效这时发现分片项0已经失效,后台不再输出关于分片项0的信息修改点击修改可以看到关于当前simpleJob的完整参数信息,当前的job每5秒执行一次,假如我们将其改为每10秒执行一次,通过控制台打印的时间窗口可以看到已经生效了失效或终止顾名思义,即将当前job停止运行,点击失效后,我们再次观察控制台,发现任务就不再执行了关于任务的界面操作是比较简单的,大家可以尝试下,下面我们通过控制台来看看elastic-job的另一个功能,任务追踪事件追踪Elastic-Job提供了事件追踪功能,用于查询、统计和监控作业执行历史和执行状态。Elastic-Job-Lite在配置中提供了JobEventConfiguration,目前支持数据库方式配置。事件追踪所配置的DataSource数据库中会自动创建JOB_EXECUTION_LOG和JOB_STATUS_TRACE_LOG两张表以及若干索引简而言之,esJob在运行过程中的产生的相关数据会被记录到这两张表中去,方便开发人员快速定位job运行过程中的日志数据或问题等操作springboot集成事件追踪功能在上一篇的项目中,我们增加mysql的依赖和配置 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> 然后再在JobScheduler的配置bean中,将JobEventConfiguration这个配置对象加入即可,以上篇的mySimpleJob为例,修改后的代码如下:@Configuration public class MySimpleJobConfig { @Value("${mySimpleJob.cron}") private String cron; @Value("${mySimpleJob.shardingTotalCount}") private int shardingTotalCount; @Value("${mySimpleJob.shardingItemParameters}") private String shardingItemParameters; @Autowired private ZookeeperRegistryCenter regCenter; @Autowired private MySimpleJob mySimpleJob; @Autowired private DataSource dataSource; @Bean(initMethod = "init") public JobScheduler simpleJobScheduler() { JobEventConfiguration jcf = new JobEventRdbConfiguration(dataSource); return new SpringJobScheduler(mySimpleJob, regCenter, ElasticJobUtils.getSimpleJobConfiguration( mySimpleJob.getClass(), cron, shardingTotalCount, shardingItemParameters),jcf //,new MyElasticJobListener() 可配置监听器 ); } }然后,启动项目,这时可以看到,在数据库中,就会在dataflow库下生成了两张表同时,一旦任务开始运行,job运行过程中产生的数据就会记录到表中去,想必表中的字段信息大家一看便知,就不再过多解释了这里想要说的是,可以通过elastic-job控制台将这些数据展示到界面上,更方便我们观察,怎么操作呢?点击左侧的数据源追踪配置,填写mysql的连接信息这时我们再次启动后台项目,再到控制台中观察作业的历史操作和历史轨迹,可以发现job的执行信息都可以展示在控制台上了
0
0
0
浏览量1490
开着皮卡写代码

【微服务】springboot集成ELK使用详解

一、前言对于一个运行中的应用来说,线上排查问题是一件很头疼的问题。不管是springboot单应用,还是springcloud微服务应用,一旦在生产环境出了问题,大多数人第一反应就是赶紧去看日志查问题。如何查呢?如果是管理不那么严格的项目,允许你登录生产服务器通过命令去查,或者将生产的日志down下来去查。但为了服务器安全,一般来说是不允许研发人员随便接触服务器,会有运维人员去操作日志,这样以来就极大的影响了排查的效率,这时候会有人说,如果有可视化的操作,能可视化检索日志的界面就好了。二、为什么需要ELK一般我们需要进行日志分析场景:直接在日志文件中 grep、awk 就可以获得自己想要的信息。但在规模较大的场景中,此方法效率低下,面临问题包括日志量太大如何归档、文本搜索太慢怎么办、如何多维度查询。需要集中化的日志管理,所有服务器上的日志收集汇总。常见解决思路是建立集中式日志收集系统,将所有节点上的日志统一收集,管理,访问。一般大型系统是一个分布式部署的架构,不同的服务模块部署在不同的服务器上,问题出现时,大部分情况需要根据问题暴露的关键信息,定位到具体的服务器和服务模块,构建一套集中式日志系统,可以提高定位问题的效率。一个完整的集中式日志系统,需要包含以下几个主要特点:收集-能够采集多种来源的;传输-能够稳定的把日志数据传输到中央系统;存储-如何存储日志数据;分析-可以支持 UI 分析;警告-能够提供错误报告,监控机制;基于上述的需求,业界很多公司在不断探索过程中,经过多年实践经验,最终形成了以ELK为主流的一整套解决方案,并且都是开源软件,之间互相配合使用,完美衔接,高效的满足了很多场合的应用,是目前主流的一种日志系统。三、ELK介绍3.1 什么是elkELK其实并不是某一款软件,而是一套完整的解决方案,是三个产品的首字母缩写,即:Elasticsearch;Logstash ;Kibana;这三个软件都是开源软件,通常配合使用,而且又先后归于 Elastic.co 公司名下,故被简称为ELK协议栈,具体来说:Elasticsearch是一个分布式的搜索和分析引擎,可以用于全文检索、结构化检索和分析,并能将这三者结合起来。Elasticsearch 基于 Lucene 开发,现在是使用最广的开源搜索引擎之一。Logstash简单来说就是一根具备实时数据传输能力的管道,负责将数据信息从管道的输入端传输到管道的输出端,与此同时这根管道还可以让你根据自己的需求在中间加上滤网,Logstash提供了很多功能强大的滤网以满足你的各种应用场景。Kibana  是一个开源的分析与可视化平台,设计出来用于和Elasticsearch一起使用的。你可以用kibana搜索、查看、交互存放在Elasticsearch索引里的数据,使用各种不同的图标、表格、地图等,kibana能够很轻易的展示高级数据分析与可视化。3.2 elk工作原理如下是elk实际工作时的原理图,还是很容易理解的Logstash的存在,让数据可视化的展示成为很多需要做日志类数据展示不可或缺的组件,比如数据源可以是静态的日志文件,也可以是mysql,或来自于kafka的topic消息数据等。四、ELK环境搭建下面演示如何搭建elk,网上的参考资料比较丰富,本文采用docker快速搭建起elk的演示环境,参考下面的步骤。4.1 搭建es环境4.1.1 获取es镜像版本可以根据自身的情况选择,我这里使用的是7.6的版本docker pull elasticsearch:7.6.24.1.2 启动es容器使用下面的命令启动es容器,注意这个配置,ES_JAVA_OPTS="-Xms512m -Xmx512m",这个配置参数值根据你的服务器配置决定,一般最好不要低于512m即可;docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms512m -Xmx512m" --name es76 -d elasticsearch:7.6.22.1.3 配置es参数进入到es容器内部,然后找到下面的这个文件然后将下面的配置参数配置进去cluster.name: "docker-cluster" http.cors.enabled: true network.host: 0.0.0.0 http.port: 9200 http.cors.allow-origin: "*"2.1.4 重启es容器并访问配置完成后重启docker容器,重启成功后,开放9200端口,然后浏览器访问,IP:9200,看到如下信息,说明es可以正常使用。4.2 搭建kibana4.2.1 拉取kibana镜像为了减少后面的配置麻烦和一些问题,建议kibana版本与es版本一致docker pull kibana:7.6.24.2.2 启动kibana容器这里的IP地址,如果是云服务器,注意使用内网的IP地址docker run --name kibana -e ELASTICSEARCH_HOSTS=http://es服务IP:9200 -p 5601:5601 -d kibana:7.6.24.2.3 修改配置文件进入到kibana容器中,进入到下面的目录中cd /usr/share/kibana/config vi kibana.yml 将如下的配置信息配置进去(es的IP地址如果是云服务器建议使用内网IP)server.name: kibanaserver.host: "0"elasticsearch.hosts: [ "http://es服务IP:9200" ]xpack.monitoring.ui.container.elasticsearch.enabled: truei18n.locale: zh-CN4.2.4 重启容器并访问上述配置信息配置完成后,重启容器,开放5601端口,浏览器就可以直接访问,IP:5601,看到下面的效果说明kibana可以正常使用了4.3 搭建logstash4.3.1 下载安装包logstash的版本建议不要与es版本差别太多即可wget https://artifacts.elastic.co/downloads/logstash/logstash-7.1.0.tar.gz4.3.2 解压安装包tar -zxvf logstash-7.1.0.tar.gz4.3.3 新增配置logstash文件进入logstash-7.1.0目录下,创建一个目录,用于保存自定义的配置文件,注意提前开发4560端口cd cd logstash-7.1.0/mkdir log-confvi logstash.conf然后添加下面的配置信息input { tcp { mode => "server" host => "0.0.0.0" port => 4560 codec => json } } output { elasticsearch { hosts => "es公网地址:9200" index => "springboot-logstash-%{+YYYY.MM.dd}" }, stdout { codec => rubydebug } }在主目录下,使用下面的命令进行启动./bin/logstash -f ./log-conf/logstash.conf看到下面的输出日志,说明当前logstash服务已经开始工作,准备接收输入日志了五、SpringBoot集成ELK5.1 集成过程参考下面的过程在springboot中快速集成elk,如果是dubbo或者springcloud项目,集成步骤也差不多5.1.1 创建springboot工程项目目录下5.1.2 导入依赖根据需要引入依赖,如果是集成elk,还需要引入下面这个 <dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>5.3</version> </dependency>5.1.3 配置logback日志springboot集成elk最关键的就是配置logback日志文件,需要按照一定的格式规范进行配置,才能将运行过程中产生的日志上报到logstash,然后经过转换输送到es,最后展现在kibana中,参考下面的配置信息,以下两种配置方式都可以;方式一:<?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <springProperty scope="context" name="springApplicationName" source="spring.application.name" /> <property name="LOG_HOME" value="logs/service.log" /> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <!--接收info日志输出到LogStash--> <appender name="LOG_STASH_INFO" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>INFO</level> </filter> <destination>logstash地址:4560</destination> <encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <providers> <timestamp> <timeZone>Asia/Shanghai</timeZone> </timestamp> <!--自定义日志输出格式--> <pattern> <pattern> { "project": "elk", "level": "%level", "service": "${springApplicationName:-}", "pid": "${PID:-}", "thread": "%thread", "class": "%logger", "message": "%message", "stack_trace": "%exception" } </pattern> </pattern> </providers> </encoder> </appender> <root > <appender-ref ref="STDOUT" /> <appender-ref ref="LOG_STASH_INFO" /> </root> </configuration> 方式二:<?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <!--提取配置文件中的服务名--> <springProperty scope="context" name="springApplicationName" source="spring.application.name" /> <property name="LOG_HOME" value="logs/service.log" /> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <destination>logstash地址:4560</destination> <encoder class="net.logstash.logback.encoder.LogstashEncoder" > <!--定义appname的名字是服务名,多服务时,根据这个进行区分日志--> <customFields>{"appname": "${springApplicationName}"}</customFields> </encoder> </appender> <root level="INFO"> <appender-ref ref="STDOUT" /> <appender-ref ref="logstash" /> </root> </configuration> 5.1.4 增加测试接口为了后续方便观察效果,增加两个测试接口,一个模拟正常的调用,另一个模拟异常调用@RestController @RequestMapping("/user") @Slf4j public class UserController { @Autowired private UserService userService; //http://localhost:8088/user/get?userId=001 @GetMapping("/get") public Object getUserInfo(String userId){ log.info("getUserInfo userId:【{}】",userId); Map userInfo = userService.getUserInfo(userId); return userInfo; } //http://localhost:8088/user/error?userId=001 @GetMapping("/error") public Object error(String userId){ log.info("error userId:【{}】",userId); Map userInfo = userService.getUserInfo(userId); int e = 1/0; return userInfo; } }5.2 效果演示5.2.1 启动服务工程启动之后,通过下面的在logstash终端的输出日志信息中,可以发现logstash已经接收到程序中上报过来的日志了,并且内部已经按照预定的格式进行了转换;5.2.2 配置索引模式为了让程序中的日志能够正常展现到es中,由于es是通过接收logstash传输过来的数据,存储到索引中才能通过kibana展现,所以索引的存储格式就很重要,需要提前在kibana上面配置一下索引的展现格式,按照下面的操作步骤配置即可。在kibana中找到下图模式配置入口自定义索引的模式,比如这里选择的就是在上面logstash中配置的名称前缀使用时间戳刷新配置最后进入到索引查看的栏目就可以看到展示的索引中的日志信息了5.2.3 调用接口验证效果依次调用上面的两个测试接口,然后查看kibana中日志的变化调用正常响应的接口接口能够正常响应,由于我们在接口方法中添加了一行输出日志信息,通过上面的搜索框,能够在es的日志信息中搜索出来;调用异常响应接口接口调用异常后,也能通过kibana快速发现异常信息输出通过上面的实验和操作体验,可以感受到在springboot中集成elk之后带来的便利,有了可视化的日志展现,提升问题排查效率的同时,也能更好的统一管理日志,并充分发挥日志的作用。5.3 ELK使用补充上面完整演示了如何在springboot中快速接入ELK进行日志的可视化展示,如果使用的是springloud或dubbo等技术栈,集成步骤类似,这里结合实际经验,补充下面几点以供参考。日志切分与清理展示的日志毕竟是要存储到ES索引中,随着时间的推移,日志文件将会越来越大,索引也将会占用较大的存储空间,如何管理这些源源不断的日志索引呢,给出下面两点建议:原始的日志文件,即logback文件建议按天切分(需要在配置文件中配置策略),这样产生的es索引文件也是按天存储;有了第一步之后,可以通过脚本或者手动的方式定期清理索引;控制日志输出级别不建议使用debug级别的日志级别,这样es中存储的日志索引文件会增长的非常快设置kibana访问密码生产环境中,日志也是非常重要的数据,在很多公司甚至不会对外开放,而是需要通过授权后才能查看,因此如果是在你的生产环境集成ELK,建议设置kibana的访问账户信息。六、写在文末本文详细介绍了如何在springboot中快速接入ELK的过程,ELK可以说在实际项目中具有很好的适用价值,不管是小项目,还是中大型项目,都具备普适参考性,值得深入了解和学习。本篇到此结束,感谢观看。
0
0
0
浏览量1746

履历