北冥有只鱼

北冥有只鱼 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

北冥有只鱼 发布了文章 · 4月11日

JVM学习笔记(一) 初遇篇

记得在实习找工作的时候,有个面试官问了JVM调优的问题,当时好像是这么问的,你知道JVM调优吗? 我说我不是很了解,然后面试官跟我说,你面试官可以了解一下,JVM调一下,会运行的更好。当时的我并不理解JVM为什么要调优,我心里的想法是难道是JVM的某些参数一开始设置的并不合理吗?但是我又想JVM发展了这么长时间了,就算是一开始有这个问题? 到现在都没解决吗?

JVM运行的不佳,有没有可能不是JVM的错,是程序员写的代码质量太差,导致的呢。
后来查了一些资料: 一般的Java项目需要JVM调优吗?,发现JVM多数情况都不需要调优了,但是有的时候程序员写的程序性能不佳,问题并不能全推给JVM。

为什么要学习JVM?

定位问题,有可能系统表现不佳,但是我们一时也无法找出程序是哪里写的不好,所以我们需要给JVM做诊断,看看问题究竟是出在哪里。
要做诊断,首先得了解JVM,本篇并不适合刚学Java的人观看,这会让你迷失。那了解JVM我们具体了解哪里呢?
第一个是运行时的区域划分(这里要跟JMM区分开),第二个是垃圾回收器和垃圾回收算法,第三个是JVM监测工具。
通过这些,我们就可以给JVM做诊断,定位问题。

其实之前也写过类似的文章,只是这次做一个整体的讨论,更成系统。

JVM运行时区域

这也是我的第一篇博客讨论的问题,JVM在运行时,向操作系统申请的内存,都被划分为哪几个区域。也就是这篇文章:

本篇我们对这些概念再度进行讨论,更加系统,更加全面,我在看视频的时候,都会讲堆和栈,但我细究下并非如此。因为对于JVM,有oracle出品的HotSpot VM,也有淘宝出品的TaobaoVM,华为出品的毕昇 JDK等,不少知名的计算机厂商都有自己定制的JDK。那这些魔改,会不会让Java语言出现不同的版本呢? 影响Java一次编写处处运行特性呢? 导致Java四分五裂,答案是并不会,有一个东西叫JVM规范,上面描述了你要想定制JDK,魔改JVM,那么也请在这个范围内进行魔改,如果肆意魔改JDK,许多成熟的开源框架就很可能无法在你定制的JDK上成功运行,为了保证各个厂商定制JDK同时,不影响Java一次编写,处处运行的特性,Oracle推出了JVM规范,只要你不违背规范,就不影响字节码的运行,在规范之外,运行你自由定制。

写到这里,突然想起大学时代一个同学问过我的一个问题,为什么定义接口,让类去实现,我直接在类里面写不就行了吗?
当时的我用的是另一个例子,介绍的接口的用处,假如是团队合作,有一个功能让你完成,这个功能用接口完成,到时候人家调用你的接口就行,不必关心具体实现,这样能够有效的实现解耦,我们可以将接口理解为规约,约定这个方法到时候要实现什么。的确接口更像是规约,因为到时候的实现类不是由你来编写。

JVM规范地址:

现在已经是到15了,因为JDK已经到15了。本次我们研究的是JDK8,到JDK11,JVM的运行时区域基本没发生变化。
同样的JVM规范也对于运行时区域进行了约束,就像是接口,对应的虚拟机实现了规约,就像是实现类。JVM规范规定JVM运行时区域有以下这几个部分:

  • The pc Register 程序计数器
  • Java Virtual Machine Stacks 虚拟机栈
  • Heap 堆
  • Method Area 方法区
  • Run-Time Constant Pool 运行时常量池
  • Native Method Stacks 本地方法栈
    最新的JDK15也是这么划分的,好像一直都没有调整过啊。事实上是有调整的,只不过规范里仍然将运行时区域划分为这几个区域。但是不同的虚拟机似乎有着不同的实现,比如使用面最广的HotSpot VM,在IDEA中做测试:

image.png
image.png
随便找个main函数测试,你会发现控制台会输出以下信息:
image.png
Heap堆,我们知道,那这个Meta Space是什么鬼? HotSpot在规范中要求的运行时区域上又加了一块?并不是的,这其实是方法区的实现,在HotSpot 1.6 中对方法区的实现是永久代,也是垃圾回收的范围之内,与年轻代、年老代相对。1.7版本将字符串常量池移动至堆中。1.8版本彻底移除永久代的实现,用MetaSpace来实现方法区。那我们自然要问为什么HotSpot虚拟机要移除永久代,改用元数据区(MetaSpace)来实现方法区呢?注意上面我们提到了Hot Spot虚拟机,事实上在早期虚拟机有许多版本,不是1.6 1.7 1.8的这种版本,是各个厂商对JVM的实现,我们常用的一般是Oracle提供的JVM,全名应该是HotSpot JVM,其实还有JRockit(Oracle出品,原先Java属于Sun,所以Oracle又自己研发了一个JVM),除此之外还有IBM JVM、Apache Harmony等在Harmony的基础上,谷歌研发了Dalvik。
image.png
然后Oracle收购了Sun,开始了天下一统,将HotSpot和JRokit进行整合,所谓的整合肯定是将各自的优点集成,JRokit就没有永久代这种实现,而且运行良好。那方法区究竟用来存储什么信息呢? 其实对这里存储的信息,JVM规范也做了规定,方法区主要存储: 类和方法的元数据以及常量池,比如Class和Method。每当一个类初次被加载的时候,它的元数据都会放到方法区中。
永久代大小是有限制的,在1.8之前类加载过多,永久代内存设置不当 就可能会导致永久代永久溢出,即万恶的 java.lang.OutOfMemoryError: PermGen ,我们不得不来根据实际情况对调整永久代的大小。在JRokit中就并没有永久代的概念,而且运行良好,不会有恼人的java.lang.OutOfMemoryError: PermGen 。在JEP 122: Remove the Permanent Generation描述了移除永久代,改用元数据区域(就是Meta Space下文统一称元数据区域)实现的原因,还比较清晰,有兴致的同学可以翻翻这份草案。关于元数据区域相关的资料还比较少,也只有Oracle官方的Java虚拟机规范及Oracle Blog有相关的描述,官方的描述如下:

In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace.
类的元数据区被存储在本地堆中,这个区域被称作元空间。

本地堆也就是说直接使用操作系统提供的内存空间,默认的空间大小只受本地内存的限制,粗略的说就是本地内存剩多少,不够了我再跟操作系统申请(一般的操作系统都有虚拟内存),但是让元空间无限大又有些浪费资源,我们可以通过 -XX:MaxMetaspaceSize来指定。除此之外,默认情况下JVM会根据运行概况来动态的调整MaxMetaspaceSize的大小。如果Metaspace的空间占用达到了设定的最大值,那么就会触发GC来收集死亡对象和类的加载器。根据JDK 8的特性,G1和CMS都会很好地收集Metaspace区(一般都伴随着Full GC)。

堆是垃圾回收器的主要工作对象,不同的垃圾回收器对堆的划分又有些区别,在JDK8下面,加上-XX:+PrintGCDetails,打印出来的:

image.png
我们来介绍下加入-XX:+PrintGCDetails后,输出内容代表的含义:

  • PSYoungGen: Parallel Scavenge(并行垃圾回收器) Young Generation (我们常说的年轻代)

    • eden space (伊甸园) 创建对象时申请的内存空间优先使用该区域

      进行垃圾回收后,eden space还存活的对象将会移动至幸存者区域(Survivor Space),幸存者区域被分为两块,一块是To Survivor、From Survivor,这两个区域的空间大小是一样的。年轻代触发垃圾回收时,Eden Space中还存活的对象会被放入到空的幸存者区域(一般是 to Space),另一个幸存者区域(即From Space)里不能被回收的对象也会被放入To Space。然后To Space 和 From Space身份互换。年轻代触发GC之后,都会计算一个晋升阀值(tenuring threshold ,即该对象大于晋升阀值之后被移动至老年代)和各年代区的大小,以及适时地调整大小。
      当Eden区被完全占用,触发垃圾回收,恰巧To 区域不够容纳在Eden Space和From Space进行垃圾回收之后还存活的对象,从JVM角度是想通过晋升阀值来避免的,但是也无法完全保证。
    • from space
    • to space
  • ParOldGen Parallel Old (Parallel Scavenge的老年代版本)

    - object space
  • Metaspace 元空间
    接着我们在JDK11中,做同样的测试,在VM Options加入-XX:+PrintGCDetails,看一下输出与JDK8有什么不同:
    image.png
    从这段输出中,我们首先看到--XX:+PrintGCDetails这个参数在JDK 11 中时过时的状态,建议我们用-Xlog:gc*代替。
    然后发现输出好像除了Metaspace,其他的和JDK8完全不一样。
    这是因为JDK 11 默认使用的垃圾回收器是G1(Garbage-First Garbage Collector 直译为垃圾回收优先回收器),G1弱化了分代的概念,采用分区(Region)的概念来管理内存,G1将堆分成相同大小的分区:
    image.png
    old space 我们就称之为老年区(这是我自己取的名字),Eden(年轻代区),Survivor(幸存者区),Humongous(巨大对象区,下文我们简称为H区,当对象大小等于Region的一半时,对象会被分配到该区,该去属于老年区)。
    有了这些我们便能看懂JDK 11 下面输出的GC参数了,默认的分区大小是1M。G1在JDK6u14版本面世,在JDK7u4版本推出,在JDK8中,可以通过-XX:+UseG1GC指定使用G1垃圾回收器。

强调一下,关于堆、栈之类的叫JVM运行时区域划分,有些错误的资料会将其称为Java 内存模型(JMM Java Memory Model)。
简单的说Java内存模型是一种规范,主要是为了跨平台的解决并发编程遇到的问题。

总结一下

到现在我们对JVM的运行时区域划分已经有了一个比较清晰的理解,在JDK8之前的JVM运行时区域为:

  • The pc Register 程序计数器
  • Java Virtual Machine Stacks 虚拟机栈
  • 永久代( Method Area 方法区)
  • Run-Time Constant Pool 运行时常量池(位于永久代
  • Native Method Stacks 本地方法栈
  • 堆 heap

    -  年轻代
        -  Eden Space
        - To Space
        - From Space
    • 年老代
  • 永久代
在JDK8之后,永久代被移除,取而代之的是元空间,堆中的内存区域划分跟选择的垃圾回收器有关,G1(JDK9 成为默认的垃圾回收器,)和ZGC(JDK 11引入,JDK15默认的垃圾回收器)、Shenandoah GC(JDK12引入的垃圾回收器) 弱化了分代的概念,采用分区来管理内存。截止到JDK 16,除了上面三款的分区的垃圾回收器,其他垃圾回收器都采用分代来管理内存。
image.png
永久代不在堆里,只是和堆在物理上是一段连续的内存。
JDK8之后,假如你选择的是分代垃圾回收器:
image.png
假如你选择的是分区垃圾回收器,比如G1,那么java运行时区域划分,就变成了这样:
image.png
堆是垃圾回收器主要关注的区域,对象主要也是在堆上分配内存,也就是说对象还可以在堆外分配内存,讲清楚这一点并不是那么容易,这并不是本篇的主题,有兴致的可以参看下面两篇文章:

System.gc

Runs the garbage collector. Calling the gc method suggests that the Java Virtual Machine expend effort toward recycling unused objects in order to make the memory they currently occupy available for quick reuse. When control returns from the method call, the Java Virtual Machine has made a best effort to reclaim space from all discarded objects.
调用垃圾回收器,调用该gc方法意味着JVM将会投入一定精力去回收无用对象以减少内存占用,当方法返回时,JVM将尽最大努力从无用对象回收内存。

注意这里的尽最大的努力,也就是说啥也不干也行,本文研究的Hot Spot和其他JVM一样,默认在调用该方法的时候立即执行GC,并且等待GC完成时方法才返回。但是也有例外,ZGC是不支持通过System.gc()来触发的,也就是调用了System.gc()也没有用。截止到JDK17(在JDK17没有看到关于新的垃圾回收器的提案),其他垃圾回收器是可以通过System.gc()来触发GC的。

finalize()方法

如果一个对象实现了finalize()方法,那么在该对象的收集阶段,该方法即会被调用。注意JVM只是帮我们管理内存,这里说的管理包括分配和释放,也就是像其他资源就需要程序员自己去释放,比如堆外内存(主要被零拷贝和NIO所使用)、文件句柄、Socket等资源,程序员必须自己手动来管理。这个主要是为了避免对象死了以后它原本持有的资源泄漏,java才提供了finalize机制让用户注册finalize()这么个回调方法来定制GC清理对象的行为。在java.io.FileInputStream中就重写了finalize(),用于释放对应文件句柄资源。JDK 8以上该方法被标记为过时,取而代之的是Cleaner。有关这方面的讨论,可以参看:

  • 新版本的Java将会废弃Object.finalize(),并添加新的java.lang.ref.Cleaner

    走进GC

    GC: Garbage Collection 垃圾收集,当堆中的对象被JVM判定为无用的时候,JVM中的垃圾回收器就会将被判定无用的对象的内存给回收掉,清除该对象的实例数据。从上面可以看出垃圾回收可以被分为两部分,一是如何定义垃圾,二是垃圾如何回收。
    如果我们将内存为一个房间的话,用户程序可以理解为房间的使用者, 垃圾回收器就是房间的清洁者,从常理上推断,在你小的时候,妈妈给你打扫房间的时候,基本是暂停了你的房间使用权一段时间,然后再让你重新重新使用,这也就是Java世界的Stop-The-World,在垃圾回收的某个阶段,JVM会暂挂用户线程,等待该阶段结束才会恢复用户线程的运行。

如何定义垃圾

引用计数法

引用计算法是通过在对象头中分配一个空间来保存该对象的引用次数。如果该对象被其他对象引用,则它的引用计数加1,如果删除对该对象的引用,那么它的引用计数就减1,当该对对象的引用计数为0时,该对象就会被判定为垃圾。

Object object = new Object();

假设object指向对象的地址是0xaaff666,那么我们就称地址为0xaaff666中存储的对象有一个引用即为object。
然后将object置为null。那么0xaaff666这个地址上的对象就被认为是垃圾。该算法是将垃圾回收分摊到整个应用程序当中了,而不是在进行垃圾收集时需要挂起整个应用,直到对堆中所有对象的处理都结束。因此采用引用计算算法的垃圾回收机制并不算是严格意义上的"Stop-The-World"的垃圾回收机制。
这个算法被抛弃的主要原因就是无法解决循环引用问题,像下面这样:

public class GcDemo {
    public Object instance;

    public static void main(String[] args) {
        GcDemo a = new GcDemo();
        GcDemo b = new GcDemo();
        a.instance = b;
        b.instance = a;
        a = null;
        b = null;
    }
}

假设a之前指向的地址是0xfffa,b指向的地址是0xfffb。那么即使是a置为null,那么0xfffa地址上的对象中的instance仍然指向
0xfffb,同样的oxfffb地址上的对象的instance指向0xfffa。像下面这样:

image.png
由此我们引出在Java采用的垃圾判定算法可达性分析算法。

可达性分析算法

可达性分析算法(Reachability Analysis)的基本思路是,通过一些被引用链(Gc Roots)的对象作为七点,从这些节点开始向下搜索,搜索走过的路径被称为引用链(Reference Chain),当一个对象到gc Roots没有任何引用链相连时(即从GC Roots节点不可达),则证明该对象是不可用的。
那哪些对象可以做为gc roots呢,在Java语言中,可作为GC Root的对象有以下几种(未列出全部,具体的参看Eclipse 堆内存分析里列举出来的各种根对象类型):

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 类静态变量引用的对象
  • 类常量引用的对象
  • JNI的native方法栈中引用的对象
  • JNI中的global对象
  • 活着的线程所使用的对象

这些就跟我们上面讲的Java运行时区域联系上了,上面我们我们就提到了虚拟机栈,存储的基本单元就是栈帧,每调用一个方法,就会有一个栈帧入栈,我们可以理解为栈帧存储了方法执行所需要的的必要信息。

基于这种思路,那么循环引用问题就被解决了,我们知道程序是以方法为基本执行单位的,一个方法就是一个基本执行单元,java也是从main方法开始执行的,我们现在来结合具体的例子来看一下可达性分析算法是如何解决循环引用问题的:

private static void testGC() {
        GcDemo a = new GcDemo();
        GcDemo b = new GcDemo();
        a.instance = b;
        b.instance = a;
 }

testGC()执行完毕之后,a和b指向的对象不再是GC Roots,所以就可以被标定为垃圾。
那为什么这些对象可以是GC Roots,这些对象有什么特殊的呢? 因为这些对象如果被标定为垃圾,被垃圾回收器回收,那么就会影响程序的正常执行。JVM在调用一个方法的时候,会形成此方法的栈帧,如果这个栈帧所对应的对象回收了,那么这个方法就无法执行了。类静态变量和常量也是同样的道理,方法在运行时随时会用到这些。

垃圾如何回收

假如你比较富有,你不想整理你的房间,你雇了一个家政来帮你打扫房间,如果这个家政在收拾垃圾的时候比较理工科,先给
垃圾区域标记上贴纸,然后再统一清除垃圾。那么该家政采用的就是标记-清除算法。该家政存在的问题就是空间利用率不高,房间有很多小的空间,然后你有一个大对象,啊,不,是一个大家具想搬进来,你仔细算了算,发现这些空闲的空间加在一起足够的,但是这些空间没有连接在一起,这个家具就搬不进来,所以你就进行了一次房间整理。

为了避免这种情况,你让家政换了一种打扫策略,家政把你的房间切为两个区,每次只使用一块,然后使用的那块需要垃圾回收时,垃圾回收完毕之后,剩下的生活用品移动到另一块,这样买其他生活用品的时候,就避免了明明感觉房间放的下,但是就是没地方放尴尬问题。但是很快你就发现了新的问题,房间的利用率下降,因为你的房子是160平,按照这个家政的策略,你就只能采用80平。这也就是标记复制算法。

很快你对这种打扫策略感到了不适,你让家政在每次打扫完房间之后,再整理一下房间。但是很快你就发现了不适,你特别爱干净,一个小时要打扫一次,因为你的生活必须品位置变动频繁,家政打扫起来也慢。这个家政采用的就是标记-整理(也有资料称之为标记-压缩)算法。

于是聪明的你很快的开始跟阿姨讲,将房间进行分区,哪个区执行标记-清楚策略,哪个区执行标记-复制策略,哪个区执行标记-整理策略。

垃圾回收器简介

在JDK 1.3之前,Serial GC是唯一的选择,该垃圾回收器是单线程的,那就意味着标记和清除阶段都需要挂起整个JVM。这也是一个分代的垃圾回收器,新生代采用标记复制,老年代采用标记整理算法。
但是随着时代的发展,Serial GC已经不再满足服务端的需要了,Java开始将原来的串行转成并行处理,Java将原来的Serial改成了并行,这也就是ParNew(这个垃圾回收器,JDK已经不建议采用了,也不知道是不是这个原因,这个垃圾回收器的资料比较少)。如果你对系统的吞吐量(程序运行时间/程序运行时间+GC时间)比较看重的话,那么JDK 8目前采用的垃圾回收器Parallel Scavenge及Parallel Old就比较适合你,JVM提供了两个参数来精确控制吞吐量:

  • -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间, 一个大于0的毫秒数
  • -XX:GCTimeRatio:直接设置吞吐量大小, 一个大于0小于100的整数
    这也是双刃剑,也不是你调的越低,JVM的垃圾收集停顿时间就越低,如果是那样的话,那JVM在出厂的时候,直接不对外提供参数,直接设置到最低不就行了吗?因为GC的耗时缩短是用于调小年轻代来获取的,如果调的过小,回收频率大大增加了,
    吞吐量随之下去了。相关的讨论可以参看这篇文章:
  • 简介JVM的Parallel Scavenge及Parallel Old垃圾收集器
    再接着就是CMS (Concurrent Mark Sweep 后文统一称CMS GC) GC(JDK9之后已经被废弃了,原因就是参数太多,有了理论上更优的选择G1),CMS收集器是一种获取最短回收停顿时间为目标的收集器,这是因为CMS收集器工作时,GC工作线程与用户线程可以并发执行,以此来达到降低收集停顿时间的目的。CMS收集器仅能用于老年代的收集,是基于标记清除算法。

在没有G1、ZGC 、Shenandoah出现之前, 假设JDK默认的垃圾回收器不能满足我们的需要,我们就需要根据业务来选择对应的垃圾回收器组合:

  • Serial GC(不同代有不同的版本)
  • ParNew(标记复制) + CMS(标记清除) GC
  • Parallel Scavenge + Parallel Old
    作为一个开发者,我的愿景就是能不能不让我关注那么多有的没的参数,就让我写代码不行吗?很快G1、ZGC、Shenandoah的出现就满足了我的需求,不用再选,性能优良(这个似乎有点争议,毕竟也没有银弹,按需选择才是正理),参数少。
    G1: Garbage First 采用分区的概念来管理堆内存,设计原则是"首先收集尽可能多的垃圾",目标是为了尽量缩短处理超大堆产生的停顿。因此G1并不会等内存耗尽(比如Serial 、Parallel)或者快耗尽的时候(比如CMS垃圾回收器)才开始垃圾回收,而是在内部采用了启发式算法,在老年代中找出具有高收集收益的分区(Region)进行收集。
    ZGC: 是JDK11(JDK11 只支持Linux)推出的一款低延时垃圾回收器,它的设计目标包括:
  • 停顿时间不超过10ms
  • 停顿时间不会随着堆的大小,或者活跃对象的大小而增加
  • 支持8MB-4TB级别的堆(未来支持16TB)
    Shenandoah GC: JDK 12 引入 ,ZGC为了追求低停顿,吞吐量有所下降,而Shenandoah暂停时间与ZGC相近,平均暂停时长为10ms,ZGC平均暂停时间1ms,最长不超过10ms,但是吞吐量有所下降。

    Full GC 和 Mirror GC

    JVM回收对象占用的内存这个动作可以分为两大类:

  • Partial GC: 并不收集整个堆的模式

    - Young GC: 只收集年轻代的GC
    - Old GC: 只收集年老代的GC。只有CMS的并发收集时这个模式
    - Mixed GC: 收集整个年轻代以及部分年老代的GC。只有G1有这个模式。
  • Full GC: 收集整个堆,包括年轻代、年老代、永久代(如果存在的话,JDK8移除该代)等所有部分的模式。
    Major GC通常是跟full GC等价,收集整个GC堆,但是因为Hotspot VM发展了这么多年,外界对各种名词的解读已经完全混乱了。当有人问你XX GC的时候,你一定要问清楚他说的是哪种。

GC触发条件

其实上面已经讨论过了,各种young gc 触发的条件都是eden区满了。CMS GC只收集老年代,触发条件是老年代使用比率超过某值。G1触发条件是堆使用比率超过某值。

引用类型

在Java中,一切都是面向对象的世界,在Java中的数据类型分为两种,一种是基本类型,另一种为引用类型。引用类型是垃圾内存回收主要关注的对象。引用类型其实还可以在分为: 强引用类型(Object object = new Object()、软引用类型(SoftReference)、弱引用类型(WeakReference)、虚引用类型(PhantomReference)

强引用

通常情况下,我们在Java中创建对象最常使用的方式是这样的:

  Object object = new Object();

object存储了对象在堆中的地址,我们也称object指向new Object()。new Object()产生的对象即存在着一个指向它的引用,也就是object,我们称这种引用类型为强引用类型。强引用的对象失效大致上有两种情况:

  • 生命周期结束

    也就是该对象明显不会再被使用时,该对象就会等待被GC回收。不少网上的资料会说,对于强引用也就是new 方式创建的对象,JVM在任何时候都不会回收其内存。但是我认为这个是说法是有问题的,假设一个方法返回值是void,然后这个方法不断的被调用,内存不断的被占用,也不释放,那么JVM就会OOM。但是JVM并没有发生这样的情况,原因在于对于强引用的对象,就算是出现了OOM也不会被回收的这种说法是错误的。但是网上的博客基本也是互相抄,一个错,那就是都错。接下来我们验证一下上面的说法:
    public class JVMDemo {
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 5; i++) {
                testGC();
            }
            System.out.println("--------------------------");
            TimeUnit.SECONDS.sleep(10);
            List<String> list = new ArrayList<>();
        }
    
        private static void testGC() throws InterruptedException {
            Byte[] bytes = new Byte[1024 * 1024 * 10];
        }
    }

    我们先用 JVM参数限制一下堆内存,我的电脑是16G的,不限制内存,跑出来垃圾回收现象可能会比较难。

  • image.png
  • 在VM Options中加入 -XX:+PrintGCDetails(告诉JVM,垃圾回收时打印垃圾回收信息) -Xmx128M(堆可用最大大小为128M) -Xms64M(堆起始内存为64M),-XX:+PrintHeapAtGC: 打印内存回收之前堆的内存使用情况。
    image.png
    输出结果:
    image.png
    这足以证明许多博客关于强引用类型,JVM在任何时候都不会回收其内存的说法是错误的。可能有同学还会说,你怎么证明打印出来的GC信息是testGC()方法造成的呢? 很简单,我们可以在for循环中注释掉对testGC()的调用。你会发现就不会输出GC信息了。
    这个故事告诉我们,要懂得理论与实践相结合,网上的博客要有在自己的甄别能力,因为可能都是错的。因为在写对象在什么时候被回收的时候也是翻了一堆资料,发现都是各说各的,都是存在着大量的问题。所幸最后还是找到了比较靠谱的资料:
  • Java对象生命周期
  • Java 中的引用类型、对象的可达性以及回收处理
  • The Truth About Garbage Collection

    对象的生命周期简介

    粗略的说一个对象的一生是这样的:

  • Created 创建阶段

    对应new,在堆上分配内存,变量初始化。
  • In Use 应用阶段

    至少被一个强引用持有着, 简单的说就是 Object object = new Object(); new 出来的对象就被object所持有。
  • Invisible 不可见阶段

    即使是强引用持有对象,但是这些引用是局部变量,就会进入这个阶段,该阶段非必经阶段,像下面这样:
    private void run() {
        try {
            Object object = new Object();
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // object对于值在try里面可见。那么程序执行到这里,
        // object指向的对象即进入不可见阶段
        System.out.println("hello world");
    }
  • Unreachable 不可达阶段

    对象处于不可达阶段是指该对象不再被任何由gc roots的强引用的引用链可达的状态。
    后文会讲gc roots配合可达性算法分析(该算法解决的是如何判断一个对象是垃圾).
  • Collected 收集阶段

    当垃圾回收器发现该对象已经处于不可达阶段,并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,对象进入"收集阶段"。如果该对象已经重写了finalize()方法,并且没有被执行过,则执行该方法的操作。如果已经执行过了finalize方法,则该对象直接进入终结阶段。finalize()方法在某种程度上可以看做是对象的自救,尽管从某种意义上来看,这种自救颇为鸡肋,还存在着种种问题,在JDK8以上的版本,该方法已经被标记为过时的。
  • Finalized 终结阶段

    当对象执行完finalize()方法仍然处于不可达状态,该对象进入终结阶段。在该阶段,等待垃圾回收器回收该对象空间。
    注意这一点,是等待,等待被垃圾回收器回收,意味着对象自救失败。
  • Deallocated 重新分配阶段

    这是垃圾回收的最后一个阶段,如果经历上面的阶段进行该对象还被判定为不可达,那么该对象的所占用的内存空间进入重新分配的候选,至于是清空该内存空间还是再分配、在什么时候发生则取决去具体的虚拟机实现。
    image.png
    算上Servlet的生命周期、线程的生命周期、类的生命周期,刚好凑够四个,可以称之为Java的四大生命周期了。

    软引用

    根据JVM内存情况: 如果内存充足,GC不会随便的回收软引用对象 ; 如果JVM内存不足 , 则GC就会主动的回收软引用对象。经常被用作缓存。上面是网上常见的说法,那这里就有一个问题,就是多少内存算不足,是不是也有一个参数来控制呢?上面我们已经看到了,不同的垃圾回收触发垃圾回收时机不同。像我在使用JDK默认的垃圾回收器测试(Parallel Scavenge)时候,常常是内存溢出了,软引用也没被回收。这让我很奇怪上面的说法,JVM内存不足,GC会主动的回收软引用对象,我在想也许是多线程的原因,垃圾回收线程还没开始工作,制造内存泄漏的线程就已经把JVM搞崩了。
    然后我有翻了翻SoftRefeence的注释:

    All soft references to softly-reachable objects are guaranteed to have been cleared before the virtual machine throws an OutOfMemoryError. Otherwise no constraints are placed upon the time at which a soft reference will be cleared or the order in which a set of such references to different objects will be cleared. Virtual machine implementations are, however, encouraged to bias against clearing recently-created or recently-used soft references.
    在OOM发生之前,JVM保证所有的软引用对象会被清除,但是,究竟虚拟机要回收哪个软引用的对象或者回收顺序是怎么样的,是没有限制的。虚拟机的实现一般倾向于清除掉最新创建或者最新被使用的SoftReference。

在查了一些资料,发现内存不足也不仅跟可用内存有关,还跟时间有关。在JVM 官网我查到了这样一个参数: -XX:SoftRefLRUPolicyMSPerMB.参数说明如下:

Soft references are kept alive longer in the server virtual machine than in the client. The rate of clearing can be controlled with the command line option -XX:SoftRefLRUPolicyMSPerMB=<N>, which specifies the number of milliseconds a soft reference will be kept alive (once it is no longer strongly reachable) for each megabyte of free space in the heap. The default value is 1000 ms per megabyte, which means that a soft reference will survive (after the last strong reference to the object has been collected) for 1 second for each megabyte of free space in the heap. Note that this is an approximate figure since soft references are cleared only during garbage collection, which may occur sporadically.
软引用在JVM的服务端模式的存活时间比在客户端的存活时间要长一些,清楚的速度可以通过--XX:SoftRefLRUPolicyMSPerMB
来控制(速度就和时间扯上联系了),该参数指定了堆中每MB内存软引用存活的时间,单位是milliseconds 。默认参数是1秒,
SoftReference概览:
image.png
get方法用于获取对应的引用对象。
示例:
class SoftObject {

}

public class SoftReferenceDemo {
    public static void main(String[] args) {
        SoftReference<SoftObject> softRef = new SoftReference<>(new SoftObject());
        List<Byte[]> byteList = new ArrayList<>();
        while (true) {
            if (softRef.get() == null) {
                System.out.println("软引用对象已经被回收.....");
                System.exit(0);
            } else {
                System.out.println("填满。。。。。。");
                byteList.add(new Byte[1024 * 1024]);
            }
        }
    }
}

IDEA中指定 VM Options, -XX:SoftRefLRUPolicyMSPerMB=0。然后很快就跑出来了: 软引用对象已经被回收......
注意通过-Xms和-Xmx限制内存。

弱引用

对应的类是WeakReference,特点是只要GC执行,弱引用一定会被回收。

public class WeakReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
        System.out.println(weakReference.get() == null ? "已经被回收" : "未被回收");
        System.gc();
        // 让gc线程执行
        TimeUnit.SECONDS.sleep(5);
        System.out.println(weakReference.get() == null ? "已经被回收" : "未被回收");
    }
}

输出结果:
image.png

虚引用

对应的类为PhantomReference,一般不会单独使用,一般会和引用队列(ava.lang.ref.ReferenceQueue)一起使用。
当gc在回收一个对象时,如果发现此对象还有一个虚引用,就会将虚引用放入到引用队列中,之后(当虚引用出队之后)再去回收该对象。因此,我们可以使用虚引用实现在对象被GC之前,进行一些额外的其他操作.
image.png
示例代码:

class MyObject{

}
public class PhantomReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        MyObject myObject = new MyObject();
        ReferenceQueue<MyObject> myObjectReferenceQueue = new ReferenceQueue<>();
        // 引用对象+引用队列
        PhantomReference<MyObject> phantomReference = new PhantomReference<>(myObject,myObjectReferenceQueue);
        myObject = null;
        // 让GC执行
        System.gc();
        TimeUnit.MILLISECONDS.sleep(1000);
        System.out.println("GC 执行....");
        // 出队,打印对应的引用对象
        System.out.println(myObjectReferenceQueue.poll());
    }
}

当虚引用所引用的对象实现了finalize方法,会延迟入队时间。

当我们说起JVM调优

经过之上的讨论,我们可以说说JVM调优了,我原本对调优的理解是有些问题的,我以为的调优是原先是运行优良,调过之后运行的更加良好。但是真实的情况是,运行的不如人意了,然后看看JVM出了什么问题,一般情况下,也不是JVM的垃圾回收有问题,一般都是程序写的有问题,我们给JVM诊断,看看病在哪里。但是也有特殊情况,就是确实需要调的(大数据领域比较需要这个),但是也不能瞎调,我们需要分析程序的运行情况,综合给出最佳参数,但是还没搞清楚状况就开始跳的,一律叫瞎调。

内存泄漏

一般JVM运行的不尽如人意的原因就是内存泄漏,之前我对内存泄漏是有认知偏差的,我以前认为的内存泄漏是像C++那样,有析构函数,调了两次,就叫内存泄漏。所说我对Java为什么还会内存泄漏是不理解的,事实上内存泄漏的定义是:

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。--《百度百科》

所以由以上定义,Java还是会发生内存泄漏的,一个典型的场景就是假设你有一张大的表,然后做了全表扫描,返回到Java中产生了一个大对象,然后频繁的产生。产生大对象之后(除了ZGC、Shenandoah GC、G1之外的垃圾回收器还是比较花时间的),如果内存不够用就会触发GC,然后内存泄漏就发生了。

JVM监测工具-MAT

Java发展了这么长时间,诊断工具也有不少,像jvisualvm、jconsole等,但是这里就只介绍简单的、好用的、免费的。也就是Eclipse出品的MAT,我个人最喜欢的JVM内存分析工具,轻松的诊断出内存泄漏问题。
下载地址如下:
https://www.eclipse.org/downl...
下载之后:
image.png
image.png
通常情况下,我们都是导出入VM的dump文件,后缀为hprof文件来进行分析。
jmap -dump:format=b,file=文件位置 进程号
image.png
然后我们启动一下导出hprof文件,导入MAT做分析。
image.png
image.png
image.png
image.png
image.png
image.png
image.png

总结一下

本篇是JVM系列的总纲,涵盖全局。基本上围绕着JVM调优,我觉得JVM调优就像是给JVM看病一样,dump工具相当于输出报告,MAT就是分析工具。

参考资料

查看原文

赞 0 收藏 0 评论 0

北冥有只鱼 发布了文章 · 3月11日

欢迎光临Spring Boot时代(一)

SSM(Spring Spring MVC MyBatis)已经让我们的开发很简洁了,但是还存在着一些问题,一就是随着项目的扩大,Spring配置文件会有些庞大,当然我们可以拆分,但是让IOC管理的复杂依赖之间的注入似乎都变化不大,二就是各种依赖之间的版本问题,我们知道各个jar包存在依赖关系,相同的jar包依赖的jar可能都不一样,我们在开发的时候就需要小心翼翼,生怕弄错了版本,导致项目问题。这些问题都将由Spring Boot来解决。

前言

在当今的Java领域,Spring Boot是十分流行的,因为用它开发确实非常快更简单,有人甚至说可以不用学Spring,直接学Spring Boot,但是Spring Boot的底层还是Spring Framework,只不过是在Spring Framework的基础上进行了一层封装而已(以我目前的水平看是这样的),不学 Spring,直接学Spring Boot,在我看来就像是建楼不建地基,直接从第二层开始建一样。所以我希望你在看本篇之前,希望你对Spring Framework已经有一定了解了,那么怎么确认是否达到这个标准呢? 可以参看我这篇博客: 欢迎光临Spring时代(一) 上柱国IOC列传。如果这篇博客的东西你都掌握了,基本上这篇博客讲的就可以基本看懂了。

这些做的练习我都放在码云上了,有时间会在GitHub上也放一份, 地址如下:

简介

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run".
如果你想创建基于Spring的单体应用,且是生产级别的,选取Spring Boot吧,它非常简单,简单到你好像不用写代码,只用运行就可以了。
We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need minimal Spring configuration.
老实说这句话我一直在究竟怎么翻译,我的第一个理解是我们为Spring 平台和第三方库设计了一个视图,能够开发者最快的开始开发,绝大部分Spring Boot程序需要的配置很少。第二个理解就我们重新审视了Spring 平台和第三方库之间的关系设计了Spring Boot框架,Spring Boot能够让开发者快速的开发,绝大部分Spring Boot 程序需要的配置文件都很少。反正两个理解相近都是在说能够让开发者快速上市,而且配置文件很少。有英语好的同学,可以在下方留言。

上面所说的视图就是我理解就是这个东西: image.png

特点

  • Create stand-alone Spring applications
创建单体Spring 程序
  • Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)
内置Tomcat、Jetty、Undertow等应用服务器(可以自己选择)
  • Provide opinionated 'starter' dependencies to simplify your build configuration

(提供starter依赖集供你选择让你的pom文件更小一点)

  • Automatically configure Spring and 3rd party libraries whenever possible
尽可能的自动配置Spring和第三方库
  • Provide production-ready features such as metrics, health checks, and externalized configuration
提供一个可随时上生产环境的特性,就像一个度量仪,用于健康监测和扩展配置。
这个应该说的是Spring Boot admin,监控应用的运行状况
  • Absolutely no code generation and no requirement for XML configuration
完全没有衍生代码和xml配置文件
可能到这里你还是有些晕哈,什么是starter,什么是自动配置,没配置XMl配置文件,数据库信息我还怎么配置?
不要着急,慢慢跟着我的思路走,下面会一一讲清楚这些名词。

Spring Boot与Spring Cloud

Spring Boot似乎与微服务的概念缠绕在一起,是不是Spring Boot就是用来做微服务的呢? 写到这里,可能还会有同学会问,什么是微服务? 简单的说,微服务是一种软件架构,那什么是软件架构? 请参看我这篇文章:

如果你常看我的文章,会发现我的文章基本上是一环扣一环的,是一个有序的系统。

那Spring Cloud是什么?

Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems
Spring Cloud 为开发人员提供了一系列工具集来解决在构建分布式系统中遇到的一些通用问题。
原来是个工具集啊,看起来跟微服务也没关系啊。不要着急,我们在Spring官网再仔细找找。

Microservice architectures are the ‘new normal’. Building small, self-contained, ready to run applications can bring great flexibility and added resilience to your code. Spring Boot’s many purpose-built features make it easy to build and run your microservices in production at scale.
微服务架构正在成为新常态,组建一个个微小的、包含容器的、运行起来简单的能够给你的系统带来高灵活性和强扩展性。 Spring Boot的许多构建特性能够让开发者在大型项目中组建和部署微服务简单起来。

And don’t forget, no microservice architecture is complete without Spring Cloud ‒ easing administration and boosting your fault-tolerance.
但是不要忘了,在Java领域,没有Spring Cloud的任何微服务架构都是不完整的,因为缺少容错能力和管理麻烦。

所以Spring Boot和Spring Cloud是可以用来组建微服务,实现微服务架构的。Spring Boot用来做开发服务,只是加快你开发Spring 应用程序的速度,不一定非得用来做微服务。Spring Cloud用来做服务治理和Spring Boot能够做到无缝集成。
到此两者的关系应该讲清楚了。

如何创建一个Spring Boot工程

版本号选讲

数字版本号参看知乎的这个问题: 软件的版本号是如何确定的?
直接想看官方约定的,请去这个网址: https://semver.org/lang/zh-CN/
这里讲的是英文大版本号,像GA之类的:

  • GA
General Availability,正式发布的版本,官方开始推荐广泛使用,国外有的用GA来表示release版本。
  • RELEASE
正式发布版,官方推荐使用的版本,有的用GA来表示。比如spring。
  • Stable
稳定版,开源软件有的会用stable来表示正式发布的版本。比如Nginx。
  • Final
最终版,也是正式发布版的一种表示方法。比如Hibernate。
  • RC
Release Candidate,发行候选版本,基本不再加入新的功能,主要修复bug。是最终发布成正式版的前一个版本,将bug修改完就可以发布成正式版了。
  • alpha
α是希腊字母的第一个,表示最早的版本,内部测试版,一般不向外部发布,bug会比较多,功能也不全,一般只有测试人员使用。
Beta
β是希腊字母的第二个,公开测试版,比alpha版本晚些,主要会有“粉丝用户”测试使用,该版本仍然存在很多bug,但比alpha版本稳定一些。这个阶段版本还会不断增加新功能。分为Beta1、Beta2等,直到逐渐稳定下来进入RC版本。
我们介绍的是常见的,然后还有一些,如果还想了解请参看后文的参考资料中的文章。

方式一: Spring Initializr

image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png

不用配置Tomcat,直接运行我们看下效果:
image.png
然后我们写个Controller试试看:
image.png
代码如下:

RestController
public class HelloWorld {
    @GetMapping(value = "/hello")
    public String test() {
        return "hello";
    }
}

输入url: http://localhost:8080/hello
结果: image.png
下面会讲什么不用之前那么一堆,就能直接编写Controller。

方式二: 直接引入依赖

  1. 新建一个maven工程,如果你不懂maven,请参看: Maven学习笔记
  2. image.png
  3. image.png
  4. 2.3.9的Spring Boot将对应starter的pom展示移除了,如果你想复制的话,可以进入这个网址:
https://docs.spring.io/spring...

试了好几次,访问pom是进入GitHub,但是总是失败。Spring Boot 的starter依赖总是以spring-boot-starter开头,我们不用写版本号,因为继承了父依赖。我们直接这么写就行了:

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

然后建一个包和类:
image.png

@SpringBootApplication
public class StudySpringBoot {
    public static void main(String[] args) {
        SpringApplication.run(StudySpringBoot.class, args);
    }
}

运行结果:
image.png
同样的控制器代码:

image.png

同样的运行结果:
image.png

分析一下这个Hello World

上面我们已经通过两种方式建立了一个Spring Boot工程,非常快的就搭建起来了一个web工程,Spring MVC的那些麻烦配置都被扔掉了,我们很快就可以开始开发。这是什么原理呢? 下面我们就来探究一下。

pom文件变小了

image.png

image.png

image.png
那这里可能有同学就会问了,我也不需要这么多啊,你给我引入了,不是影响我启动速度吗?
不用担心,这是按需引入,Spring Boot会根据你pom中引入的starter来自动按需引入,我们看一下我们引入的web starter。

image.png

image.png
所以不用担心父依赖引入的东西全部被引入。Spring Boot 将我们的库和开发场景做成了一个个starter,我们不用再费心开发的时候去一个一个引入了,比如你开发的是web程序就找web-starter,Spring Boot提供的starter如下:

image.png
这样我们就只用根据场景去选择starter了,这样就不用担心jar包版本不匹配的问题了。这也就是上面我们讲的Spring Boot的第
三个特点: Provide opinionated 'starter' dependencies to simplify your build configuration。提供starter,让你的pom文件更简单。

自动配置的原理

下面我们来研究为什么我们不用配置DispatchServlet、扫描包、注解驱动等Spring MVC配置,就能直接写Controller,其实我们也可以猜的到,我们不配,那就是Spring Boot帮我们配置,这也就叫自动配置。
这里我把启动程序贴一下:

@SpringBootApplication
public class BootApplication {
    public static void main(String[] args) {
        SpringApplication.run(BootApplication.class, args);
    }
}

这是一个普通的main函数,我们首先研究上面的@SpringBootApplication这个注解。

image.png
然后我们看到了三个值得注意的注解:

  • @ComponentScan
这个注解我们熟悉,在欢迎光临Spring时代(一) 上柱国IOC列传我们介绍过了,你看学Spring Boot之前,学Spring是必要的吧。
自动扫描不用配的原因找到了。
  • @EnableAutoConfiguration

@EnableAutoConfiguration概览:

image.png
@EnableAutoConfiguration又发现了两个自定义注解:

@AutoConfigurationPackage
// 将AutoConfigurationImportSelector这个类加入到IOC容器中
@Import(AutoConfigurationImportSelector.class)
- `AutoConfigurationImportSelector`
字面意思是自动配置引入选择器,估计就是这个类根据starter执行按需引入这个功能的。我们研究一下AutoConfigurationImportSelector这个类: 

image.png
还记得Deferred意为延缓,还记得ImportSelector这个接口的作用吗? 该接口的selectImports方法,返回的值就能够加入到IOC容器中。不记得的再翻一翻在欢迎光临Spring时代(一) 上柱国IOC列传
我们接着打断点研究一下:

image.png
注意也不知道是IDEA的bug还是怎么回事,我在99行打断点,启动的时候进不来,但是我在getAutoConfigurationEntry这个方法中打断点就进来了,我当时很奇怪,难道是高版本的Spring Boot改了流程? 但是看了很多视频,发现人家在这里打断点就行,所以最后确定是我IDEA的问题。
我们接着来研究:

image.png

image.png
最终是WebMvcAutoConfiguration来帮我们搞定自动配置的,我们大致看一下这个类:

image.png
又出现了五个新的Spring Boot自定义注解:

@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
        ValidationAutoConfiguration.class })

我们先看上面三个@ConditionalOn开头的,这好像跟条件注解有点像,对它本质上就是条件注解。你可以接着点进入看,发现里面有@Conditional
其实不用翻注释我们直接从名字上就能推断出来:

  • @ConditionalOnWebApplication: 是一个web应用程序且引入的依赖中有Servlet时,算是满足条件。
  • @ConditionalOnClass: 引入的依赖中有Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class这三个类条件成立
  • @ConditionalOnMissingBean: missing有缺失的意思,也就是说容器中没有这个bean,条件成立。

组合在一起就是这三个条件成立,自动加载WebMvcAutoConfiguration,然后开始自动配置。
这里再补充一下,这些自动配置类都是从哪里获得的呢? 我们想一下这个问题,我们总不能写在代码里,没增加一个自动配置类,我就要该代码吧。其实这个也是配置文件中的,还是在AutoConfigurationImportSelector的selectImports方法中:

image.png
image.png

image.png

image.png
总有些元数据是程序需要通过配置文件来获取的。
接着我们来看@AutoConfigurationPackage注解,基本上这个注解一看完,我们就对Spring Boot的自动配置原理有一个清晰的理解了。
image.png
我们接着打断点看一下这个类做了什么:

image.png
然后我们在看下AutoConfigurationPackage上的注释:

Registers packages with {@link AutoConfigurationPackages}. When no {@link #basePackages
base packages} or {@link #basePackageClasses base package classes} are specified, the
package of the annotated class is registered.
当在@ComponentScan没指定base packages或base package classes这个属性时,那么这个类会自动将Spring Boot主类所在的包的子包上面有三层注解的类加入到IOC容器中。

总结一下

到现在我们已经大致上对Spring Boot有一个大致的理解了,为什么Spring Boot做Web不需要配置Servlet那一大堆东西,因为Spring Boot的自动配置类已经帮我们配置了,Spring Boot 根据条件注解和starter来确定要启用哪个自动配置类。为什么能让我们pom变小,因为Spring Boot把我们的开发场景变成了starter,如果我们要开发web,那么首先就要引入web starter。如果是MyBatis那就有 myBatis-starter。
Spring Boot的执行流程图:

image.png

无法被舍弃的配置文件

尽管做了大量的自动配置,大大减少了Spring Boot配置文件的体积,但是总是有些配置我们不想硬编码,比如数据库连接、端口等。所以Spring Boot也提供了加载配置文件的方式,Spring Boot支持了一种新的配置文件,我们称之为yaml,通过缩进来表达K,V对,而且要求k和v之间要一个空格的距离。
Spring Boot目前主要支持的配置文件有两种:

  • application.properties k,v对
  • application.yml 缩进

默认情况下 application.properties 的优先级高于application.yml。

@ConfigurationProperties

我们目前借助org.springframework.boot.autoconfigure.web.ServerProperties来讲解Spring Boot加载配置文件的思想,我们先大致看一下这个类:

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {

    /**
     * Server HTTP port. 端口
     */
    private Integer port;

    /**
     * Network address to which the server should bind. 地址
     */
    private InetAddress address;

    @NestedConfigurationProperty
    private final ErrorProperties error = new ErrorProperties();

    /**
     * Strategy for handling X-Forwarded-* headers.
     */
    private ForwardHeadersStrategy forwardHeadersStrategy;

    /**
     * Value to use for the Server response header (if empty, no header is sent).
     */
    private String serverHeader;

    /**
     * Maximum size of the HTTP message header.
     */
    private DataSize maxHttpHeaderSize = DataSize.ofKilobytes(8);

    /**
     * Type of shutdown that the server will support.
     */
    private Shutdown shutdown = Shutdown.IMMEDIATE;

    @NestedConfigurationProperty
    private Ssl ssl;

    @NestedConfigurationProperty
    private final Compression compression = new Compression();

    @NestedConfigurationProperty
    private final Http2 http2 = new Http2();

    private final Servlet servlet = new Servlet();

    private final Tomcat tomcat = new Tomcat();

    private final Jetty jetty = new Jetty();

    private final Netty netty = new Netty();

    private final Undertow undertow = new Undertow();
    }

ServerProperties 中还有一个静态内部类,我们也拿出来看一下,这里我就直接贴部分源码了:

    public static class Servlet {
    /**
         * Servlet context init parameters.
         */
        private final Map<String, String> contextParameters = new HashMap<>();

        /**
         * Context path of the application.
         */
        private String contextPath;

        /**
         * Display name of the application.
         */
        private String applicationDisplayName = "application";
}

在上面我们注意到有一个@ConfigurationProperties注解,我们进去看他的注释,碰见英文不要排斥,开发常用的单词就那么多,不行还有百度翻译。

Annotation for externalized configuration. Add this to a class definition or a @Bean method in a @Configuration class if you want to bind and validate some external Properties (e.g. from a .properties file).

该注解用来引入外部的配置文件,具体的水就是如果你想要为配置类或配置类形式的Java Bean(也就是加上了@Configuration的类、加上@bean的注解)设置一些从外部文件的值(比如properties配置文件),那么你就可以使用这个注解。

我的解读: 虽然Spring Boot尽量减少了配置文件,但是有些配置文件还是需要引入,引入了,怎么绑定到对应的对象上呢,那就是通过@ConfigurationProperties这个注解来引入。

Binding is either performed by calling setters on the annotated class or, if @ConstructorBinding is in use, by binding to the constructor parameters.
这个绑定是通过调用带有注解的类的方法是通过set方法,如果构造函数上有@ConstructorBinding,那么就通过构造函数来完成绑定。

我的解读: 如果有@ConstructorBinding,这个那就调用构造函数绑定值,如果没有那就调Set方法。

Note that contrary to @Value, SpEL expressions are not evaluated since property values are externalized.
注意相对于@Value注解,SpEL表达式不会对这种额外的配置求值。 @Value表达式是Spring EL表达式,这个可以自行查下相关资料,这里就不做介绍了。

那么问题又来了,怎么匹配呢? 我这么多属性怎么对应到配置文件对应的值呢? 我们接着看@ConfigurationProperties这个注解的源码,其实注解是一个语法糖,本篇也算是Java 的稍微高级一点的知识,我这里默认你已经对Java已经比较了解了。

答案就是@ConfigurationProperties中的value属性,别名是prefix,prefix意味前缀。所以匹配的规则就是前缀加上属性名。
ServerProperties上面的@ConfigurationProperties的prefix是server,里面有一个端口属性,所以我们在properties就应该这么写:

server.port=8884
server.servlet.context-path=/studyBoot

然后我们启动一下项目,看一下配置是否成功:
image.png
在yaml中的配置方法注意是缩进加空格:

server:
  port: 8882
  servlet:
    context-path: /study

yaml就相当于把propertis中的.改成了缩进,=号变成了空格。注意这个规律。
ServerProperties 还有别的属性这里就不一一细讲了。我们再来做几个联系来体会一下 @ConfigurationProperties这个注解的用处。

@Getter //  Lombok插件
@Setter //  Lombok插件
@NoArgsConstructor //  Lombok插件
@ToString //  Lombok插件 不懂 百度一下
@Component
@ConfigurationProperties(prefix = "student")
public class Student {
    private int age;
    private String name;
    private boolean sex;
    private String[] locations;
    private StudentCard car;
}
@NoArgsConstructor
@Setter
@Getter
@ToString
public class StudentCard {
    private String cardNo;
}

yaml中的配置:

student:
 age: 23
 name: zs
 sex: true
 locations:
   - 上海
 car:
   cardNo: 8888

我们测试一下:

@RunWith(SpringRunner.class)
@SpringBootTest
class BootApplicationTests {
    @Autowired
    private Student student;
    @Test
    void contextLoads() {
        System.out.println(student);
    }
}

测试结果:

image.png
平时写yaml这种靠缩进的也比较头疼,好在IDEA有强大的提示,但是有的时候我是更喜欢写properties文件格式的,然后转成yaml。之前项目做过一个多数据源的,我在yaml里配置,怎么都不成功,然后那个时候IDEA没这个多数据源的提示,当时着实让我头疼了一把,如果你觉得这个头疼,有在线将properties转成yaml的工具。

@PropertySource简介

上面我们讲的取值与赋值都是走的Spring Boot默认的配置文件: application.properties,application.yml。在Spring Boot中properties的优先级要高于yaml。但是假如有些配置文件,我们不想写入默认文件中想单独抽出来加载应该怎么办呢? 实际上就是通过@PropertySource来完成的。我们还是能看源码上的注释,就尽量看源码上的注释。

Annotation providing a convenient and declarative mechanism for adding a @link org.springframework.core.env.PropertySource PropertySource} to Spring's
{@link org.springframework.core.env.Environment Environment}. To be used in
conjunction with @{@link Configuration} classes.
@PropertySource 注解和Spring的环境变量配合一块使用,用于配置类之上。

代码中还有示例,我们照着做就行,首先我们准备一个properties文件:

testbean.name=myTestBean

然后准备配置类和一个javaBean:

@Getter
@Setter
@NoArgsConstructor
@ToString
public class TestBean {
    private String name;
}

@Configuration
@PropertySource(value = "classpath:app.properties")
public class SpringBootConfig {
    private Environment env;

    @Autowired
    public void setEnv(Environment env) {
        this.env = env;
    }

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

测试一下:

@RunWith(SpringRunner.class)
@SpringBootTest
class BootApplicationTests {

    @Autowired
    private TestBean bean;

    @Test
    void contextLoads() {
        System.out.println(bean);
    }
}

测试结果:
image.png

@ImportResource 还是需要配置文件

虽然配置类已经很强大了,但是有些还是无法移植到配置类中,在@ImportResource中有这样一段注释:

but where some XML functionality such as namespaces is still necessary.
但是有些XML的功能比如命名空间仍然是必须的。好像是跟groovy这门语言有关系,因为Spring 不只属于Java,要讨论下去似乎要站在Spring整体的角度去考虑,这又是一个庞大的课题,这里我们就姑且将其他理解为引入xml配置文件的吧。
具体怎么用呢? 还是在配置文件中做一个Java Bean,然后在Spring Boot的启动主类上加载它就行。
首先我们准备一个配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 这里只是做个示例,实际上这个更适合配置类-->    
    <bean id = "testBean" class = "com.example.boot.pojo.TestBean">
        <property name="name" value="zd"/>
    </bean>
</beans>

然后在主类上引入:

@SpringBootApplication
@ImportResource(locations = "classpath:spring-mvc.xml")
public class BootApplication {
    public static void main(String[] args) {
        SpringApplication.run(BootApplication.class, args);
    }
}

测试代码:

@RunWith(SpringRunner.class)
@SpringBootTest
class BootApplicationTests {

    @Autowired
    private TestBean bean;
    @Test
    void contextLoads() {
        System.out.println(bean);
    }
}

测试结果:

image.png

配置文件位置

熟悉maven的人可能清楚,maven约定了目录结构,resources我们称之为资源文件夹,我们会把配置文件放在这个位置。
Spring Boot启动的时候就会默认的加载application.properties文件,如果没有则会去找yaml。properties的优先级要始终高于yaml。但是如果两者同时存在,相互冲突的配置向,以properties的配置为主。如果不冲突,则相互补充(想是合并在了一个配置文件一样)。

假如你的项目大的过分,达到需要把配置文件拆分之后,也需要单独放置一个文件夹的话,那么Spring Boot会默认从这四处寻找配置文件:

  • file: 项目根路径/config(此时config文件夹与资源文件夹平级)
  • file: 项目根目录
  • classpath: 资源文件位置/config
  • classpath: 资源文件位置/项目根目录

优先级从上达到下。

环境切换

我们之前一直强调过我们有开发(dev)、测试(uat)、生产环境(prod),对应的也有三套配置文件,那我们该怎么配才能让做到让应用程序在不同的环境使用不同的配置文件呢? Spring Boot 也考虑到了,你说贴心不贴心。
如果有properties文件,会优先选取properties文件进行读取。

在主配置文件中指定激活环境。

我们以properties为例,yaml是同样的,这里不再做对应的展示。首先我们建立一个开发环境中的配置文件,文件名指定为: application-dev.properties,里面指定端口:

server.port=8885

然后在主配置文件里选取激活的文件环境:

server:
  port: 8882
  servlet:
    context-path: /study
Spring:
  profiles: dev

注意这个dev就和开发环境中的配置文件对上了,通过application-环境。
再启动的时候,你就发现端口是8882了。

运行参数

Spring Boot web应用是做成一个jar包的,java 中启动一个jar包通过的指令是java -jar xxx.jar,其实这后面也可以跟运行参数。

比如java -jar xxx.jar ----spring.profiles.active=uat ,然后Spring Boot 在启动的时候就会去加载名为application-uat.properties文件,在IDEA中我们可以实现对应的效果:

image.png

image.png
然后你会发现运行参数的优先级最高,主配置文件中我们制定的是dev环境,运行参数中我们制定的是uat环境。

VM参数

JVM参数,我们通常用来限制JVM的内存,因为JVM一向是有多大内存就吃多大内存。一般的命令格式 java -参数 。
我们也可以通过这个来设置启动环境,在IDEA如下配置就可以:

image.png
也可以达到同样的效果。

Spring Boot整合的思想

总纲

上面我们讲Spring Boot把我们开发的各种场景做成了各种各样的starter,我们开发的时候只需要按需引入即可。引入之后,在配置文件中配置必要的文件参数,Spring Boot就能让自动配置类完成自动整合。所以总这里总结一下Spring Boot开发的一般步骤:

  • 根据场景去找starter
Spring 官方出的starter依赖名通常以spring-boot-starter开头,可以去上面放的官网中去找找看。
如果不属于Spring Boot,那么starter通常spring-boot-starter在后面,比如mybatis-spring-boot-starter
  • 根据启动器在配置文件中配置参数。

整合MyBatis

首先我们仔细想一下MyBatis需要什么? 首先肯定需要mybatis-spring-boot-starter,这里面是MyBatis帮我们做的,里面已经集成了一些必要的。但是我们想想,这个启动器不应该集成的:

  • 数据库连接池
如果这个集成了,岂不是还要关注市面上连接池的动向。
  • 数据库驱动
这个也应该让开发者自己选择,毕竟数据库不少,驱动版本也不少。starter集成那么多,又耦合起来了。
然后我们引入依赖:
 <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.22</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

然后怎么配参数:
我们依然去百度搜索看看有没有官方文档:
image.png
点进去看一下:
Alt text
里面有对应的示例,这里我们就简单的整合一下就可以了,理解这个思想很重要。
在整合MyBatis之前,我们还得先搞定连接池,这次选定的连接池Druid,还是先去百度搜搜看看有没有官方文档,一般都有,告诉你参数应该怎么配。
Alt text
Alt text
我们直接从官网复制: server.port=8884

server.servlet.context-path=/studyBoot
mybatis.mapper-locations=classpath*:com/example/boot/mapper/*.xml 
spring.datasource.druid.url=jdbc:mysql://192.168.2.129:3306/study?useUnicode=true&characterEncoding=utf-8
spring.datasource.druid.username=root
spring.datasource.druid.password=RrXMoWG8+6gl
spring.datasource.druid.driver-class-name=com.mysql.jdbc.Driver

注意看官网要看仔细,想想我们SSM整合中我们配Mapper.xml也配置了Mapper.xml对应的接口。
image.png
上面说的是Spring Boot会自动将打上@Mapper接口的注解去和mapper-locations中指定的xml做关联。
如果你想定制可以用@MapperScan,里面指定你想扫描的包。
具体的代码,可以去GitHub看。

整合Redis

接着我们来做个练习尝试在Spring Boot 整合Redis。第一步去找Redis 对应的starter:
image.png

我们点进去看一下:

image.png
我大意了,没有闪,我万万没想到Spring Boot 自己做了Redis的starter,所以大家以后整合第三方库,还是先去Spring Boot的starter表找一下,没有再按照 库-Spring-Boot-Starter去百度找官方文档。我们上面搜到的那个是在Spring的基础上又封装了一层,我们本次就只整合Spring提供的。
我们上面看到MyBatis的配置类是MybatisProperties,那么我们也可以类比猜想一下,Redis的配置类就是RedisProperties,其实我在整合的时候就是这么想的,然后一搜果然是。
我们来配一下:

spring.redis.host=192.168.2.129:6379
spring.redis.password=foobared

然后我们在Spring 配置类中配置一下RedisTemplate就行了。

总结一下

本篇基本上介绍了Spring Boot的基本使用和基本原理,算上我之前的几篇文章,整体算是基本圆满了。如果想学习框架不妨跟着我的文章走,先后顺序依次是:

希望会对大家学习会有所帮助。

参考资料

查看原文

赞 0 收藏 0 评论 0

北冥有只鱼 发布了文章 · 3月10日

假装是小白之重学Spring MVC(二)

在假装是小白之重学Spring MVC(一)中已经介绍了Spring MVC比较常用的知识点,但是还是有些遗漏,比如将请求放入Session,在配置文件中配置太烦了,我们能否将配置文件移动至配置类中,又比如SSM(Spring Spring MVC MyBatis)整合。
本篇的主要内容就是介绍这些,将配置文件转换成配置类需要懂一些Spring的核心注解,比如@Import,这个注解在欢迎光临Spring时代(一) 上柱国IOC列传已经介绍过了,不懂的可以再翻一下这篇文章。

放入session中

我们如何将数据放入session中呢? 在原生Servlet时代,我们从HttpServletRequest中获取HttpSession对象就好,像下面这样:

当然你在Spring MVC框架还可以接着用,Spring MVC又提供了@SessionAttributes注解用于将数据放入Session中,@SessionAttribute用于将session中的值,通过翻阅源码,我们可以发现@SessionAttributes只能作用于类上,在翻阅源码的过程中发现了@SessionAttribute注解,本来以为和@SessionAttributes是一样的用处 ,后来翻源码才发现,这个是用来取出Session中的值放到方法参数中,只能作用方法参数上。

又通过看源码的注释,我们可以看到SessionAttributes的两个value和name是同义语,假设有请求域(请求域中的数据都是呈K、V对形式,我门现在谈的就是key)中有和value值相同的数据,那么就会将该数据也会放到Session中。而Type属性则是,只要是请求域的数据是该类型的,就放入到Session中。
我们现在来测试一下@SessionAttributes:

Controller
SessionAttributes(value = "password")
public class SessionDemoController {

    @RequestMapping(value = "/session", method = RequestMethod.GET)
    public String testGet(@RequestParam(value = "name") String userName, @RequestParam(value = "password") String password ,Model model) {
        System.out.println("username:" + userName);
        System.out.println("password:" + password);
        model.addAttribute("password",password);
        return "success";
    }
}

success.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<h1> success </h1>
<h1> username:  ${sessionScope.userName}</h1>
<h1> password: ${sessionScope.password}</h1>
</body>
</html>

url: http://localhost:8080/studySpringFrameWork_war_exploded/session?name=aa&&password=aaa

测试结果:

SessionAttribute:

RequestMapping(value = "/servlet", method = RequestMethod.GET)
    public String testGet(@RequestParam(value = "name") String userName,  @SessionAttribute(value = "password")String password) {
        System.out.println("username:" + userName);
        System.out.println("password:" + password);
        return "success";
    }

url: http://localhost:8080/studySpringFrameWork_war_exploded/servlet?name=aaa
测试结果:

可以看到url中没有password,password依然打印出来有值,这个就是session中的值。

配置文件的配置移动至配置类

简单的说配置文件的本质就是,在读取配置文件的时候,根据标签将对应类的对象注册进IOC容器中。但是配置文件配置了太多也是很烦的,可不可以转换成配置类的形式呢? Spring MVC: 当然可以。
那首先第一个问题就是以前是配置文件中让Tomcat启动的时候读取配置文件,注册DispacherServlet怎么移植?
通过org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer来移植,上面的注释很清晰,有的时候碰见英文不要抵触,读一读多累积几个单词,语法大多都不难:

如果觉得这个格式不是很喜欢,可以去看对应的Java Doc,下面是地址:




接着我们来看WebApplicationInitializer的注释:

Interface to be implemented in Servlet 3.0+ environments in order to configure the ServletContext programmatically -- as opposed to (or possibly in conjunction with) the traditional web.xml-based approach.
该接口在在Servlet 3.0以上的版本才被实现能够实现以配置类的形式配置ServletContext ,和传统的基于web.xml的配置方式相对。

Implementations of this SPI will be detected automatically by SpringServletContainerInitializer, which itself is bootstrapped automatically by any Servlet 3.0 container. See its Javadoc for details on this bootstrapping mechanism.

SpringServletContainerInitializer会自动监测该接口的实现类,然后被任何支持Servlet 3.0版本以上的容器自动加载。

感觉是不是有点拗口,有点不明白什么意思的感觉,我的理解就是在支持Servlet 3.0的Servelt容器才能使用Spring MVC配置类的形式。下面这段注释说的是该接口的实现类会被Servlet容器自动加载。
但是我们这次并不是选择实现WebApplicationInitializer,这样做有些麻烦了,Spring MVC提供了一个简单点的Web初始化器: org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer,供我们使用。
AbstractAnnotationConfigDispatcherServletInitializer是一个抽象类,我们继承它,然后继承一下它看看:

public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[0];
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {

        return new Class[0];
    }    
    /**
     * 用于设置拦截的URL
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[0];
    }
} 

getRootConfigClasses和getServletConfigClasses这两个方法,我们就需要专门拿出来讲讲,从名字上可以推断都是加载配置类,那这两个方法有什么区别呢? 我们还是翻翻官方文档,提供了Java Config 和 基于xml的等价写法:

我们看下对比哈:

也就是说getServletConfigClasses作用就是等同于加载Spring MVC的配置文件,getServletMappings就是设定要拦截的URL。getRootConfigClasses。我们本次学习的Spring MVC都是只注册了一个DispatcherServlet,Spring MVC允许我们注册多个DispacherServlet,那么多个Spring MVC的配置文件就是隔开的,但是呢,在这些不同的DispatcherServlet之间如果有一些bean需要共享怎么办呢?getRootConfigClasses就加载顶层的配置类,被各个DispatcherServlet共享。

当前我们就只有一个DispatcherServlet,所以我们就只建一个空类。所以AppInitializer 就被改造成了下面这样:

public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{RootConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {

        return new Class[]{WebConfig.class};
    }

    /**
     * 用于设置拦截的URL
     *
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}
@Configuration
public class RootConfig {
}
@EnableWebMvc
@ComponentScan(basePackages = {"org.example.mvc"})
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyHandlerInterceptor());
    }
}

@EnableWebMvc是新面孔,我们这里需要大致讲一下。

@EnableWebMvc简介

要完全讲清楚@EnableWebMvc,我们完全可以再开一篇文章去讲,因为在这个注解身上,Spring做了大量的工作,要完全讲清楚不是一件简单的事情,这里我做的介绍只是让各位同学明白,@EnableWebMvc相当于配置文件中做了什么?

能翻官方文档,尽量还是翻官方文档,官方文档有这部分的介绍:

这个截图似乎还不是很清晰,我们放在IDE里截图看一下:

   <mvc:annotation-driven/>  这行不少视频都说,这个是把加上了@Controller的类变成处理请求的类,但是也不知道是不是跟我用的Spring MVC版本有关系,我没加这个也行。

但是建议你还是加,解释清楚不加也行(可能会出一些莫名的问题),但是这也不是一件简单的事情,因为这要涉及很多源码的解读,如果有兴致了解可以参看:

拦截器等标签的移植

各位仔细看上面的WebConfig这个类,这个类继承了WebMvcConfigurer ,我在里面重写了addInterceptors方法,从方法名字上可以推断出来,这个方法是加拦截器的。然后我们将完整的写一下:

@EnableWebMvc
@ComponentScan(basePackages = {"org.example.mvc"})
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyHandlerInterceptor()).addPathPatterns("/**").excludePathPatterns("login");
    }
}

我们再度对比一下:

关于bean标签我们不讲怎么移植,这在欢迎光临Spring时代(一) 上柱国IOC列传已经讲过了,这里不再讲一遍。WebMvcConfigurer 的方法这里就不一一拿出来细讲了,可以自己看下注释。我们这里只讲大致的思想。

测试一下

注意要先注释掉web.xml预先加载的那个配置。基本上测试是成功的。这里我就不再贴代码了。
我把代码放在码云上,各位有兴致的同学可以翻翻看看。基本上到这里,Spring MVC就告一段落了。
地址如下:

SSM整合

SSM 整合前言

到这里我们就可以开始讲一下,SSM整合了,当时跟同学一块学SSM整合的时候,有的同学不理解思想,就是背,有的时候自己尝试做整合,背错了就整合失败了。这里我希望讲思想,有了这个思想,我想再去做整合的时候,就会知道自己错在哪里。
在看到这里的时候,我希望你对Spring的思想有一点了解,建议参看我Spring系列的文章:

注意这里是SSM整合,那MyBatis也要求会,如果不会请参看我MyBatis系列的文章。

如果不会Spring MVC,请参看我Spring MVC系列的文章:

SSM整合的思想-天下归Spring统

Spring Framework 是一个成功的框架,解决了对象之间有复杂的依赖关系的时候,在不希望是硬编码的情况下,取对象优雅和可配置化,那么其他框架也要纳入到Spring Framework中,让我们取该框架的对象时候优雅一点。这也就是整合思想,将该框架的核心对象放入IOC容器中,然后取出来。再讲一遍,假设是MyBatis需要数据源、扫描xml,我们就统一在IOC容器中配置。
然后取的时候用Spring 提供的取对象方式,取出来就行了。SSM整合的核心思想就是将Spring MVC、MyBatis的核心对象放进IOC容器中,然后我们取。

那么我们对MyBatis 整合的愿景是什么呢? 我们再看下我们学习MyBatis的代码:

那怎么整合将MyBatis整合进入Spring才能实现这样的效果呢? 仅仅看目前的代码,我们是将SqlSessionFactory放入IOC容器中,然后从IOC容器获取session,再调用getMapper方法。这样是不是有点麻烦呢? 有没有更简洁的方案呢? 这个解决方案叫mybatis-spring,依赖如下:

  <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.6</version>
 </dependency>

这个解决方案能够做到将Mapper接口(即Xml对应的接口,下文会统称为Mapper对象)对象放入到IOC容器中,我们只用在需要对应Mapper的地方,用@Autowired注解注入即可

整合MyBatis

我们这里只介绍基于xml形式的,基于配置类的,将bean标签移动至配置类即可。

基于xml形式的

首先我们用Druid连接池管理数据源,那当然是在配置文件中配置一个即可:

 <!--加载配置文件-->
<bean id = "placeholderConfigurer" class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:jdbc.properties</value>
            </list>
        </property>
    </bean>
   <bean id = "dataSource" class = "com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.dev.driver}">
        </property>
        <property name = "url" value="${jdbc.dev.driver}">
        </property>
        <property name = "username" value = "${jdbc.dev.username}">
        </property>
        <property name = "password" value = "${jdbc.dev.password}">
        </property>
    </bean>

要实现将Mapper接口的对象放入到IOC容器中,核心是两个类:

  • org.mybatis.spring.SqlSessionFactoryBean
  • org.mybatis.spring.mapper.MapperScannerConfigurer

SqlSessionFactoryBean概览:

MapperScannerConfigurer概览:

其实上面也有官方文档:

地址是: http://mybatis.org/spring/zh/...
这上面讲了mybatis-spring的起源和如何整合,我们照着做就行。

  <bean  id = "sqlSessionFactory"  class = "org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref = "dataSource"></property>
        <property name = "mapperLocations" value="org\example\mybatis\*.xml">
        </property>
    </bean>

    <bean  id = "mapperScannerConfigurer" class = "org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value = "org.example.mybatis" />
        <property name="sqlSessionFactoryBeanName" value = "sqlSessionFactory"></property>
     </bean> 

可能有同学会问,那之前的配置文件咋弄? 切换环境怎么办?
SqlSessionFactoryBean中有一个configuration字段是Configuration类型的,我们看下Configuration类:

然后再配置就行了。如果你不想配置bean,也可以指定路径,通过SqlSessionFactoryBean的configLocation指定MyBatis的配置文件位置就可以了。

整合Spring MVC

其实在上面介绍Spring MVC的时候已经在用Spring,大家可能感受不到整合的这个过程,这就叫无缝集成。
接下来讲的就是,如果你有多个DisPatcherServlet,但是有一批bean是共享的,又不想在在每个配置文件中都配置一份。怎么办。在用配置类的情况下,我们是通过getRootConfigClasses来配置的,还记得它的xml写法吗?

<web-app>
    
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!-- 这里用来加载多有DispatcherServlet共享的bean-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>
    <servlet>
        <servlet-name>app1</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/app1-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app1</servlet-name>
        <url-pattern>/app1/*</url-pattern>
    </servlet-mapping>

</web-app>

小插曲

我以为这里整合的顺风顺水一点毛病都没有,就打算收尾了,然后就启动了项目,然后就报少jar包,少的是这两个:

<dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>

然后发现补上了之后,还是注入失败,此时我陷入了深思,最大的痛苦还是,我不知道我错在了哪里? 因为控制台也不报错,比报错还可怕的就是,程序不报错。于是我开始排查,在main函数里加载配置文件试了试看:

public static void main(String[] args) throws IOException {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-MVC.xml");
    BlogMapper blogMapper = applicationContext.getBean(BlogMapper.class);
    System.out.println(blogMapper.selectAllReturnMap());
}

发现成功获取,于是我就怀疑我的配置文件没加载,下面是我的web.xml中的配置:

然后是Spring MVC的配置文件:

于是我猜想首先加载的是context-param标签中的配置文件,在这个过程中完成对各个bean的初始化。
然后再加载Spring MVC的配置文件,对Spring MVC的一些bean进行初始化。
然后我把整合MyBatis的代码就移入了applicationContext.xml中,然后就成功了。

排错思路

基本上我也是在引入日志之后,才推断出来我的Spring-MVC配置文件没有成功加载的,日志可以把Spring IOC容器创建的bean打印出来:

通过日志我发现我单独在main函数中加载Spring MVC的配置文件的时候,有sqlSessionFactory创建成功的日志,但是我启动Tomcat就没有。所以我才推断Spring-MVC的配置文件加载的是不是有些晚了。然后验证一下果然是。日志配置也有一些坑,比如说Log 4j 2不支持properties的配置文件,名字也必须交log4j2.properties。然后日志怎么配,跟着GitHub上的配就行了。GitHub上对应项目的地址如下:

源码选讲之Spring MVC的执行流程

接下来我们研究一下Spring MVC的执行流程,这也是高频面试题,研究这个是为了让我们对Spring MVC的理解更进一步。
首先请求总是先进入到DispatcherServlet中,然后我们还是大致看一下DispatcherServlet这个源码,源码太庞大了,不便于展示,这里就直接看两个核心方法了:

  • HandlerExecutionChain (处理器执行链)
  • doDispatch (请求分发)

我们主要看doDispath方法:



基于此我们可以大致画出Spring MVC的执行流程:

其实这个Spring MVC执行流程已经人尽皆知了,毕竟是高频面试题,但是我是想从源码的角度去看。基本上整个流程都在DispatcherServlet的dispatch方法中,有兴致的同学可以也看一下。

总结一下

每次重学框架都会有新的认知,这次也算是小有收货,把心里以前的疑问都扫清了。希望对各位同学而学习Spring MVC会有所帮助,但是到这里就结束了吗? 远远没有,不觉得这些配置有些烦吗? 还有依赖管理,其实我做的时候都是小心翼翼的,生怕起了依赖冲突,能不能简化一下呢? 当然可以,这些都将由Spring 家族的一个明星成员Spring Boot给出答案。

参考资料

查看原文

赞 0 收藏 0 评论 0

北冥有只鱼 发布了文章 · 3月10日

假装是小白之重学Spring MVC(一)

在编码一段时间之后,再重新看之前学过的框架,发现有新的认知,像Spring,我就重新学习了一把:

这次重学的是Spring MVC,以前学习Spring MVC的时候是在B站看的视频,确实不错,让我快速入门了,但是我还是觉得不是很系统,感觉知识点支离破碎,我希望用我的方式将这些方式串联起来。在学习本篇之前,需要有Java EE和Spring Framework的基础,如果没有Java EE的基础,可以参看JavaWeb视频教程,如果没有Spring Framework的基础,请参看我上面的文章,如果觉得四篇有点多,可以先只看 欢迎光临Spring时代(一) 上柱国IOC列传,如果可以的话,还是建议先看完上面四篇文章来看这四篇文章。

原生Servlet时代的问题

简单的说Servlet是Java中的一个接口,位于javax.servlet下。我们来看一下Servlet上的注释:

A servlet is a small Java program that runs within a Web server. Servlets receive and respond to requests from Web clients,usually across HTTP, the HyperText Transfer Protocol.
Servlet是一个运行在Web 服务器中的一个小Java程序,Servlet接收和处理Web 客户端的请求,通常用于Http协议。
我们通常用的是它的实现类HttpServlet,然后重写doGet和doPost方法,像下面这样:
WebServlet(name ="/servlet")
public class ServletDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.getParameterNames();
        req.getParameter("username");

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 处理post请求
        super.doPost(req, resp);
    }
}

它有哪些痛点呢? 第一个就是取参数的问题,取参数只有这一种方式,参数一多就有点让人心烦了,不是那么的符合面向对象的思想,程序员除了关心业务逻辑的,还要关注如何获取参数,的确HttpServletRequest里面放置的参数很多了,很全很全:

那getParameterNames去哪里了? HttpServletRequest 是一个接口,getParameterNames从ServletRequest继承而来。

能否让程序员更加专注于业务逻辑一点呢,让取参数变的更简单一些,比如我想用学生类对象接收,又比如我需要的参数只有一个Long类型的id,接收参数的时候能否就直接让用Long id来接收呢。这是我们提出的第一个问题,取参数能否变的简单一些。除了取参数,在Ajax技术已经普遍应用的今天,能否让我响应前端的Ajax请求在简单一些呢? 在原生Servlet,我们处理Ajax,我们在响应中的设置编码,设置响应的类型,然后通过一些Java的JSON库将我们的对象转成JSON形式,发送给前端,大致上要这么做:

Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    Student student = new Student(13, "223", null);
    Gson gson = new Gson();
    resp.setContentType("application/json;charset=utf-8");
    resp.setCharacterEncoding("UTF-8");
    System.out.println(req.getMethod());
    System.out.println("处理get请求");
    PrintWriter writer = resp.getWriter();
    writer.println(gson.toJson(student));
}

这是我们提出的第二个问题。还有文件上传下载,原生Servlet的文件名乱码问题,异常的统一处理,系统出了错直接把出错日志返回到网页上?用户一脸懵逼,这是发生了什么? 我们能否统一的处理一下异常呢? 比如服务端出了问题,就直接给个提示,服务端发生了错误,请联系系统工程师。这样是不是更友好一些呢。

总结一下

我们这里再总结一下,我们使用原生Servlet 进行Web编程所遇到的痛点:

  • 处理参数麻烦,不符合面向对象原则
  • 处理Ajax麻烦,而且有些流程是通用的,但是还要重复编写
  • 上传和下载文件中文名乱码
  • 没有提供统一的异常处理机制

这四点让我们在关注于业务的逻辑的同时,也要在取参数上花上一番功夫,这些问题是使用原生Servlet普遍都会遇到的,
很自然我们在碰到这些问题之后会对原生的Servlet进行扩展,那么既然大家都会用到,那么就会有人或社区针对上面的问题,提出解决方案,一般我们称这种类型的框架为Web 框架,在Java Web领域比较常用的有:

  • Spring MVC
Spring 公司出品,其实人家叫Spring Web MVC,不过我们更习惯叫它Spring MVC,性能强大,可以和Spring Framework无缝集成。
  • Apache Struts 2.x
Apache 出品,不过现在来看没有Spring MVC性能高,Spring MVC使用更为广泛
  • Apache Tapestry 5.x
Apache 出品,这个倒是头一次见,我也是在重新学习Spring MVC的时候,去翻官方文档才看到的这个框架,查了一下,发现资料不多。这里作为了解吧。

本篇我们介绍的就是Spring MVC,各位同学在学习的时候,一定注意体会思想,技术发展的是很快的,如果疲于学习各种框架,你会发现淹没在技术的海洋中,那什么是通往彼岸的船呢?那就是思想,解决问题,可能思想是相同的,但是具体实现上可能会有一些小差异,这也就是学一通百。这也是我在文章开头会花很大的力气,介绍这种技术出现的原因以及思想,介绍框架的使用很容易,但是介绍思想相对来说就更难一些。

这里我还想顺带聊一下如何学习知识,一般来如果官方文档写的不错的话,我就会直接去看官方文档,因为相对来说官方文档更为权威,也是一手资料。我在重学Spring MVC,去翻了一下Spring MVC的官方文档,发现写的还是不错的,有兴致的同学也可以翻翻官方文档。

  1. 点击链接https://spring.io/projects/sp... 进入Spring官网。

准备工作

本篇我们还是使用maven来搭建项目,如果不懂maven还想下jar包,在 欢迎光临Spring时代(一) 上柱国IOC列传一文中我也介绍了下载jar包的方法,这里不再赘述,欢迎光临Spring时代(一) 上柱国IOC列传一文所需要的依赖我们还要接着用,新需要的依赖如下:

    <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.8.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

先用起来

上面介绍的Java Web领域的Web框架解决上面我们提出的问题就是通过扩展原生Servlet来做的,那如何使用Spring MVC提供的一系列扩展Servlet呢? 那么首先就要让Tomcat启动的时候知道,也就是在web.xml进行配置:

  <servlet>
        <servlet-name>springDispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
       <!--初始dispatcherServlet需要的参数contextConfigLocation,DispatcherServlet有这个属性-->
        <init-param>
            <!--加载配置文件,指明配置文件的位置-->
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </init-param>
        <!--元素标记容器是否应该在web应用程序启动的时候就加载这个servlet,(实例化并调用其init()方法)。-->
        <load-on-startup>1</load-on-startup>
    </servlet>
 <servlet-mapping>
        <servlet-name>springDispatcherServlet</servlet-name>
         <!--    /:  拦截一切请求   -->
        <!--  /user:  拦截所有以/user开头的请求  -->
        <!--  /user/abc.do: 只拦截该请求   -->
        <!--  .action:  只拦截.action的请求  -->
        <url-pattern>/</url-pattern>
 </servlet-mapping>

我们看下contextConfigLocation在DispatcherServlet哪里,首先到DispatcherServlet去看:

发现没找到,我们去它的父类找:

上面我们初始化DispatcherServlet的时候指明了配置文件的位置,那么配置文件中要配置什么呢? 除了数据库连接池之类的,我们回忆一下使用原生Servelt+jsp进行编程的时候,我们还能控制页面的跳转,虽然在前后端分离时代,这个页面的跳转被前端仔拿走了,但是Spring MVC还是提供了这样的功能,Spring中称之为视图解析器, 所以还要在配置文件中将这个配置文件中加进来,也就是纳入到IOC容器管辖。写到这里似乎有点Spring 整合Spring MVC的味道了,但是在Java Web领域Spring是管理对象中心,不管用什么框架你都得将核心对象纳入到IOC容器中,以便我们优雅的使用。所以在开篇我们强调学习本篇要有Spring的基础。
在配置文件中需要配置的:

  <!--配置扫描器-->
    <context:component-scan base-package="org.example"/>
    <!--配置视图解析器-->
    <bean id = "internalResourceViewResolver" class = "org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name = "prefix" value = "view/"></property>
        <property name = "suffix" value = ".jsp"></property>
    </bean>

学过Spring的人会知道配置扫描器的作用,将加上了Spring提供注解的类(@Controller、@Service、@Repository、@Component)加入到IOC容器中,那配置的这个视图解析器是做什么的呢? 该类就用来控制页面的跳转,那这个前缀是什么意思呢? 假如Spring MVC认为你返回的是一个视图(也就是页面)的话,如下面:

// 该注解将该类变为处理HTTP请求的类
Controller
public class StudentController {
    // 将处理请求方式为GET,URI为servlet的请求
    @RequestMapping(value = "/servlet", method = RequestMethod.GET)
    public String testGet() {
        System.out.println("hello world");
        return "success";
    }
     // 将处理请求方式为post,URI为servlet的请求
    @RequestMapping(value = "/servlet", method = RequestMethod.POST)
    public String testPost() {
        System.out.println("hello world");
        return "success";
    }
}

返回值是String,Spring MVC会认为你要跳转到view/success.jsp页面。下面我们在webapp下建一个名为success.jsp。

接下来我们测试一下:

梳理一下

我们要使用Spring MVC提供的扩展Servlet,首先需要在web.xml配置文件让Spring MVC的DispatcherServlet接管我们的所有请求,为了做到这一点,我们需要让Tomcat在启动的时候就初始化DispatcherServlet。

当一个类上拥有@Controller注解,则该类在Spring MVC框架中就被视为一个能够处理请求的类,该类中拥有@RequestMapping的方法,将能够处理HTTP请求。@RequestMapping中的value属性用于指定URI,假如HTTP请求的URI和该方法上@RequestMapping的value属性吻合,那么请求将进入该方法。 @RequestMapping中的method用于限制请求方式,method值要求是RequestMethod类型,RequestMethod是Method。

由于在Spring MVC在设计之初,前后端分离时代还没到来,那个时候Java还能控制页面的跳转,所以Spring MVC用internalResourceViewResolver做视图解析。但是本篇并不会介绍多少有关页面跳转,静态资源处理的东西,这些东西已经不再适合这个前后端分离时代。

简化取参数的方式

似乎Spring MVC下处理请求的类只是变的简介了一些,并没有让我们感受到取参数的简化啊。我们接下来就通过几个注解来体会一下Spring MVC下优雅的取参数方式。

直接接对象

Spring MVC可以将前端的参数直接变为一个对象,在参数名能对应的情况下,首先我们准备一个简单的对象:

// 为了节省篇幅,这里引入了Lombok插件,通过这些注解就可以产生构造函数,get、set方法、重写toString方法
Data 
Getter 
Setter
@ToString 
public class User {
    private String userName;
    private String password;
}

@Controller
public class StudentController {

@RequestMapping(value = "/obj", method = RequestMethod.GET)
public String testOBj(User user) {
    System.out.println(user);
    return "success";
}

}
测试一下:

测试结果

@RequestParam

@Controller
public class StudentController {
    @RequestMapping(value = "/servlet", method = RequestMethod.GET)
    public String testGet(@RequestParam(value = "name") String userName, @RequestParam(value = "password") String password) {
        System.out.println("username:" + userName);
        System.out.println("password:" + password);
        return "success";
    }
}

假如HTTP请求的参数名和方法的参数名相同,在请求进入该方法的时候,Spring MVC的请求方法的方法参数可以直接接收。
假如HTTP的请求和Controller中的方法形参不相同,也就是不同名,可通过@RequestParam作为桥梁,通过value属性将请求参数映射到指定参数变量上,但是形参上有@RequestParam,则默认该形参必须有,否则会报错。
我们测试一下:

可以通过@RequestParam的required属性设置为false,取消要求该参数必须有的设定。默认情况@RequestParam要求的参数必须要有。

@RequestHeader和@CookieValue

  • @RequestHeader: 完成请求头(header)数据到处理器功能处理方法参数上的绑定
  • @CookieValue: 完成Cookie数据到处理器功能处理方法的方法参数上的绑定

我们首先看下请求头里有什么:

然后再捋一捋Session和Cookie之间的关系:
由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识别具体的用户,这个机制就是Session。这个机制就是Session。典型的场景比如购物车,当你电脑下单按钮时,由于HTTP协议的无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用于标识特定的用户,这个Session是保存在服务端的。
服务端在识别到特定的用户的时候,就需要Cookie登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用Cookie来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在Cookie里面记录一个Session Id,以后每次请求把这个回话ID发送到服务器,我就知道你是谁了。
然后我们看下Cookie中有什么:

示例:

@Controller
public class StudentController {
    @RequestMapping(value = "info", method = RequestMethod.GET)
    public void test(@RequestHeader("User-Agent") String useAgent, @CookieValue("JSESSIONID") String jsesionId) {
        System.out.println(useAgent);
        System.out.println(jsesionId);
    }
}

@RequestBody

我们知道get请求,请求参数放置在URI之后,没有请求体,post请求的请求参数可以放在URI之后,也可以放在请求体中。
上面我们已经可以看到,假如请求参数是get,且参数名和请求方法中形参的属性名能对的上,那Spring MVC就会将请求参数帮我们封装为一个对象,那对于post请求呢? 我将请求参数放置在请求体中,我们就需要通过@RequestBody就可以将post请求发送的json格式的数据,转为Java中的对象。

简化文件上传

原生Servlet的上传

前台代码:

 <form action="upload" method = "post" enctype = "multipart/form-data">
       用户名:<input type = "text" name = "userName"/>
       密码:<input type = "text" name = "password"/>
       文件:<input type = "file" name = "pic">
       <input type = "submit" value = "提交">
   </form>

在原生Servlet时代,我们通过Apache提供的组件做文件上传,后台的逻辑大致是这样的:

 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 统一编码
        req.setCharacterEncoding("utf-8");
        resp.setCharacterEncoding("utf-8");
        resp.setContentType("text/html;charset=UTF-8");
        boolean isMultipartContent = ServletFileUpload.isMultipartContent(req);
        // 表单中有上传文件,method中必须有enctype这个属性
        if (isMultipartContent) {
            FileItemFactory fileItemFactory = new DiskFileItemFactory();
            ServletFileUpload servletFileUpload = new ServletFileUpload(fileItemFactory);
            List<FileItem> items = servletFileUpload.parseRequest(req);
            for (FileItem item : items) {
                if (item.isFormField()){
                    // 处理表单的非文件字段
                }else{
                    // 将上传文件写入到文件服务器中
                    item.write(new File("服务器的路径/"));
                }
            }
        }
    }

Spring MVC下的使用Apache组件进行文件上传

  • 使用Apache组件的上传就变成了这样

首先在配置文件中,我们要配置文件解析器:

 <bean id = "commonsMultipartResolver" class = "org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize">
            <!--单位是B 1048576 = 1024 * 1024 -->
            <value>1048576</value>
        </property>
    </bean>
 // 使用MultipartFile当形参,SpringMVC会自动将表单中的文件对象注入到该参数中
 // SpringMVC也会尝试将表单的非文件对象放入User对象中
 // post请求我们一般习惯性将文件对象放进请求体中
 @RequestMapping(value = "mvcUpload" , method = RequestMethod.POST)
  public void testUpload1(@RequestBody  User u , MultipartFile pic){

 }

忘了讲,这两个Apache组件的依赖如下:

<dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.3</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.5</version>
        </dependency>

Servlet3 实现文件上传

首先在web.xml中配置上传的位置:

 <servlet>
        <servlet-name>springDispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--初始dispatcherServlet需要的参数contextConfigLocation,DispatcherServlet有这个属性-->
        <init-param>
        <!--加载配置文件,指明配置文件的位置-->
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </init-param>
        <!--元素标记容器是否应该在web应用程序启动的时候就加载这个servlet,(实例化并调用其init()方法)。-->
        <load-on-startup>1</load-on-startup>
        <multipart-config>
            <location>c:temp</location>
            <max-file-size>1048576</max-file-size>
            <!--单个数据大小-->
            <file-size-threshold>10240</file-size-threshold>
        </multipart-config>
    </servlet>

这里强调一下,要注意Servlet的版本:

然后在IOC容器中加入:

   <bean  id = "multipartResolver" class = "org.springframework.web.multipart.support.StandardServletMultipartResolver">
    </bean>

后端代码不变。

简化文件下载

这里其实感觉没有简化多少,用原生的Servlet也不是那么麻烦。Spring MVC这里又给我们提供了一种选择而已。
下面是示例:

 @RequestMapping(value = "testUpload")
    public void downloadFile(Long id, HttpServletResponse resp, HttpServletRequest request) throws IOException {
        // 根据id去查文件的位置
        resp.setContentType("application/x-msdownload");
        String userAgent = request.getHeader("User-Agent");
        if (userAgent.contains("IE")) {
            resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("文件名", StandardCharsets.UTF_8.name()));
        } else {
            resp.setHeader("Content-Disposition", "attachment;filename=" + new String("文件名".getBytes("UTF-8"), StandardCharsets.ISO_8859_1.name()));
        }
        Files.copy(Paths.get("文件夹", "文件名"), resp.getOutputStream());
    }

    @RequestMapping(value = "testUpload2")
    public ResponseEntity<byte[]> testUpload(Long id, HttpServletResponse resp, HttpServletRequest request) throws IOException {
        // 根据id去查文件的位置
        resp.setContentType("application/x-msdownload");
        String userAgent = request.getHeader("User-Agent");
        HttpHeaders httpHeaders = new HttpHeaders();
        if (userAgent.contains("IE")) {
            resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("文件名", StandardCharsets.UTF_8.name()));
        } else {
            resp.setHeader("Content-Disposition", "attachment;filename=" + new String("文件名".getBytes("UTF-8"), StandardCharsets.ISO_8859_1.name()));
        }
        Files.copy(Paths.get("服务器上的文件夹", "文件名"), resp.getOutputStream());
        httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        byte[] array = FileUtils.readFileToByteArray(new File("文件夹", "文件名"));
        return new ResponseEntity<byte[]>(array,httpHeaders, HttpStatus.CREATED);
    }

对Restful风格的支持

什么是Restful风格

Restfu是一种软件风格,严格意义上Restfu是一种编码风格,简单的说,通常情况我们设计的后端接口可以对应四种操作,即增删查改,这也是服务端工程师经常被称作CRUD仔的原因,那么怎么让增删查改和请求方式绑定在一起呢? 对Restful风格不熟悉的朋友可能会说,不是只有两种请求方式吗? 怎么对应四种操作呢? 事实上HTTP协议规定的请求方式可不止有四种,有人说是十四种,但是我在火狐开发者文档上只查到八种,但是浏览器只支持两种,所以我们用原生Servlet进行编程的时候,只重写了HttpServlet的doGet和doPost方法。 这八种请求方式有四种我们可以拎出来看看:

  • DELETE
  • GET
  • POST
  • PUT

粗略的说,Restful风格中规定DELETE代表删除某个资源,GET代表向服务端获取某个资源,POST代表向服务器新增一个资源,PUT代表向服务器请求更新一个资源。这样的设计理念,就不用开发者在设计接口的时候,在接口中加入动词来标识这个接口要完成的动作,这样更有利于维护,贯彻单一设计职责,后续的开发人员从接口就能看出来这个请求是做什么的,这也是Restful风格盛行的原因。

但是我在翻阅火狐的开发者文档的时候,发现文档在介绍浏览器对八种请求方式都是支持的:

但是我在网上翻了许多资料,都是说浏览器只支持GET和POST的请求,仔细想了想,这也许跟浏览器的版本有关系吧。不同版本支持的HTTP协议是不同的,HTTP1.1之前,请求方式只有GET和POST两种,HTTP1.1协议新增了五种请求方式:

  • OPTIONS
  • HEAD
  • PUT
  • DELETE
  • TRACE

浏览器支持是支持,但是客户端发起请求的时候,怎么让服务端知道你用的是PUT和DELETE请求的,通用的做法就是在表单代码中增加隐藏属性,把POST请求包装成PUT请求。
另外顺便提一句,火狐浏览器的开发者文档写的不错,有介绍HTTP协议的,想学HTTP协议,找不到权威的参考资料的可以去看下,文末的参考资料有链接。

对Restful的支持

  • @ResponseBody
在如今的前后端分离时代,跳转页面已经被前端控制,前后端通信的方式也是通过Ajax实现局部刷新,但是在SpringMVC还是有跳转视图的概念,那怎么告知Spring MVC,我不要跳转页面,我就是给前端页面发送了数据呢? 就是通过在请求方法上加@ResponseBody来完成的。像下面这样:
  • @RestController
如果你觉得每个方法加@ResponseBody比较方法,可以在类上加上这个注解,加上
  • @GetMapping@PostMapping@PutMapping@DeleteMapping
见名知义,支持不同的请求方式,用法和@RequestMapping一样。将这五个注解统称为请求注解。
  • @PathVariable
服务端使用请求注解后,可以类似{参数名}站位符传参,此时需要通过@PathVariable注解可以将URL中站位符参数绑定到控制器处理方法的形参中。

此时客户端的请求地址如: http://localhost:8080/studySpringFrameWork_war_exploded/delete/1
服务端代码:

@RequestMapping(value = "delete/{id}", method = RequestMethod.GET)
public String getInfo(@PathVariable(value = "id") String id) {
    System.out.println("id:" + id);
    return id;
}

{参数值} 和 @PathVariable(value = "id") 要维持一致。

统一的异常处理

简介

Spring MVC处理异常有三种方式:
(1) 使用Spring MVC 提供的简单异常处理器 SimpleMappingExceptionResolver
(2) 现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器
(3) 使用@ExceptionHandler + @ControllerAdvice注解实现异常处理
最顶层的异常处理就是HandlerExceptionResolver,继承结构图如下:

一般我们常用的是第一种和第三种处理方式。

第一种处理方式

第一种方式具体的实操就是在配置文件中配置,要对哪些异常进行处理,跳转到哪些页面,像下面这样:

   <bean id="simpleMappingExceptionResolver"  class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <!-- 定义默认的异常处理页面,当该异常类型发生时跳转到error页面-->
        <property name="defaultErrorView" value="error"/>
        <!--定义异常处理页面用来获取异常信息的变量名,默认名为exception-->
        <property name="exceptionAttribute" value="ex"/>
        <!--定义需要特殊处理的异常,有两种配置方式 1种是value标签方式  2.props标签方式  还是配置类更好,无需关心这些细节-->
        <property name="exceptionMappings">
            <!-- value标签多个异常用逗号隔开-->
<!--            <value>-->
<!--                java.lang.ArithmeticException=error,-->
<!--                java.lang.NullPointerException=exception-->
<!--            </value>-->
            <!-- props标签,每个prop标签的key代表处理一种异常,prop标签中代表具体要跳转的页面 -->
            <props>
                <prop key="java.lang.ArithmeticException">
                    error
                </prop>
            </props>
        </property>
    </bean>

第三种处理方式

// 这个注解会处理所有方法的异常
@ControllerAdvice
public class ExceptionHandlerControllerAdvice {
    // 该注解中声明要处理哪些异常
    @ExceptionHandler({Exception.class})
    public  String error(Exception ex , Model model){
        // 向请求域中放入异常信息
        model.addAttribute("errorMsg",ex.getMessage());
        // 跳转至error页面
        return "error";
    }
}

拦截器

简介

在用户的请求到达请求方法之前会进入拦截器,我们可以在拦截器里面做一些通用操作,比如鉴权,判断用户登录之类的。折让我想到了AOP,也可以做到在方法之前、方法执行之后执行。那拦截器和AOP有什么区别呢? 某种意义上拦截器可以算在AOP里面,但是拦截器又不能算在AOP里面,我们简单的讲一下拦截器的原理,用户的请求在到达DispacherServlet之后,由DispacherServlet分发请求,DispacherServlet会查看该请求是否被拦截器拦截,如果被拦截,那么先将该请求交给拦截器,如下图所示:

而AOP则是通过动态代理来做的,在运行的时候生成需要增强方法的类的子类来做到的。拦截器算是在整个Spring MVC执行流程的一环,只能拦URL,AOP则更为细致。执行顺序上,拦截器先于AOP执行。

怎么用?

实现org.springframework.web.servlet.HandlerInterceptor接口。我们先大致的看一下这个方法:

preHandle在请求到处理器之前执行,postHandle在处理请求的方法执行完毕之后,视图渲染之前执行。afterCompletion视图渲染之后执行。这里的视图是什么意思呢? 也就是Spring MVC的V视图层,页面渲染完就是页面形成完毕
然后在配置文件或配置类中配置拦截器即可。
首先实现HandlerInterceptor接口:

public class MyHandlerInterceptor implements HandlerInterceptor {
    /**
     * 返回true代表将请求交给真正的控制器
     * 返回false代表将请求放给下一个拦截器,如果找不到下一个拦截器,
     * 则该请求不会到达真正的拦截器
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("在方法执行之前执行................");
        return true;
    }
    /**
     * 在处理器方法执行之后执行
     * ModelAndView中存放方法返回的数据和要跳转的视图
     * 方法未成功执行,不会触发此方法
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("在方法执行之后执行................");
    }

    /**
     * 在页面渲染之后获取
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 打印方法的异常信息
        ex.printStackTrace();
        System.out.println("在视图渲染完毕执行之后执行................");
    }
}

配置文件中配置:

  <mvc:interceptors>
            <mvc:interceptor>
<!--                /*: 只能拦截一级路径-->
<!--                /**: 可以拦截一级或多级路径-->
                <mvc:mapping path = "/**"/>
                <mvc:exclude-mapping path="login"/>
                <bean class="org.example.mvc.MyHandlerInterceptor"></bean>
            </mvc:interceptor>
    </mvc:interceptors>

补充介绍

Spring MVC的M(model)、V(view)、C(Controller),上面我们似乎就将关注点放在C上了,也就是接收参数,处理参数了。其实M也讲了,就是用@ResponseBody向前端返回JSON格式的数据,还有V我们这里也并没有细讲,原因在于这些并不常用了,服务端已经不能控制住页面的跳转了,关于返回数据也主要是向前端返回JSON格式的数据。上面似乎有提到过Model,这是一个在跳转对应页面后取数据的类,在Controller取完数据之后,将数据放入Model,然后指明跳转到哪个页面,然后在对应的页面取出Model中存储的数据,Model是Spring MVC提供给我们的类,除此之外还有ModelAndView,既能存放数据又能存放视图,当返回类型是这个的时候,SpringMVC会ModelAndView取视图跳转,然后我们就可以在对应的页面取数据了。但是这些并不常用,所以着墨不多,本文重点关注的都是在前后端分离时代,高频常用的,其实这里本来想介绍Spring MVC统一处理乱码的,就是在web.xml中配置一个过滤器:

<filter>
        <filter-name>encoding</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>utf-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encoding</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

但是不知道咋会事,也许是我用的Spring MVC版本太新了,这个乱码问题就被解决了。如果有同学在编码的时候发现有中文乱码问题的话,可以在web.xml把上面的配置加一下。

写在最后

每次重学框架都会有新的认知,一不小心又把篇幅拉的太大了,总结一下本文讲了什么,本文主要讲了Spring MVC的高频使用点,Spring MVC对原生的Servlet进行了增强,为了使用Spring MVC提供的增强的Servlet,我们需要让Spring MVC的DispatcherServlet接管所有的请求,这也就是在Tomcar的配置文件web.xml配置在启动的时候就初始化DispatcherServlet,由于Spring MVC刚设计之初,服务端还能控制页面跳转,所以我们还要在配置文件中配置视图解析器,也就是InternalResourceViewResolver。然后我们就能体会到Spring MVC提供给我们的遍历,简化取参数的方式让我们更专注于业务逻辑,统一的异常处理让我们的页面不至于直接把错误信息直接输出到页面上,简单的获取前端文件的方式,拦截器可以让我们统一进行鉴权,以及对RESTful风格的友好支持。希望对大家会有所帮助。

参考资料

查看原文

赞 0 收藏 0 评论 0

北冥有只鱼 发布了文章 · 3月10日

假装是小白之重学MyBatis(一)

在工作中发现对MyBatis还是有理解不到位的地方,所以就打算重新学习一下MyBatis。

简介

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

首先是MyBatis给自己的定位是持久层框架,那什么是持久层,简单的说就是和数据库沟通的那一层,也就是MVC模式下的dao(Data Access Object)层,dao层的代码负责将业务代码变成对数据库表的操作,至于为什么叫持久层,我觉得是跟数据库的持久性特点有关系呢!

在dao层没有任何框架之前,我们是直接使用原生的jdbc来操纵数据库的数据,拼接SQL,设置参数,获取结果集重复性的工作常常让人十分厌烦,但是JDBC从设计思路上讲也足够优秀了,做到了跨数据库,让使用者不必关心是哪个数据库,而采取不同的操作,JDBC就是一组接口,由各大数据库厂商提供对应的实现类,所以也不大可能做完全的定制的化操作,所以JDBC的设计思想就是宽泛一点。

但是我们希望简单点,所以如果你看视频去学习的话,基本上学完JDBC,就会讲如何封装一个工具类JdbcUtils,来避免重复代码的编写,但这并不是一个Java程序员的痛点,对吗?Java社区也关注到了这个问题,开始着手对JDBC进行扩展,进行升级。这也就是MyBatis、Hibernate、Spring Data JPA等ORM框架。

等等你刚才又提到了一个名词,ORM框架,那什么是ORM框架? ORM Object Relational Mapping 即对象关系映射,听起来好抽象啊! 不要着急,听我细细道来,Java是一门面向对象的语言,我们现在普遍使用的数据库是关系型数据库(表为主要形式),ORM的思想就是能否将表映射为对象呢? 一条数据记录就是一个对象。

所以MyBatis是一款优秀的持久层、ORM框架,在JDBC的基础上进行扩展、封装,免除了几乎所有JDBC代码以及设置参数和获取结果集的工作,大大简化了持久层代码开发。

对简化了持久层代码的开发,简单点,简单点,我们都喜欢简单的东西。

如何学习一门技术?

一般情况,学一门框架,最好还是去官网去学,以前我是图速度快,去B站找的视频。现在发现MyBatis官方写的教程挺不错的,更让我喜欢的是有中文版本:

好到我让我觉得我这篇博客,是不是还是有必要写。但是思虑再三,还是打算写,官方文档配合自己的理解,让自己的知识更成系统。

准备工作

要用MyBatis,我们首先要引入MyBatis,MyBatis是在原生JDBC的基础上做扩展,所以我们要引入对应的数据库驱动(数据库驱动就是数据库厂商实现的JDBC),本篇我们使用的是MySQL,Druid来管理数据库连接。 本篇我们依然使用Maven来搭建项目:
对应的依赖如下:

<dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.5</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.30</version>
            <scope>test</scope>
        </dependency>

slf4j是日志框架,输出的信息会更细致,建议引入。

如果你不会用maven

建议你去学maven,参看我这篇博客: Maven学习笔记,非常通俗易懂的入门。

如果你不想学Maven,想用jar包模式,也行,我的博客就是这么贴心,哈哈哈哈。

  1. 首先进入MyBatis官网

3.

第一个MyBatis 程序

首先我们要建一个配置文件


对了还要建一个表,如果你不懂的建表,本篇文章可能就不适合你,我建的表叫Blog。
表语句我就不放了,最近重学SSM,写了一些例子,都放在GitHub上了,下面是链接:

大概解释一下配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--加载配置文件-->
    <properties resource="jdbc.properties"/>
    <!--指定默认环境, 一般情况下,我们有三套环境,dev 开发 ,uat 测试 ,prod 生产 -->
    <environments default="development">
        <environment id="development">
            <!-- 设置事务管理器的管理方式  -->
            <transactionManager type="JDBC"/>
            <!-- 设置数据源连接的关联方式为数据池  -->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.dev.driver}"/>
                <property name="url" value="${jdbc.dev.url}"/>
                <property name="username" value="${jdbc.dev.username}"/>
                <property name="password" value="${jdbc.dev.password}"/>
            </dataSource>
        </environment>
    </environments>  
    <mappers>
        <!--设置扫描的xml,org/example/mybatis是包的全类名,这个BlogMapper.xml会讲-->
        <mapper resource="org/example/mybatis/BlogMapper.xml"/>
    </mappers>
</configuration>

Hello World

新建一个接口

public interface BlogMapper {
    Blog selectBlog(Long id);
}

新建一个xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 这里的namespace必须是BlogMapper的全类名 -->
<mapper namespace = "org.example.mybatis.BlogMapper">
      <!-- MyBatis 会自动将查询出来的记录封装为resultType设定类型的对象,这里体会一下ORM的思想-->
    <select id = "selectBlog" resultType = "org.example.mvc.entity.Blog">
            select * from Blog where id = #{id}
    </select>
</mapper>

测试代码

 public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        // 读取配置文件
        InputStream inputStream = Resources.getResourceAsStream(resource);
        // 构建一个SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 开启一个会话
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 加载指定的接口
        BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
        // 调用接口中的方法,相当于执行对应mapper中的查询语句
        Blog blog = blogMapper.selectBlog(1L);
        // 打印查询语句
        System.out.println(blog);
    }

执行结果:

总结一下

第一个程序可能还比较晕哈,先跟着做,然后我们来解释一下MyBatis做了什么:

那BlogMaper.java究竟是怎么和BlogMapper.xml关联起来的呢? 还记得上面的配置吗?


本质上还是动态代理,运行时创建接口的实现类,如果你不懂什么是动态代理,请参看:

用MyBatis操纵数据库的基本流程:

  • 新建接口
  • 建xml,这里要注意一下,XML也不能瞎建,上面的约束还是要注意一下:


不用记也没关系,可以在MyBatis官网拷贝一下:

  • 在配置文件的mapper标签,开启扫描标签,关联接口和xml文件(可以批量设定的,后面会讲)
  • 然后SqlSession对象的获取指定接口的方法,即可调用对应的SQL语句。

仔细体会下,这样相对于原生的JDBC是不是更加清晰了呢。

传递参数

上面我们在调用BlogMapper中的selectBlog仅仅只是在接口中写了参数,MYBatis就能自动的将#{id}替换为我们传递的参数,是不是很强大呢! 这种方式也支持多个参数,但是该SQL需要的参数有七八个怎么办?接口方法上写七八个参数? 其实也行,那传的是一个数组或者List呢? MyBatis能遍历一下吗? MyBatis: 当然支持。
对于数据库来说,查总是最让我们关心的,select标签的属性也是最多的,如下面代码所示:

<select id="selectPerson"
        parameterType="int"
        parameterMap="deprecated"
        resultType="hashmap"
        resultMap="personResultMap"
        flushCache="false"
        useCache="true"
        timeout="10"
        fetchSize="256"
        statementType="PREPARED"
        resultSetType="FORWARD_ONLY">
</select>

这些属性在MyBatis官网有详细的介绍:


我们这里只拎出来常用的来介绍,在传递参数这里,我们介绍的就是parameterType(参数类型)属性,这个属性是可选的,MyBatis可以通过Typehandler来推断出来传递的参数,来将我们SQL语句中的#{id}(我们下文会统一称之为占位符)替换掉。
注意单个参数,占位符中参数名和方法中的参数名无需保持一致。多个参数时,就需要启用@Param注解了。@Param中的属性值要和占位符中的参数名保持一致,不然MyBatis拿到两个参数值,无法推断出应该用哪个值替换占位符。

如果你想要用对象

直接传对象(Map也是一样), 然后在占位符中写对应的属性名(Map的时候是key)即可:

<!-- 注意这个时候写在占位符中的属性名要有get方法,否则调用会失败-->
    <select id="selectBlogByObj"   resultType="org.example.mvc.entity.Blog"  >
            select * from Blog where id = #{id}
    </select>

测试代码:

public static void main(String[] args) throws IOException {
        selectByObj();
    }
    private static void selectByObj() throws IOException {
        BlogMapper blogMapper = getMapper(BlogMapper.class);
        Blog blog = new Blog();
        blog.setId(1L);
        blog.setName("aa");
        blog = blogMapper.selectBlogByObj(blog);
        System.out.println(blog);
    }
    public static <T> T getMapper(Class<T> t) throws IOException {
        String resource = "mybatis-config.xml";
        // 读取配置文件
        InputStream inputStream = Resources.getResourceAsStream(resource);
        // 构建一个SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 开启一个会话
        SqlSession sqlSession = sqlSessionFactory.openSession();
        return (T) sqlSession.getMapper(t);
    }

运行结果:

下面再介绍使用的时候,不会再贴getMapper和main方法,只写对应的方法。

集合

遍历List 、数组 、Map

集合常常被用来构建in语句,那么在xml中怎么遍历呢? 通过foreach标签来遍历,像下面这样:

   <select id = "selectBlogByList" resultType="org.example.mvc.entity.Blog">
        select * from Blog where id in
        <foreach collection="list" item="item" open="(" separator="," close=")">
            #{item}
        </foreach>
    </select>

这个foreach标签就是把java中foreach映射进到xml了而已。item是迭代元素 , list是迭代的集合。open、close用于指定拼接形成的字符串,以及集合项迭代的分隔符。也就是说假设我传入的List有1,2这两个元素,最终形成的sql语句就会是下面这样:

 select * from Blog where id in (1,2)

看MyBatis 多么的智能。假如集合类型是Map的时候,index 是键,item 是值。
上面我查到的可能是多个对象,那在MyBatis中应该怎么接收呢?只用将该标签id对应的方法改为List<Blog>就行。

  List<Blog> selectBlogByArray(Long[] idLongArray);

默认情况下,collection即为集合名的纯小写,比如传入的是List类型,那么Collection中就应该写list。如果是数组就应该写Array。假如用@Param指定了参数名,那么就写@Param中指定的参数名。

返回类型

Map

上面我们已经讲了返回一条数据和返回多条数据,这个用Map接收返回类型,看上去有点违背直觉,事实上他也是完全符合直觉的。我们用Map接收一下试试看:

<select id = "selectAllReturnMap" resultType="map">
     select * from Blog
</select>

接口中声明的方法:

   Map<String,Object> selectAllReturnMap();
 private static void selectAllReturnMap() throws IOException {
        BlogMapper blogMapper = getMapper(BlogMapper.class);
        System.out.println(blogMapper.selectAllReturnMap());
    }

测试结果如下:

大意就是返回了三个,你用一个接收,接收不了。
可能这个时候又同学就会问了,Map不是能装多组值吗? 为啥收不了三条记录,那我们想一下Map的key是不是不能重复,三条记录的key都是属性名,某种意义上一个Map就是一个对象,key就是属性名,value就是属性值。所以我们应该这么收:

List<Map<String,Object>> selectAllReturnMap();

测试结果:

{占位符} VS ${占位符}

上面我们用的替换参数的占位符,都是#号开头的,MyBatis会把#{参数名}替换为我们调用接口对应方法时传递的参数值,一般我们称之为#号占位符,其实还有一种是以$开头的。我们称之为dollar(刀乐)占位符。默认情况下,使用 #{} 参数语法时,MyBatis 会创建 PreparedStatement 参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。
下面我们用一个例子来感受一下两者的不一样,我们先配下日志,日志输出真正执行的SQL,方便我们分析问题

我们按照要求引入对应的依赖:

 <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.12.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>2.14.0</version>
            <type>pom</type>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>

然后在resources建一个文件log4j.properties,文件内容如下:

# 全局日志配置
log4j.rootLogger=DEBUG,ERROR, stdout
# MyBatis 日志配置 会输出执行包下的详细信息
log4j.logger.org.mybatis.example=DEBUG 
# 控制台输出
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

这个照着贴就行,介绍日志并不是本篇的内容,我们是借助日志来研究井号占位符和美元占位符的不同之处。
然后在配置文件中开启日志:

   <settings>
        <setting name="logImpl" value="LOG4J"/>
    </settings>

号占位符

xml中的代码:

  <select id="selectByMark"  resultType = "org.example.mvc.entity.Blog">
             select * from Blog where id = #{id} and name =  #{name}
    </select>

BlogMapper.中的代码:

List<Blog> selectByMark(@Param("id") String id,@Param("name") String name);

测试代码:

  private static void markVsDollar() throws IOException {
        BlogMapper blogMapper = getMapper(BlogMapper.class);
        blogMapper.selectByMark("1","aa; delete Blog;");
    // blogMapper.selectByDollar("1","aa; delete Blog");
    }

执行结果:

DEBUG [main] - Created connection 551479935.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@20deea7f]
DEBUG [main] - ==>  Preparing: select * from Blog where id = ? and name = ?
DEBUG [main] - ==> Parameters: 1(String), aa or 1 = 1(String)
DEBUG [main] - <==      Total: 0

所以最终的SQL语句就是: select * from Blog where id = '1' and name = 'aa or 1 = 1'
我们的数据库并没有这样的数据,所以一条这样的数据都没查出来。

$号占位符

xml中的代码:

  <select id="selectByDollar"  resultType = "org.example.mvc.entity.Blog">
             select * from Blog where id = ${id} and name = ${name}
    </select>
List<Blog> selectByDollar(@Param("id") String id, @Param("name") String name);

测试代码:

  private static void markVsDollar() throws IOException {
        BlogMapper blogMapper = getMapper(BlogMapper.class);
        blogMapper.selectByDollar("1","1 or  1 = 1;");
    }

执行结果:

DEBUG [main] - Created connection 1327006586.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4f18837a]
DEBUG [main] - ==>  Preparing: select * from Blog where id = 1 and name = 1 or 1 = 1;
DEBUG [main] - ==> Parameters: 
DEBUG [main] - <==      Total: 2

各位仔细的对比一下两个实际执行的SQL语句,一个Parameters有值,有参数类型,一个没有,直接是原样替换。
第二种事实上被称之为SQL注入,查到了不应该查到的记录。但是$号占位符也不是一无是处,比如在排序的时候,根据前端传递的字段来进行排序:

ORDER BY ${columnName}

可能有同学会说,这样是不是似乎也有注入风险,不按约定的字段,随便穿了一个过来,然后不就报错了,避免这个问题可以在Java中做判断,只有是约定的字段才会传给真正执行的SQL。

总结一下

井号占位符会自动给我们转义,根据类型来判断转不转义,如果是字符串类型,MyBatis会自动为我们将参数上加上引号。如果是数字类型,就不会加上。
美元占位符是原样替换,有SQL注入的风险,但有的时候在我们并不想MyBatis为我们拼上引号的时候,比如说根据前端传递的字段来进行排序,所以$占位符要慎用。

update、delete、insert、调用存储过程

数据变更语句 insert,update 和 delete 的实现非常接近:

<insert
  id="insertAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  keyProperty=""
  keyColumn=""
  useGeneratedKeys=""
  timeout="20">

<update
  id="updateAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

<delete
  id="deleteAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

parameterType类型和select标签的parameterType使用方式一样,我们主要讲statementType、useGeneratedKeys。
statementType: 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
JDBC基础不扎实的同学,可能会问Statement、PreparedStatement 、CallableStatement是啥?
简单的说,Statement接口提供了执行语句和获取结果的基本方法;PreparedStatement接口添加了处理输入参数的方法;
CallableStatement接口添加了调用存储过程核函数以及处理输出参数的方法。

useGeneratedKeys 使用示例

<!--
            useGeneratedKeys 开启接收主键
            keyProperty 将返回的主键放在对象的哪个属性上
 -->
<insert id="insertEntity"  useGeneratedKeys="true" keyProperty="id" >
     insert into Blog (name)
     values (#{name})
</insert>

java代码:

   private static void insertEntity() throws IOException {
        String resource = "mybatis-config.xml";
        // 读取配置文件
        InputStream inputStream = Resources.getResourceAsStream(resource);
        // 构建一个SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 开启一个会话
        SqlSession sqlSession = sqlSessionFactory.openSession();
        BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
        Blog blog = new Blog();
        blog.setName("aaa");
        blogMapper.insertEntity(blog);
        // 记得提交
        sqlSession.commit();
        // 关闭会话,不然新增不会成功
        sqlSession.close();
        System.out.println(blog);
    }

所以上面的getMapper方法,我们还要再改造一下,实际使用中也就是SSM(Spring Spring MVC MyBatis)整合之后,引入数据库连接池之后,连接使用完毕之后会还给连接池,这里我们就不改造了。
测试结果:

数据库结果:

数据库支持自增主键可以这么搞,不支持的就不可以这么搞了。

常用标签

上面我们在介绍参数类型是集合时,已经介绍了遍历标签,<foreach> , 但是有循环,怎么可以没有判断,switch呢。那怎么把逻辑判断移入xml中呢? 将判断变成一个一个标签吗? 那得记住多少标签啊? MyBatis的前身iBatis就这么做的,MyBatis做出的改变就是通过OGNL表达式配合if标签来完成逻辑判断。

OGNL表达式简介

OGNL(Object-Graph Navigation Language)是一种表达式语言(EL),简单来说就是一种简化了的Java属性的取值语言,在传统的Struts框架,以及MyBatis中都有大量使用,开源Java诊断利器Arthas也使用它做表达式过滤,简单但不失灵活的设计衍生出来了好多高阶玩法。

我们这里简单的介绍一下常用的,详情可以参看OGNL官方文档: http://commons.apache.org/pro...

OGNL生来就是为了简化Java属性的取值,比如想根据名称name引用当前上下文环境中的对象,则直接键入即可,如果想要引用当前上下文环境中对象text的属性title,则键入text.title即可。如果调用对象的方法,直接通过对象.方法名()即可。
访问数组,直接通过数组名[索引]访问即可。

判断参数不为null 和 集合size大于0

 <select id="selectBlogByList" resultType="org.example.mvc.entity.Blog">
        select * from Blog where id in
        <if test = "list != null and list.size() > 0">
            <foreach collection="list" item="item" open="(" separator="," close=")">
                #{item}
            </foreach>
        </if>
    </select>

参数值等于某个字符串

   <select id="selectByMark" resultType="org.example.mvc.entity.Blog">
             select * from Blog where id = #{id}
             <if test = ' "张三".equals(name) '>
                 and name =  #{name}
             </if>
    </select>

choose、when、otherwise

有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。为了丰富我们的适用场景,我们又向Blog表里加了两个字段:title和status,以前只有id,name。
示例:

   <select id="testChoose" resultType="org.example.mvc.entity.Blog">
         select * from Blog  where status = 'active'
        <choose>
            <when test="title != null">
                AND title like #{title}
            </when>
            <when test=" name != null ">
                AND name like #{name}
            </when>
            <otherwise>
                AND status != 'active'
            </otherwise>
        </choose>
    </select>

如果title不为空,就搜索title,如果name不为空, 就模糊搜索name。如果这两个都是空,就去搜索status 不是active的。

where、trim、set

前面的示例我们已经解决了拼接SQL的问题,下面我们来看下面一个SQL:

  select * from Blog where id in
        <if test = "list != null and list.size() > 0">
            <foreach collection="list" item="item" open="(" separator="," close=")">
                #{item}
            </foreach>
        </if>

如果条件不成立,SQL就会变成这样:

select * from Blog where id in

很明显这会报错,那为了避免这种情况,我们是不是要在where 后面拼接一个 1 = 1呢? 然后SQL就变成了下面这样:

  select * from Blog where  1 = 1
        <if test = "list != null and list.size() > 0">
         id in
            <foreach collection="list" item="item" open="(" separator="," close=")">
                #{item}
            </foreach>
        </if>

但是这相当不优雅,MyBatis提供了<where>标签,where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。所以我们的SQL可以就变成了这样:

   <select id="selectBlogByList" resultType="org.example.mvc.entity.Blog">
        select * from Blog 
        <where>    
            <if test = "list != null and list.size() > 0">
                id in
                <foreach collection="list" item="item" open="(" separator="," close=")">
                    #{item}
                </foreach>
            </if>
    </where>
    </select>

如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。
用于动态更新语句的类似解决方案叫做 set。set 元素可以用于动态包含需要更新的列,忽略其它不更新的列。比如:

<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。
来看看与 set 元素等价的自定义 trim 元素吧:

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

注意,我们覆盖了后缀值设置,并且自定义了前缀值。

bind标签

bind 元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文:

<select id="selectByMark" resultType="org.example.mvc.entity.Blog">
        select * from Blog where id = #{id}
         <if test = ' "张三".equals(name) '>
             <bind name="pattern" value="'%' + name + '%'" />
             and name =  #{pattern}
         </if>
</select>

别名

还记得我们在配置文件中配置的mappers吗? 我们其实也可以批量配置 ,批量关联:

<mappers>
        <!--批量关联-->
        <package name = "org.example.mybatis"/>
 </mappers>

我们的resultType写的是全类名,那不能不写那么多呢? 可以,在配置文件中这样配置就可以了:

  <typeAliases>
        <package name="org.example.mvc.entity"/>
    </typeAliases>


注意这个顺序,只能按properties、settings、typeAliases这个顺序来。
然后我们写resultType中的值写类名就可以了。

类型转换器

现在我们来关注一下,MyBatis实现ORM的一组核心类,类型转换器。我们上面讲MyBatis将表的记录变成Java中的对象,那数据类型是怎么对的上的呢? 就是通过类型转换器:

可以重写已有的类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型, 具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可选地)将它映射到一个 JDBC 类型。
直接实现TypeHandler接口还是有点麻烦的,所以我们这里介绍的是继承BaseTypeHandler来体会MyBatis中类型转换器的强大功能。BaseTypeHandler概览:

转换器是双向的,所以就有从数据库到Java的,从Java到数据库的:
setNonNullParameter Java到数据库就是剩下的剩下的三个get方法,从名字上我们就可以推断出来,是从数据库到Java的。
这次我们定义的类型转换器就是将Java的Boolean转成数据库中的int类型。

public class MyTypeHandler extends BaseTypeHandler<Boolean> {
    // java-DB
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Boolean parameter, JdbcType jdbcType) throws SQLException {
        if (parameter) {
            ps.setInt(i, 1);
        } else {
            ps.setInt(i, 0);
        }
    }

    // DB-java
    @Override
    public Boolean getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getInt(columnName) == 1 ? true : false;
    }

    //  DB-java
    @Override
    public Boolean getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return rs.getInt(columnIndex) == 1 ? true : false;
    }

    //  java-Db
    @Override
    public Boolean getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getInt(columnIndex) == 1 ? true : false;
    }
}

然后在Blog实体和表中加入对应的字段,Blog为Boolean,Blog表为int ,这里不再做展示了。
然后我们在配置文件中注册使用这个类型转换器:

<typeHandlers>
  <package name="org.mybatis.example"/>
</typeHandlers>

测试代码:

private static void selectBlogByCollection() throws IOException {
    BlogMapper blogMapper = getMapper(BlogMapper.class);
    List<Long> idLongList = new ArrayList<>();
    idLongList.add(1L);
    System.out.println(blogMapper.selectBlogByList(idLongList));
}

数据库中的值:

测试结果:

转换成功。

总结一下

本篇主要讲的是MyBatis的基本使用,如何配置、传参、返回类型、占位符、常用标签、OGNL表达式。开篇讲的基本上都是使用频率很高的,这算是把MyBatis又重学了一遍,以前学MyBatis看视频,现在发现如果官方文档写的比较丰富的话,看官方文档是更好的选择。希望对会大家有所帮助

参考资料

查看原文

赞 0 收藏 0 评论 0

北冥有只鱼 发布了文章 · 2月28日

欢迎光临Spring时代(二) 上柱国AOP列传

这里其实讲的是使用如何在Spring Framework如何使用AOP。在看本篇之前,建议先看代理模式-AOP绪论代理模式-AOP绪论是本篇的基础理论,不看也行,本篇讲的也就是讲基本使用。

AOP 简论

AOP: Aspect oriented programming 面向切面编程,是OOP(面向对象编程)的扩展。
如果说OOP是解决面向过程中遇到的一些问题(比如代码复用性不强等)的话,那么AOP解决的就是通用代码重复编写的问题。
这么说可能有点抽象,我们向现实的场景拉一拉,我们的方法通常是一步一步执行的,不同的方法有不同的调用时机。

但总有一些方法似乎是在所有的代码流程里面都需要,比如检测是否登录,记录执行时间等。但是我们总不想做重复的事情,就是在每个代码中写一遍,我们的愿望是写一次,然后标定哪些代码在哪里需要执行呢? AOP解决的就是这类问题。那么这样做的话,我们首先就要标明哪里是需要重复执行的,也就是注入点,用AOP的术语就是切入点(pointcut), 第二个就是声明在切入点在满足哪些时机时注入,比如可以选择一个方法在执行时注入,在方法发生异常时注入。像Spring Proxy并不支持多种多样的连接点,百分之九十九的情况是在方法执行的时候注入。这个时间我们用术语称之为连接点(joinPoint)。第三个就是注入的时机,是在方法前还是方法后,还是方法前后都来一遍。这个概念我们一般称之为建议(advice)。

pointCut+advice可以描述一段代码在什么时机注入(jointPoint),注入到哪里(pointCut),而被注入的代码我们用切面(Aspect)这个概念来描述,这也就是面向切面编程 (Aspect oriented programmin)。

仔细想一下,上面讲的AOP中的建议,是不是跟动态代理中的在不修改原来类的基础上增强一个类有点像呢? 没错AOP是借助于动态代理来实现的,如果你不懂什么是动态代理,请参看这篇文章:

其实不懂动态代理,也能看懂本篇。本篇主要介绍的是AOP在Spring中是如何使用的,AOP是一种理念,Spring实现了AOP。只不过理解动态代理对Spring实现的AOP会理解更深而已。

准备工作

欢迎光临Spring时代(一) 上柱国IOC列传的依赖我们还要接着用,同时我们还要再补充以下依赖:

<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.2.8.RELEASE</version>
        </dependency>
<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
</dependency>

不会maven想下jar包的,在欢迎光临Spring时代(一) 上柱国IOC列传中已经介绍了下载jar包的方法,这里不再重复介绍了。现在Spring Framework的轻量级已经体现出来了,依赖很少,大致上只需要七个jar包就能帮助我们优雅的管理对象之间的依赖关系和AOP了。

AOP在Spring中的实现简介

Java中方法依附于类,而要使用Spring提供的AOP也就需要将需要增强的类加入到IOC容器中,而在欢迎光临Spring时代(一) 上柱国IOC列传我们已经介绍过了,IOC容器有两种形式,一种是基于配置文件的,一种是基于注解的。我们首先来介绍基于配置文件的。在Spring Framework中我们一般称建议为通知,在方法执行前执行,我们称之为前置通知,在方法执行后执行,我们称之为后置通知,在方法执行之前和在方法执行之后都执行我们称之为环绕通知,在方法发生异常的时候执行我们称之为异常通知。

在AOP中我们介绍了三个概念:

  • 切点(pointCut)描述在哪些方法上执行
  • 连接点(joinPoint) 描述了注入时机
  • 切面(Aspect) 描述的是被注入代码。

注意这三个概念,我们在用Spring Framework提供的AOP的时候常常会碰到。

如何让一个普通类中的方法称为一个切面呢? 那肯定要让Spring Framework辨识到这个方法跟别的方法不一样,Spring Framework提供了以下接口:

  • org.springframework.aop.MethodBeforeAdvice
前置通知 在方法执行之前执行
  • org.springframework.aop.AfterReturningAdvice
后置通知 在方法执行之后执行
  • org.aopalliance.intercept.MethodInterceptor
环绕通知 拦截目标方法的调用,即调用目标方法的整个过程,即可以做到方法执行之前执行、方法执行之后执行、方法发生了异常之后执行
  • org.springframework.aop.ThrowsAdvice
异常通知 在方法发生了异常之后执行
实现它们,并将它们纳入到IOC容器的管辖,再准备切点,连接点。我们就能做到AOP。

我们上面讲切点是说要增强哪些方法,那方法在Java中应该怎么描述呢? 我们用全类名+方法名? 这样写起来多麻烦啊!这也就是execution表达式出世的原因,简单点,再简单点

execution表达式简介

格式通常如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
  • modifiers-pattern?: 指定方法的修饰符(public private protected),支持通配符,该部分可以省略
  • ret-type-pattern: 方法的返回值类型,支持通配符,可使用*来拦截所有的返回类型
  • declaring-type-pattern?: 该方法所属的类,即全类名+方法名。也可以用*拦截所有方法
  • name-pattern: 指定要匹配的方法名,支持通配符,可以使用*通配符来匹配所有方法。
  • param-pattern: 指定方法声明中的形参列表,支持两个通配符,即 * 和..,其中*代表一个任意类型的参数,而..表零个或多个任意类型的参数。例如,() 匹配一个不接受任何参数的方法,而(..) 匹配一个接受任意数量参数的方法,( * )匹配了一个接受一个任何类型的参数的方法,(*,String)匹配了一个接受两个参数的方法,其中第一个参数是任意类型,第二个参数必须是String类型。
  • throws-pattern:指定方法声明抛出的异常,支持通配符,该部分可以省略。

基于配置文件的AOP

第一种配置文件的AOP方式

前置通知、后置通知、环绕通知

首先我们实现MethodBeforeAdvice:

public class LogBefore implements MethodBeforeAdvice{
    @Override
    public void before(Method method, Object[] args, Object o) throws Throwable {
        System.out.println("在方法之前执行");
    }
}

然后将LogBefore 加入到IOC容器中,方式有很多种,可参看欢迎光临Spring时代(一) 上柱国IOC列传,这里选取是在配置文件里配置:

<bean id = "logBefore" class="org.example.aop.LogBefore">
</bean>
 <bean id = "landlord" class = "org.example.aop.Landlord">
  </bean>

然后准备切点,并将切点和切面联系起来,如上文所说,Spring Framework并没有给我们提供多少连接点,我们选择的就是在方法执行这个时机,之前、之后,还是环绕。

<!-- 这个配置是开启AOP用的,记得一定要加-->
  <aop:aspectj-autoproxy/>
<aop:config>
        <aop:pointcut id = "pointCut" expression = "execution(public void org.example.aop.Landlord.rentHouse())"/>
        <aop:advisor advice-ref="logBefore" pointcut-ref="pointCut"></aop:advisor>
</aop:config>  

所以我们的切点就是位于org.example.aop包下的Landlord的 返回值类型为void的rentHouse方法。 <aop:advisor>用于关联切点和切面。advice-ref指向切面的id,pointcut-ref指向的是切点的id。
我们准备一下代码测试一下:

public interface IRentHouse {
    /**
     * 实现该接口的类将能够对外提供房子
     */
    void rentHouse();
}
public class Landlord implements IRentHouse {
    @Override
    public void rentHouse() {
        System.out.println(" 我向你提供房子..... ");
    }
}
 private static void testLogAop() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        Landlord landlord = applicationContext.getBean("landlord", Landlord.class);
        landlord.rentHouse();
    }

运行结果:
我放在配置文件中的不是org.example.aop.Landlord,为什么我现在取不出来了啊。因为你此时要增强Landlord中的rentHouse方法,恰巧你实现了接口,那我就用JDK的动态代理给你增强啊。你现再在把配置文件中的AOP配置去掉,就会发现能成功运行了,或者 我们现在换种接值方式,向容器中索取的是IRentHouse类型的Bean就不会出错了, 代码如下:

  private static void testLogAop() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        IRentHouse landlord = applicationContext.getBean("landlord", IRentHouse.class);
        landlord.rentHouse();
    }

这个例子是我特意准备的例子,让需要增强的类实现了一个接口,为的就是与代理模式-AOP绪论这篇文章形成映照,如果看过这篇文章的同学,就会知道动态代理还有另一种形式是基于Cglib库来做的,这种增强就是在运行时创建需要增强类的子类,所以此时你将上面的Landlord不实现IRentHouse接口,取Landlord类型的就可以了。这其中就有向上转型、向下转型的知识点。可以仔细体会一下。

后置通知和环绕通知基本上就是照葫芦画瓢而已,跟前置通知类似。后置通知我们实现AfterReturningAdvice接口,环绕通知我们实现MethodInterceptor接口:

public class LogAfter implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("这是后置通知...............");
    }
}

public class LogAround implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("这是环绕通知,在方法执行之前执行.........");
        Object result = null;
        try {
         //  invocation 控制目标方法的执行,注释这段,则目标方法不会执行。
         result = invocation.proceed();
        }catch (Exception ex){
            System.out.println("这是环绕通知,在方法执行之后执行.........,可以当做异常通知");
            // ex.printStackTrace();
        }
        System.out.println("这是环绕通知,在方法执行之后执行.........");
        return result;
    }
}

在配置文件中配置:

 <bean id = "logAfter" class="org.example.aop.LogAfter">
 </bean>
 <bean id = "logAround" class = "org.example.aop.LogAround">
 </bean>
<aop:config>
        <aop:pointcut id = "pointCut" expression = "execution(public void org.example.aop.Landlord.rentHouse())"/>
        <aop:advisor advice-ref="logAround" pointcut-ref="pointCut"></aop:advisor>
    </aop:config>
    <aop:config>
        <aop:pointcut id = "pointCut" expression = "execution(public void org.example.aop.Landlord.rentHouse())"/>
        <aop:advisor advice-ref="logException" pointcut-ref="pointCut"></aop:advisor>
    </aop:config>
    <aop:config>
        <aop:pointcut id = "pointCut" expression = "execution(public void org.example.aop.Landlord.rentHouse())"/>
        <aop:advisor advice-ref="logBefore" pointcut-ref="pointCut"></aop:advisor>
    </aop:config>
    <aop:config>
        <aop:pointcut id = "pointCut" expression = " execution(public void org.example.aop.Landlord.rentHouse())"/>
        <aop:advisor advice-ref="logAfter" pointcut-ref="pointCut"></aop:advisor>
    </aop:config>

测试代码:

private static void testLogAop() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        Landlord landlord = applicationContext.getBean("landlord", Landlord.class);
        landlord.rentHouse();
    }

运行结果:

异常通知

异常通知跟其他接口的不一样之处,就在于这是一个空接口:

那么在方法在执行过程中,发生了异常,Spring该怎么回调这个方法呢? 别着急我们先看注释:

问题解决了,我们照注释要求的来:

public class LogException implements ThrowsAdvice {
    public void afterThrowing(Method method, Object[] args, Object target, Exception ex){
        System.out.println(method.getName()+"方法发生了异常");
    }
}

然后再度测试,还是上面的测试代码,在目标代码中请做出一个Exception的子类,我做的是除零异常:

第二种配置文件的AOP方式

上面实现的各种通知都是基于接口的,如果你不想实现接口,Spring也能将一个普通的类变成通知类,写到这里有想到了动态代理,想必还是基于动态代理的第二种形式来做的。我们首先准备一个通知类:

@Component
public class LogSchema {
    public void before(JoinPoint joinPoint) {
        System.out.println("z方法调用执行之前执行");
    }
    public void invoke(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("z环绕通知的前置通知........");
        Object result = null;
        try {
            // 控制方法的使用
            result = joinPoint.proceed();
            System.out.println("z打印方法的返回值..........." + result);
        } catch (Exception e) {
            System.out.println("z环绕通知的异常通知........");
        }
        System.out.println("z环绕通知的后置通知........");
    }
    public void after(JoinPoint joinPoint, Object returningValue) {
        System.out.println("z方法的返回值是:" + returningValue);
        System.out.println("z方法调用执行之后执行");
    }
    public void afterThrowing(JoinPoint joinPoint, ArithmeticException ex) {
        System.out.println("z这是异常通知的通知");
    }
}

然后将这个类在配置文件变成通知类:

<aop:config>
        <aop:pointcut id="schema" expression="execution(public void org.example.aop.Landlord.rentHouse(int ))"/>
        <aop:aspect ref="logSchema">
            <aop:around method = "invoke" pointcut-ref="schema" arg-names="joinPoint"/>
            <aop:before method = "before" pointcut-ref="schema"  arg-names="joinPoint"/>
            <aop:after-returning method="after"  returning="returningValue" arg-names="joinPoint,returningValue" pointcut-ref="schema"/>
            <aop:after-throwing method="afterThrowing" pointcut-ref="schema" arg-names="joinPoint,ex" throwing="ex"/>
        </aop:aspect>
    </aop:config>

测试代码:

基于注解的AOP

接下来我们介绍基于注解形式的Spring AOP,关于注解其实也有几种不同的写法,主要区别在于取参数的方式不同。一种是在注解中绑定参数,一种是用JoinPoint对象获取参数。两种我们都介绍,在使用注解的方式开发AOP之前,我们首先要Spring对AOP的支持,如果你是配置文件请在配置文件中加上: <aop:aspectj-autoproxy/>,如果是配置类请在配置类上加上@EnableAspectJAutoProxy注解。不然Spring就认为你不想用AOP。

基于@Pointcut的写法

取目标方法参数的第一种形式

@Aspect
// 该注解将该类纳入IOC容器
@Component 
public class LogAspectAnnotation {

    /**
     * Pointcut注解用于定义切点,可以被其他方法引用。
     * 相当于配置文件的: <aop:pointcut id = "pointCut" expression = "execution(public void    &org.example.aop.Landlord.rentHouse())"/>
     * Pointcut有两个属性一个是value用于指定execution表达式,argNames用于匹配参数取参数。
     * 记得语法是跟在配置文件中不同的是写完execution表达式,用&&args写参数名,多的用逗号隔开,在argNames中写对应的参数名
     * 然后在切点的方法中也要写,用于绑定参数。
     * @param i
     * @return
     */
    @Pointcut(value = "execution(public void org.example.aop.Landlord.rentHouse(int)) && args(i)",argNames = "i")
    public int  pointCut(int i){
        return i;
    }
    @Before(value = "pointCut(i)",argNames = "i")
    public void before(int i){
        System.out.println("方法调用执行之前执行");
    }
}

取目标方法参数的第二种形式

@Pointcut(value = "execution(public void org.example.aop.Landlord.rentHouse(int))")
    public void  pointCut(){
    }
    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint){
        for (Object arg : joinPoint.getArgs()) {
            System.out.println(arg);
        }
        System.out.println("方法调用执行之前执行");
    }

JoinPoint 可以获取目标类的所有元数据,我们来大致看一下JoinPoint类:

其实还可以这么写

// value 就直接相当于切点
@Before(value = "execution(public void org.example.aop.Landlord.rentHouse(int))")
    public void before(JoinPoint joinPoint){
        // 这里是打印切点的参数
        for (Object arg : joinPoint.getArgs()) {
            System.out.println(arg);
        }
        System.out.println("方法调用执行之前执行");
    }

后置通知、环绕通知、异常通知

@Aspect
@Component
public class LogAspectAnnotation {

    /**
     * Pointcut注解用于定义切点,可以被其他方法引用。
     * 相当于配置文件的: <aop:pointcut id = "pointCut" expression = "execution(public void org.example.aop.Landlord.rentHouse())"/>
     * Pointcut有两个属性一个是value用于指定execution表达式,argNames用于匹配参数取参数。
     * 记得语法是跟在配置文件中不同的是写完execution表达式,用&&args写参数名,多的用逗号隔开,在argNames中写对应的参数名
     * 然后在切点的方法中也要写,用于绑定参数。
     *
     * @param
     * @return
     */
    @Pointcut(value = "execution(public void org.example.aop.Landlord.rentHouse(int))")
    public void pointCut() {
    }

    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("方法调用执行之前执行");
    }

    /**
     * 相对于前置通知,AfterReturning中多了一个属性就是returning,用于获取目标方法的返回值。
     * returning中的值要和后置通知的参数名保持一致
     *
     * @param joinPoint
     * @param returningValue
     */
    @AfterReturning(value = "execution(public void org.example.aop.Landlord.rentHouse(int))", returning = "returningValue")
    public void after(JoinPoint joinPoint, Object returningValue) {
        System.out.println("方法的返回值是:" + returningValue);
        System.out.println("方法调用执行之后执行");
    }

    /**
     * 环绕通知用ProceedingJoinPoint来控制方法的执行
     *
     * @param joinPoint
     * @throws Throwable
     */
    @Around("execution(public void org.example.aop.Landlord.rentHouse(int))")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知的前置通知........");
        Object result = null;
        try {
            // 控制方法的使用
            result = joinPoint.proceed();
            System.out.println("打印方法的返回值..........." + result);
        } catch (Exception e) {
            System.out.println("环绕通知的异常通知........");
        }
        System.out.println("环绕通知的后置通知........");
    }

    /**
     * throwing属性会将发生异常时的对象传递给方法的参数,所以throwing的属性值和参数名要保持一致
     * 发生了方法中的异常就会触发异常通知,当前方法就是ArithmeticException时触发
     *
     * @param joinPoint
     * @param ex
     */
    @AfterThrowing(value = "execution(public void org.example.aop.Landlord.rentHouse(int))", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, ArithmeticException ex) {
        System.out.println("这是异常通知的通知");
        ex.printStackTrace();
    }
}

还是上面的测试代码,我在Landlord的rentHouse方法中做了一个1除以0操作,我们来看一下测试结果:

总结一下

之前我学Spring Framework的时候就是看的颜群老师在B站发布的视频,这个教程也确实不错,当时学的时候心里还存在些疑问,比如AOP的实现,IOC究竟做了什么,打算以后有时间将这些疑问全部解决等。到现在这些问题算上之前的三篇博客大多都得以解决,这也算是重新梳理了一下自己对Spring Framework的认识,因为我还是希望自己的知识成系统一点,不是零零碎碎的。希望对大家学习Spring Framework有所帮助。

参考资料

查看原文

赞 0 收藏 0 评论 0

北冥有只鱼 发布了文章 · 2月27日

代理模式-AOP绪论

本篇可以理解为Spring AOP的铺垫,以前大概会用Spring框架提供的AOP,但是对AOP还不是很了解,于是打算系统的梳理一下代理模式和AOP。

简单的谈一下代理

软件世界是对现实世界抽象,所以软件世界的某些概念也不是凭空产生,也是从现实世界中脱胎而来,就像多态这个概念一样,Java官方出的指导书 《The Java™ Tutorials》在讲多态的时候,首先提到多态首先是一个生物学的概念。那么设计模式中的代理模式,我们也可以认为是从现实世界中抽象而来,在现实世界代理这个词也是很常见的,比如房产中介代理房东的房子,经纪人代理明星谈商演。基本上代理人都在一定范围内代理被代表人的事务。

那我们为什么需要代理模式呢? 我们事实上也可以从现实世界去寻找答案,为什么房东要找房产中介呢? 因为房东也是人,也有自己的生活,不可能将全身心都放置在将自己的房屋出租上,不愿意做改变。那么软件世界的代理模式,我认为也是有着同样的原因,原来的某个对象需要再度承接一个功能,但是我们并不愿意修改对象附属的类,原因可能有很多,也许旧的对象已经对外使用,一旦修改也许会有其他意想不到的反应,也许我们没办法修改等等。原因有很多,同时对一个过去的类修改也不符合开闭原则,对扩展开放,对修改封闭。于是我们就希望在不修改目标对象的功能前提下,对目标的功能进行扩展。

网上的大多数博客在讲代理模式的时候,大多都会从一个接口入手,然后需求是扩展实现对该接口实现类的增强,然后写代码演示。像是下面这样:

public interface IRentHouse {
    /**
     * 实现该接口的类将能够对外提供房子
     */
    void rentHouse();
}
public class Landlord implements IRentHouse {
    @Override
    public void rentHouse() {
        System.out.println(" 我向你提供房子..... ");
    }
}
public class HouseAgent implements IRentHouse {

    @Override
    public void rentHouse() {
        System.out.println("在该类的方法之前执行该方法");
        IRentHouse landlord = new Landlord();
        landlord.rentHouse();
        System.out.println("在该类的方法之后执行该方法");
    }
}
public class Test {
    public static void main(String[] args) {
        IRentHouse landlord = new HouseAgent();
        landlord.rentHouse();
    }
}

测试结果:

许多博客在刚讲静态代理的时候通常会从这里入手,在不改变原来类同时,对原来的类进行加强。对,这算是一个相当现实的需求,尤其是在原来的类在系统中使用的比较多的情况下,且运行比较稳定,一旦改动就算是再小心翼翼,也无法保证对原来的系统一点影响都没有,最好的方法是不改,那如何增强原有的目标对象,你新增强的一般就是要满足新的调用者的需求,那我就新增一个类吧,供你调用。很完美,那问题又来了,为什么各个博客都是从接口出发呢?

因为代理类和目标类实现相同接口,是为了尽可能的保证代理对象的内部结构和目标对象保持一致,这样我们对代理对象的操作都可以转移到目标对象身上,我们只用着眼于增强代码的编写。

从面向对象的设计角度来看,如果我这个时候我不放置一个顶层的接口,像上面我将接口中的方法移动至类中,不再有接口。那你这个时候增强又改该怎么做呢?这就要聊聊类、接口、抽象类的区别了,这也是常见的面试题,在学对象的时候,我们常常和过去的面向过程进行比较,强调的一句话是类拥有属性和行为,但在面向过程系语言本身是不提供这种特性的。在学习Java的时候,在学完类,接着就是抽象类和接口,我们可以说接口强调行为,是一种契约,我们进行面向对象设计的时候,将多个类行为抽象出来,放到接口中,这样扩展性更强,该怎么理解这个扩展性更强呢?就像上面那样,面向接口编程。

假设我们顽固的不进行抽象,许多类中都放置了相同的方法,那么在使用的时候就很难对旧有的类进行扩展,进行升级,我们不得不改动旧有的类。那抽象类呢? 该怎么理解抽象类呢? 如果接口是对许多类行为的抽象,那么抽象类就是对这一类对象行为的抽象,抽象的层次是不一样的。就像是乳制品企业大家都要实现一个标准,但是怎么实现的国家并不管。抽象类抽象的是鸟、蜂鸟、老鹰。这一个体系的类的共性,比如都会飞。

其实到这里,静态代理基本上就讲完了,代理模式着眼于在不改变旧的类的基础上进行增强,那么增强通常说的就是方法,行为增强,属性是增加。那么为了扩展性强,我们设计的时候可以将行为放置在接口中或者你放在抽象类里也行,这样我们就可以无缝增强。关于设计模式,我觉得我们不要被拘束住,我觉得设计模式是一种理念,践行的方式不一样而已。

静态代理着眼于增强一个类的功能,那么当我们的系统中有很多类都需要增强的时候,就有点不适合了,假设有三十个类都需要增强,且设计都比较好,都将行为抽象放置到了接口中,这种情况下,你总不能写三十个静态代理类吧。当然不能让我们自己写,我们让JVM帮我们写。这也就是动态代理。

动态代理

换个角度看创建对象的过程

对于Java程序员来说,一个对象的创建过程可能是这样的:

我们在思考下,将面向对象的这种思想贯彻到底,思考一下,作为Java程序员我们使用类来对现实世界的事物进行建模,那么类的行为是否也应该建模呢?也就是描述类的类,也就是位于JDK中的类: java.lang.Class。每个类仅会有一个Class对象,从这个角度来看,Java中类的关系结构如下图所示:

所以假如我想创建一个对象,JVM的确会将该类的字节码加载进入JVM中,那么在该对象创建之前,该对象的Class对象会先行创建,所以对象创建过程就变成了下面这样:

在创建任何类的对象之前,JVM会首先创建给类对应的Class对象,每个类仅对应一个,如果已经创建则不再创建。然后在创建该类的时候,用于获取该类的元信息。

基于接口的代理

我们这里再度强调一下我们的目标:

  • 我们有一批类,然后我们想在不改变它们的基础之上,增强它们, 我们还希望只着眼于编写增强目标对象代码的编写。
  • 我们还希望由程序来编写这些类,而不是由程序员来编写,因为太多了。

第一个目标我们可以让目标对象和代理对象实现共同的接口,这样我们就能只着眼于编写目标对象代码的编写。

那第二个目标该如何实现呢? 我们知道接口是无法实例化的,我们上面讲了目标对象有一个Class类对象,拥有该类对象的构造方法,字段等信息。我们通过Class类对象就可以代理目标类,那关于增强代码的编写,JDK提供了java.lang.reflect.InvocationHandler(接口)和 java.lang.reflect.Proxy类帮助我们在运行时产生接口的实现类。

我们再回想一下我们的需求,不想代理类,让JVM写。那么怎么让JVM知道你要代理哪个类呢?一般的设计思维就是首先你要告知代理类和目标类需要共同实现的接口,你要告知要代理的目标类是哪一个类由哪一个类加载器加载。这也就是Proxy类的:
getProxyClass方法,我们先大致看一下这个方法:

public static Class<?> getProxyClass(ClassLoader loader,  Class<?>... interfaces)throws IllegalArgumentException

该方法就是按照上述的思想设计的,第一个参数为目标类的类加载器,第二个参数为代理类和目标类共同实现的接口。
那增强呢? 说好的增强呢? 这就跟上面的InvocationHandler接口有关系了,通过getProxyClass获取代理类,这是JDK为我们创建的代理类,但是它没有本体(或者JDK在为我们创建完本地就把这个类删除掉了),只能通过Class类对象,通过反射接口来间接的创建对象。
所以上面的静态代理如果改造成静态代理的话,就可以这么改造:

 private static void dynamicProxy() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        /**
         *  第一个参数为目标类的加载器
         *  第二个参数为目标类和代理类需要实现的接口
         */
        Class<?> rentHouseProxy = Proxy.getProxyClass(IRentHouse.class.getClassLoader(), IRentHouse.class);
        //这种由JDK动态代理的类,会有一个参数类型为InvocationHandler的构造函数。我们通过反射来获取
        Constructor<?> constructor = rentHouseProxy.getConstructor(InvocationHandler.class);
        // 通过反射创建对象,向其传入InvocationHandler对象,目标类和代理类共同实现的接口中的方法被调用时,会先调用                               
        // InvocationHandler的invoke方法有目标对象需要增强的方法。为目标对象需要增强的方法调用所需要的的参数
        IRentHouse iRentHouseProxy = (IRentHouse) constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                IRentHouse iRentHouse = new Landlord();
                System.out.println("方法调用之前.............");
                Object result = method.invoke(iRentHouse, args);
                System.out.println("方法调用之后.............");
                return result;
            }
        });
        iRentHouseProxy.rentHouse();
    }   

上面这种写法还要我们在调用的时候显式的new一下我们想要增强的类,属于硬编码,不具备通用性,假设我想动态代理另一个类,那我还得再写一个吗? 事实上我还可以这么写:

   private static Object getProxy(final Object target) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        Class<?> proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
        Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class);
        Object proxy = constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法开始执行..........");
                Object object = method.invoke(target, args);
                System.out.println(method.getName() + "方法执行结束..........");
                return object;
            }
        });
        return proxy;
    }

这样我们就将目标对象,传递进来了,通用性更强。事实上还可以这么写:

private static Object getProxyPlus(final Object target) {
        Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("方法开始执行..........");
                Object obj = method.invoke(target, args);
                System.out.println("方法执行结束..........");
                return obj;
            }
        });
        return proxy;
    }

CGLIB代理简介

然后我想增强一个类,这个类恰巧没有实现接口怎么办? 这就需要Cglib了,其实道理倒是类似的,你有接口,我就给你创建实现类,你没接口还要增强,我就给你动态的创建子类。通过“继承”可以继承父类所有的公开方法,然后可以重写这些方法,在重写时对这些方法增强,这就是cglib的思想。

Cglib代理一个类的通常思路是这样的,首先实现MethodInterceptor接口,MethodInterceptor接口简介:

我们可以这么实现:

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("方法执行前执行");
        System.out.println(args);
        Object returnValue = methodProxy.invokeSuper(obj, args);
        System.out.println("方法执行后执行");
        return returnValue;
    }
}

注意调用在intercept方法中调用代理类的方法,会再度回到intercept方法中,造成死循环。intercept就可以认为是代理类的增强方法,自己调用自己会导致递归,所以搜门上面的调用是用methodProxy调用继承的父类的函数,这就属于代理类。
测试代码:

private static void cglibDemo() {
        // 一般都是从Enhancer入手
        Enhancer enhancer = new Enhancer();
        // 设置需要增强的类。
        enhancer.setSuperclass(Car.class);
        // 设置增强类的实际增强者
        enhancer.setCallback(new MyMethodInterceptor());
        // 创建实际的代理类
        Car car = (Car) enhancer.create();
        System.out.println(car.getBrand());
    }

这种增强是对类所有的公有方法进行增强。这里关于Cglib的介绍就到这里,在学习Cglib的动态代理的时候也查了网上的一些资料,怎么说呢? 总是不那么尽如人意,总是存在这样那样的缺憾。想想还是自己做翻译吧,但是Cglib的文档又稍微有些庞大,想想还是不放在这里吧,希望各位同学注意体会思想就好。

总结一下

不管是动态代理还是静态代理都是着眼于在不修改原来类的基础上进行增强,静态代理是我们手动的编写目标类的增强类,这种代理在我们的代理类有很多的时候就有些不适用了,我们并不想为每一个需要增强的累都加一个代理类。这也就是需要动态代理的时候,让JVM帮我们创建代理类。创建的代理类也有两种形式,一种是基于接口的(JDK官方提供的),另一种是基于类的(Cglib来完成)。基于接口的是在运行时创建接口的实现类,基于类是在运行时创建需要增强类的子类。

参考资料

查看原文

赞 0 收藏 0 评论 0

北冥有只鱼 发布了文章 · 2月15日

欢迎光临Spring时代(一) 上柱国IOC列传

本篇我们就开始学习Java领域大名鼎鼎的Spring Framework,本篇介绍的Spring Framework中的IOC(控制反转,也称依赖注入),本来想用初遇篇,这个题目的,但是一想这个题目不太见名知义,就改成上柱国列传了。为什么叫上柱国IOC列传呢,因为觉得上柱国这个名字挺好听。
上柱国: 原义为自春秋起为军事武装的高级统帅,引申义为功勋的荣誉称号,战国时楚、赵置,位令尹、相国下,甚尊。原为保卫国都之官

缘起

2002年 Rod Johnson在2002年编著的《Expert One-On-One J2EE Design And Development》一书中对,对Java EE正统框架(EJB)臃肿、低效、脱离现实的学院派提出了质疑,然后以该书为指导思想,编写了interface21框架。然后在interface21框架的基础上,经过重新设计,于2004年发布。到现在开始已经十八年左右了。

开发者: Spring Framework 请回答

《欢迎光临Spring时代-绪论》中我们提出了几个关于如何取对象的问题,这里我们再回忆一下:

  • 对象之间有复杂的依赖关系的时候,在不希望是硬编码的情况下,如何取对象才能做到优雅和可配置化?
  • 知道如何创建对象,但是无法把握创建对象的时机。我现在就希望把如何创建对象代码交给"负责什么时候创建的代码"。后者在对应的时机,就调用对应的创建对象函数。
  • 虽然Java不用操心内存回收问题,但是我还是希望能节省资源一下,在控制器这一层,不希望处理的每个请求都new一下对应的service实现类,我希望能将该Service做成单例模式,能否在做成单例的同时又做到让取对象优雅呢?

Spring Framework: IOC容器就是答案

在我刚学Spring框架的时候,不少视频都会说,我们并不在new对象,而是将对象放进IOC容器中,你要取的时候,向IOC容器中取即可。这里我们先从实例入手再来解释IOC。结合着例子来解释IOC会更加易懂。

准备工作

本篇我们还基于maven来做示例,基本上开发Spring Framework的程序基本上就只需要五个jar包就够了,分别是下面这五个:

  • spring-context
  • spring-core
  • spring-aop
  • spring-beans
  • spring-expression

这次我们选的版本都是5.2.8.RELEASE。
如果你不会用maven,然后请参考我的这篇博客:

如果暂时还不想学maven,还想做jar包下载。那么有两种形式可以下载到对应的jar包:

  • Spring Framework 官方maven库

浏览器访问这个网址: https://maven.springframework...

下载完粘贴到对应的lib目录下即可。

  • 去maven的中央仓库下载
输入上面的jar包名字,粘贴对应的依赖即可。
开发工具我这里用的是IDEA,提示比较强大,用着比较顺手。
如果你不习惯用IDEA,是Eclipse党,这边推荐你下载Spring官方出品的开发工具STS,也是在Eclipse的基础开发的,开发Spring框架的程序更加快速。
浏览器输入: https://spring.io/tools/

本文的示例都是基于IDEA来做的,如果不会用IDEA,想用STS,可以参考文末后面的参考资料,是视频。

第一个Spring Framework程序


一般Spring框架的这个配置文件,我们都命名为applicationContext.xml,这是一种大致的约定。
上面的问题是如何优雅的取对象,在取之前你首先就得存,向applicationContext.xml放对象。怎么放?
像下面这样放:

我建了一个Student类,然后里面有name和age属性,无参和有参构造函数,get和set函数,重写了toString方法。
bean里的id是唯一的,class是Student类的全类名(包名+类名)。
如何取:

public class SpringDemo {
    public static void main(String[] args) {
        // 加载配置文件
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        Student student = (Student) applicationContext.getBean("student");
        System.out.println(student);
    }
}

打印结果:

这就叫DI和IOC

我们直接new,在Spring的配置文件配置参数,然后间接取对象的方式, 我们称之为控制反转(IOC Inversion of Control)或依赖注入(DI Dependency Injection)。首先解释一下为什么有两个称呼,刚开始是只有控制反转,后来发现控制反转这个概念有点难以理解,就在一次大会上讲控制反转改为依赖注入。
那什么是控制反转,我们可以认为new 对象是一种控制权,然后控制反转就是我们将new对象的控制权交给Spring Framework。依赖注入呢? 其实也是一样,我们可以理解为在Spring的配置文件配置完对象之后,Spring将该对象给需要该对象的对象,此时就回答了上面我们提出的第二个问题:

对象之间有复杂的依赖关系的时候,在不希望是硬编码的情况下,如何取对象才能做到优雅和可配置化?
如果对象之间有复杂的依赖关系,那么就请将这种复杂的依赖关系当做配置参数一样,放在Spring的配置文件中,然后再从Spring IOC容器中取值,我们此时姑且就可以将配置文件当做IOC容器,放置了对象。

对象之间有依赖关系该如何配置呢?

用上面的配置只能解决简单的值,那如果某个对象的某个属性也是对象类型的呢? Spring也想到了,当对象的属性是属性用ref注入,像下面这样:

<bean id = "studentCard" class = "org.example.StudentCard">
        <property name = "id" value="1"></property>
        <property name = "cardNo" value="code01"></property>
     </bean>

    <bean id="student" class="org.example.Student">
        <property name = "name" value = "zs"></property>
        <property name = "age" value = "23"></property>
        <property name = "studentCard" ref = "studentCard"></property>
    </bean>

普通属性用value,那么引用类型就用ref,ref的值是配置文件中bean标签的id属性,所以在applicationContext.xml中id禁止重复。

通过注解将对象放入IOC容器中

我们上面的第三个问题,我们Dao层的对象在对应的Service只需要一份就可以了,Spring容器中的对象默认都是单例的。
那Dao层有的时候都没有属性,我们还要写在配置文件中吗?Spring也想到了,提供了下面几个注解:

@Controller 对应控制层
@Service 对应服务层
@Component 通用注解,如果你不确定这个对象属于那一层的话,就用这个。
@Repository 对应dao层

然后在配置文件中加入:

 <!--配置扫描器,base-package放需要扫描的包,指定了包之后,Spring会扫描。
    该包下面的类,如果有以上四个注解,那么就会将对应的类加入到容器中,id默认为类名首字母转小写。
    多个包也可以写,用逗号隔开即可。如果写是一个包下面有多个包,
    比如org.example.service,org.example.dao。写到二级包:org.example。Spring会自动扫描所有的子包。
 -->
    <context:component-scan base-package="org.example"/>



所以我们上面的第三个问题就得到了回答,我们写Service层需要对应的dao层对应的时候就可以这么写:

@Service
public class StudentService {
    // 假装Student是dao层,被打上@Autowired的属性,Spring在扫描的时候会自动去容器去寻找对应的类型
    // 然后给该属性注入值,所以如果你有两个IOC容器中有两个Student对象,那么可能就会报错
    // Spring官方并不推荐如此注入
    @Autowired
    private Student student;
    
    public void print(){
        System.out.println(student);
    }
}

官方推荐的注入方式是 @Autowired出现在set方法或构造方法上:

@Service
public class StudentService {
    private Student student;
    @Autowired
    public StudentService(Student student) {
        this.student = student;
    }
    public void print(){
        System.out.println(student);
    }
}

至此我们上面提出的第一个问题和第三个问题得到了解决:

  • 对象有复杂的依赖关系,我们在配置文件中佩,在调用的类中,用Autowired自动注入,这很优雅。
  • 对应的Serveice层通过注解就能将dao层注入,就不用再每个业务方法中,重复new了。

然后有不懂SpringMVC框架的同学这里可能就会问了,那我在Servlet中该如何取IOC容器中的对象啊,Servlet的初始化又不像main函数,有个明确的入口,用户是可以从任意一个网页进入的。对于这个问题可以参看:

Spring视频教程的P11、P12、P13、P14。

接着我们回答第二个问题,难以把握对象的创建时机的这个问题,对于这个问题,Spring框架的答案是条件注解。
IOC容器有两种形式,一种是基于配置文件(我们上面用的就是),一种是基于注解。条件注解是基于注解形式的,查了一些资料还是没找到如何用配置文件实现条件注解的。但是基于配置文件的IOC还需要再补充一些,所以下面是先将配置文件形式的讲解完毕后,才会讲基于注解的,条件注解也是基于注解。

基于XML的依赖注入

不同的注入方式

我们知道创建一个对象是有几种不同的方式的:

  1. 通过无参构造函数,然后通过set函数设置值
  2. 通过有参构造函数
  3. 通过反射
  4. 通过序列化

同样的在配置文件中配置对象参数的也有几种形式,上面的property的配置的形式就是通过第一种方式来创建对象的。有兴致的同学可以测试下。
接下来我们介绍的就是通过第二种方式将对象放入IOC容器中:

<bean id = "studentCard" class = "org.example.StudentCard">
    <constructor-arg value="1" index = "0"/>
    <constructor-arg value="11" index = "1"/>
 </bean>

constructor-arg有四个属性:

  • value (具体的值,不加index的话,具体的值和构造函数要求的参数类型要保持一致,默认情况下标签的先后顺序和构造函数保持一致)
  • index 用于指定给构造函数的第几个参数
  • type 指定参数类型
  • name 用于指定参数名
  • ref 这个讲过是指引用类型

通过反射产生对象:

 <bean id = "studentCard" class = "org.example.StudentCard" p:id="1" p:cardNo="23">
 </bean>


引用类型通过p:属性名-ref来设定IOC容器中bean的ID

将集合放入对应的IOC容器中

首先我们准备一个集合的类,构造函数省略,get和set方法省略:

public class CollectionDemo {
    private List<String> list;
    private String[] arrayString;
    private Set<String> set;
    private Map<String, Object> map;
    private Properties properties;
}

Spring配置文件:

 <bean id = "collectionDemo"  class = "org.example.CollectionDemo">
            <property name="list">
                <value>
                    14
                </value>
            </property>
            <property name="arrayString">
                <array>
                    <value>ddd</value>
                </array>
            </property>
            <property name = "set">
                <set>
                    <value>aaa</value>
                </set>
            </property>
            <property name="map">
                <map>
                    <entry>
                        <key>
                            <value>zs</value>
                        </key>
                         <value>zs</value>
                    </entry>
                </map>
            </property>
            <property name = "properties">
                <props>
                    <prop key="aa">bb</prop>
                    <prop key="cc">dd</prop>
                </props>
            </property>
    </bean>

示例:

public class SpringDemo {

    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        CollectionDemo collectionDemo = (CollectionDemo)applicationContext.getBean("collectionDemo");
        System.out.println(collectionDemo);
    }
}


刚学Spring框架的时候,视频上说控制反转,我们自己不再new对象,我以为就是真的不再new了,就在配置文件里面配就行了,后来随着编码量的上升,才发现这是对Spring框架的一种误解,是有复杂依赖关系的我们在配置文件里面配,像你要是想用个HashMap,就不必了。

特殊值的注入问题

我们在配置文件配置对象的时候,用的值都和XML预定义的符号值不冲突,什么意思呢? 假设我给对象的值是<,就会报错。

我们上面配置Student对象的参数的时候,我们用的是这种:

<bean id="student" class="org.example.Student">
    <property name = "name" value = "zs"></property>
    <property name = "age" value = "23"></property>
    <property name = "studentCard" ref = "studentCard"></property>
</bean>

我们称之为value属性注入,其实还可以这么写:

<bean id="student" class="org.example.Student">
        <property name = "name" >
            <value type="java.lang.String">zs</value>
        </property>
        <property name = "age" value = "23"></property>
        <property name = "studentCard" ref = "studentCard"></property>
    </bean>

我们称之为value子标签注入。 两者的区别如下:

所以当我们配置的属性值是< 这个符号的时候我们就可以这么写:

<bean id="student" class="org.example.Student">
    <property name = "name" >
        <value type="java.lang.String">z&lt;s</value>
    </property>
</bean>

也可以这么写:

 <bean id="student" class="org.example.Student">
        <property name = "name" >
            <value type="java.lang.String"><![CDATA[z<3]]></value>
        </property>
    </bean>

那我要给属性的值注入null怎么办? 方法有两个

  • 不给值(property 标签都不写,写标签value里面啥都不写,如果是String的话,默认给的是空字符串)
  • 用null标签
<bean id="student" class="org.example.Student" autowire = "byName">
        <property name = "name" >
                <null/>
        </property>
  </bean>

各种类型的自动注入

上面我们提到在Spring的配置文件中配置对象的属性值的时候,如果属性值是对象类型的,那么用ref就可以了,其实这个也可以不写,用自动注入就可以了,用这种自动注入也有两种方式:

  1. byName Spring会自动的去寻找容器中id为对象属性类名首字母转小写的对象。
<bean id = "studentCard" class = "org.example.StudentCard">
        <property name="id" value="1"></property>
        <property name="cardNo" value="zs"></property>
     </bean>

    <bean id="student" class="org.example.Student" autowire = "byName">
        <property name = "name" >
            <value type="java.lang.String"><![CDATA[z<3]]></value>
        </property>
        <property name="age">
            <value>34</value>
        </property>
    </bean>

运行结果不再展示,假设你把第一个bean标签的id改为studentCard1,那么就注入不了。

  1. byType 按类型,自动去寻找匹配的类型,如果你有两个StudentCard类型,也是无法注入。
<bean id="student" class="org.example.Student" autowire = "byType">
        <property name = "name" >
            <value type="java.lang.String"><![CDATA[z<3]]></value>
        </property>
        <property name="age">
            <value>34</value>
        </property>
    </bean

基于注解的依赖注入

@Bean+方法

在xml里面限制还是挺多的,如果你不小心写错了属性名,那么也是到运行时才能发现错误,如果你不小心给错了类型值,也是到运行时才能发现错误,比如属性是数字类型,你给了一个字符串。很多时候我们都希望尽可能早的发现错误,那么我们的配置文件能不能变换一种形式呢? 用代码做配置文件怎么样呢? 好啊,很好的想法啊,那我们就用代码做配置文件吧。

@Configuration
public class SpringConfig {
     @Bean(name = "studentCard")
    public StudentCard studentCard(){
        return  new StudentCard(11,"22");
    }
    @Bean
    public Student student(@Qualifier("studentCard") StudentCard studentCard){
        return new Student(20,"zs",studentCard);
    }
    @Bean
    public StudentCard studentCard2(){
        return  new StudentCard(11,"22");
    }
}
private static void annotationTest() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        Student student = applicationContext.getBean("student", Student.class);
        System.out.println(student);
    }

运行结果:

  1. 问如何让一个类成为一个配置文件?
给一个类打上注解@Configuration
  1. 如何将bean标签移植到配置类中?
将原先的bean标签变成方法,方法返回对象类型即可。同时方法要加上@Bean注解。方法名默认为id。
  1. 那我想自定义id名,可以吗?
可以,在@bean标签中,用name属性自定义就好
  1. 那引用类型的属性注入怎么办?
像上面一样,在方法参数中写对应的参数即可。
也可以调用对应的方法来完成注入。
你可以自己new,但是为什么不直接用IOC容器的呢。
  1. 那我有两个对象,都属于一个类。 我想指定一个对象注入怎么办?
@Qualifier中指定对象名即可。

@import 、FactoryBean、ImportBeanDefinitionRegistrar、ImportSelector

@import(注解) 、FactoryBean(接口)、ImportBeanDefinitionRegistrar(接口)、ImportSelector(接口)是Spring提供的将对象加入IOC容器的另外方式。

  • @import
@Configuration
@ComponentScan(basePackages = "org.example")
// ImportTest是我建的一个空类,用来测试@import,value是一个数组
@Import(value = {ImportTest.class})
public class SpringConfig {
}

测试一下:

 private static void annotationPrintAllBean() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        String[] beanNames = applicationContext.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }

测试结果:

这种方式加进来的bean名是全类名。

  • ImportSelector简述

首先实现ImportSelector接口:

public class MyImportSelector implements ImportSelector {
    // 最后返回的即为需要加入到容器的类名
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{ImportTest.class.getName()};
    }
}

然后在配置类上引入:

@Configuration
@ComponentScan(basePackages = "org.example")
@Import(value = MyImportSelector.class)
public class SpringConfig {
}

测试代码:

 private static void annotationPrintAllBean() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        String[] beanNames = applicationContext.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }

测试结果:

AnnotationMetadata 中携带打上@import注解的配置类上的元信息。

  • ImportBeanDefinitionRegistrar概述

先实现接口:

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // BeanDefinitionRegistry可以将bean注册进IOC工厂
        // 我们需要准备一个BeanDefinition。
        BeanDefinition beanDefinition = new RootBeanDefinition(ImportTest.class);
        registry.registerBeanDefinition("importTest",beanDefinition);
    }
}

配置类引入:

@Configuration
@ComponentScan(basePackages = "org.example")
@Import(value = MyImportBeanDefinitionRegistrar.class)
public class SpringConfig {
}

测试代码:

private static void annotationPrintAllBean() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        String[] beanNames = applicationContext.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }

测试结果:

  • FactoryBean

FactoryBean是一个接口,还有一个接口叫BeanFactory。
反射是框架的灵魂,有的时候,某个bean重复性的属性太多,在配置文件里面配置也是一件让人烦心的事情,但是程序不是擅长做重复工作吗? 我们能否少写点,从配置文件中读,然后用程序来做这种重复性工作呢?这也就是FactoryBean接口做的事情。

@Component
public class MyFactory implements FactoryBean<Car> {

    private String cardInfo;

    public MyFactory() {
        // 假装从Spring的配置文件中读到了值。
        this.cardInfo = "brand,100,200.12";;
    }

    /**
     * 向IOC容器中放入对象
     * @return
     * @throws Exception
     */
    @Override
    public Car getObject() throws Exception {
        Car car = new Car();
        String[] cardInfoArray = cardInfo.split(",");
        car.setBrand(cardInfoArray[0]);
        car.setMaxSpeed(Integer.parseInt(cardInfoArray[1]));
        car.setPrice(Double.parseDouble(cardInfoArray[2]));
        return car;
    }

    /**
     * 向IOC容器返回指定的类型
     * @return
     */
    @Override
    public Class<?> getObjectType() {
        return Car.class;
    }

    /**
     * 设置是否是单例模式
     * @return
     */
    @Override
    public boolean isSingleton() {
        return false;
    }
}

测试代码:

  private static void testFactoryBean() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        // 代表取MyFactory
        MyFactory myFactory = applicationContext.getBean("&myFactory", MyFactory.class);
        // 不加&代表取工厂中放入的bean
        Car car = applicationContext.getBean("myFactory", Car.class);
        System.out.println(myFactory);
        System.out.println(car);
    }

测试结果:

扫描包移植-@ComponentScan

排除某些类

在配置类上加上:

@ComponentScan(basePackages = "org.example", excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = {Service.class})})
  • basePackages: 默认扫描本包及其子包。
  • excludeFilters : 排除的类,要求两个属性,第一个是type,可以理解为策略。

是一个枚举类型,值都在FilterType中,一共有六种:

  • ANNOTATION 过滤打上指定注解(@Controller,@Component,@Repository,@Service)上的类。
  • ASSIGNABLE_TYPE: 指定的类型,已经写在配置类中的,比如说Student类,无法排除。默认应当是先加载配置文件中的类,然后在根据扫描的包,扫描类,去将要加入到IOC容器的对象,加入到IOC容器中。
  • ASPECTJ 按照Aspectj的表达式
  • REGEX 按照正则表达式
  • CUSTOM 自定义规则

我建了一个类,打上了Service注解,现在我们来测试下:

@Configuration
@ComponentScan(basePackages = "org.example", excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = {Service.class})})
// @ComponentScan(basePackages = "org.example", excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,value = {StudentDao.class})})
// @ComponentScan(basePackages = "org.example", includeFilters= {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,value = {StudentDao.class})},useDefaultFilters = false)
// @ComponentScan(basePackages = "org.example", includeFilters = {@ComponentScan.Filter(type = FilterType.CUSTOM,value = {MyFilter.class})},useDefaultFilters = false)
public class SpringConfig {
    @Bean(name = "studentCard")
    public StudentCard studentCard(){
        return  new StudentCard(11,"22");
    }
    @Bean
    public Student student(@Qualifier("studentCard") StudentCard studentCard){
        return new Student(20,"zs",studentCard);
    }
    @Bean
    public StudentCard studentCard2(){
        return  new StudentCard(11,"22");
    }
}
private static void annotationTest() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        // 获取IOC容器中所有的bean
        String[] beanNameArray = applicationContext.getBeanDefinitionNames();
        for (String beanDefinitionName : beanNameArray) {
            System.out.println(beanDefinitionName);
        }
    }

测试结果:

没有service了。
下面解除第二个@ComponentScan的注释,将第一个@ComponentScan的注释解除掉。再度测试:

会发现StudentDao没了。

只包含某些类

与排除指定的类是类似的,只不过Spring默认会加载子包上需要加入到IOC容器中的类,也就是说你想只包含的类在basePackages下面,那么这个包含就是无效的。所以我们需要通过useDefaultFilters来禁止Spring的默认行为。
我们注释掉其他的@ComponentScan,只让第三个@ComponentScan解除注释。测试一下:

会发现打上@Service类的对象没了。

自定义规则排除或包含某些类

自定义规则要实现TypeFilter,像下面这样:

public class MyFilter implements TypeFilter {
    // 返回true加入到IOC容器中
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        // 获取扫描的元数据类型
        ClassMetadata classMetadata = metadataReader.getClassMetadata();
        // 每扫到子包下的一个类,这个方法就会被调用。
        String className = classMetadata.getClassName();
        // 只要类名中包含Student的,我就加入到容器中
        if (className.contains("Student")){
            return true;
        }else{
            return false;
        }
    }
}

还是上面的配置类, 我们解除第四个@ComponentScan的注释,其他的全部注释。测试一下。

HomeStudentCondition是我自己建的类,没加我们介绍的Spring提供的注解,也加进来了。

条件注解

条件注解可以让某些对象在某些条件满足的情况下,才加入到IOC容器中(等价于创建该对象),如果该条件不满足则该对象不创建,也就是不加入到对应的IOC容器中。那条件该怎么告诉Spring框架呢? 也是通过一个类,这个类要求实现Condition接口。
顺带提一下Spring Boot很大程度上也依赖于条件注解。
首先两个bean:

public class HomeStudent extends Student {
    /**
     * 出入证 无参和构造函数 get set方法不再列出
     */
    private String pass;    
}
public class BoardStudent extends Student {
    /**
     * 宿舍号 无参和构造函数 get set方法不再列出
     */
    private String dormitoryNumber;
}

然后准备条件,需要实现Condition接口:

public class HomeStudentCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        // 获取当前的环境,一般开发环境分成三套: 开发、测试、生产。
        Environment environment = conditionContext.getEnvironment();
        // 从环境中获取学生类型
        String studentType = environment.getProperty("studentType");
        // 如果是住宿学生就加入到IOC容器中
        if ("HomeStudent".equals(studentType)){
            return true;
        }
        return false;
    }
}
public class BoardStudentCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment environment = context.getEnvironment();
        String studentType = environment.getProperty("studentType");
        if ("BoardStudent".equals(studentType)){
            return true;
        }
        return false;
    }
}

然后在对应的bean上加上条件变量:

@Configuration
@ComponentScan(basePackages = "org.example")
public class SpringConfig {
    @Bean
    @Conditional(HomeStudentCondition.class)
    public HomeStudent homeStudent() {
        return new HomeStudent("出入证");
    }
    @Bean
    @Conditional(BoardStudentCondition.class)
    public BoardStudent boardStudent() {
        return new BoardStudent("宿舍200");
    }
}

在IDEA中配置环境。

测试代码:

 private static void annotationTest() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        String[] beanNames = applicationContext.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    } 

测试结果:

会发现boardStudent没了。

bean的作用域

  • singleton 单例(默认值),在每个Spring IOC容器中,一个bean仅对应一个对象实例
  • prototype 原型,每次从IOC容器中获取对应的对象的时候,都会返回一个新的对象实例。
  • request 一次HTTP请求中,一个bean定义对应一个实例,即每次HTTP请求会将有各自的bean实例,他们依据某个bean定义创建而成。仅在基于WEB的Spring ApplicationContext的情况才生效
  • session 在一个HTTP Session中,一个bean对应一个实例,仅在基于WEB的Spring ApplicationContext的情况才生效。
  • 在一个全局的的HTTP Session中,一个bean定义对应一个实例。典型情况下仅在使用porlet(一个Tomcat容器)的时候有效。

仅在基于WEB的Spring ApplicationContext的情况才生效。
我们主要常用的是singleton和prototype,下面我们来测试一下:

public class SpringConfig {
    @Bean(name = "studentCard")
    public StudentCard studentCard(){
        return  new StudentCard(11,"22");
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Student student(@Qualifier("studentCard") StudentCard studentCard){
        return new Student(20,"zs",studentCard);
    }

    @Bean
    public StudentCard studentCard2(){
        return  new StudentCard(11,"22");
    }
}
  private static void annotationTest() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        Student student1 = (Student) applicationContext.getBean("student");
        Student student2 = (Student) applicationContext.getBean("student");
        System.out.println(student1 == student2);
    }

结果:

ConfigurableBeanFactory不是枚举类型,只是有两个常量字符串: singleton和prototype。你直接写这两个字符串中任意一个也行。
配置文件中进行测试:

<bean id="student" class="org.example.Student" scope="prototype">
        <property name = "name" >
            <value type="java.lang.String"><![CDATA[z<3]]></value>
        </property>
        <property name="age">
            <value>34</value>
        </property>
    </bean>
 private static void xmlTest() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        Student student1 = (Student) applicationContext.getBean("student");
        Student student2 = (Student) applicationContext.getBean("student");
        System.out.println(student1 == student2);
    }

一点点细节的补充

在singleton作用域下,容器在启动的时候就会创建该对象,但是也支持懒加载,即在首次向容器中获取该对象的时候创建。
那么怎么告知IOC容器,在启动的时候,先不要创建呢? 通过@Lazy注解。如何测试呢? 你可以在对应对象的构造函数上,打断点或者输出点东西测试,也可以在启动的时候,打印IOC容器来所有对象的名字来打印。
这里是在对应对象的构造函数上输出一点东西来测试。

  • 配置类懒加载
@Configuration
public class SpringConfig {
    @Bean(name = "studentCard")
    public StudentCard studentCard(){
        return  new StudentCard(11,"22");
    }
    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Student student(@Qualifier("studentCard") StudentCard studentCard){
        return new Student(20,"zs",studentCard);
    }
    @Bean
    @Lazy
    public StudentCard studentCard2(){
        return  new StudentCard(11,"22");
    }
}
  • 配置文件懒加载
  <bean id="student" class="org.example.Student" lazy-init="true">
        <property name="age">
            <value>34</value>
        </property>
    </bean>

这里测试结果就不再展示了。

自动装配的几种形式

上面我们已经介绍了,自动装配的两个注解了:

  • @Autowired 默认根据类型去查找,如果找不到,默认会报错。
@Autowired(required = false),就可以避免在容器中找不到对应类型时抛出错误。
  • @Qualifier 按照名字进行装配

这里我们再介绍几个:

  • @Primary
优先,可以配合@Autowired使用,假设容器里有两个@Autowired需要的对象,在装配时,被打上@Primary的对象,优先被选中
  • @Resource 并非来自Spring,来自Java的JSR250提案,默认按照bean名进行匹配,如果没找到对应的bean名,则去匹配对应的类型,JDK已经自带。
  • @Inject 并非来自Spring,来自于Java的 JSR330提案。需要引入jar包。

Spring帝国简介

从刚开始的Spring framework,到现在Spring家族已经有很多产品了:

后面我们将会介绍SpringMVC,接管MVC的C的一个框架。Java领域的问题,在Spring帝国,几乎都可以找到解决方案(一个是方案整合(Spring Cloud),一个是自己提供(Spring MVC)。)

总结一下

最开始我是从视频开始学习Spring框架的,看视频也是最快学框架的方式,其实看视频的时候,心里还是有些疑问的,但是又找不到人去问。感觉视频中讲的有的时候很牵强,不成系统,零零碎碎的。我不是很喜欢零零碎碎的知识点,我喜欢系统一点的,于是就又系统的整理了一下自己对Spring的理解,也算是入门教程,也算是总结。希望对各位学习Spring有所帮助。

参考资料:

查看原文

赞 0 收藏 0 评论 0

北冥有只鱼 发布了文章 · 2月10日

欢迎光临Spring时代-绪论

在编码一段时间之后,对框架、库又有了新的理解,于是打算重新学习下框架、库。

重回Servlet

让我回忆下我的Java之路,刚开始是学Java SE,在控制台里面输出看结果,当时觉得这个控制台还是比较单调的,不如动画桌面生动,本来打算学习下Swing,后来发现这玩意有点丑,后来发现Java的市场在WEB端,也就是浏览器这里,然后就开始学习JavaScript、CSS、HTML、Jquery。学完这三个,还是没有感应到Java的用处,我那个时候在想浏览器中输入框的值该怎么到Java里面呢? 答案就是Servlet,Servlet是浏览器和服务端的桥梁。像下面这样:

然后接到值之后,我们还是通常需要存储一下,也就是跟数据库打交道,也就是JDBC,后来发现JDBC有些步骤是重复的,不必每次和数据库打交道的时候都重复写一遍,也就是DBUtils。这似乎是一个完整的流程了,从页面接收数据,存储到数据库,然后页面发起请求再将数据库中的数据返回给数据库。但是这个过程并不完美,第一个就是Java和数据库的合作,每次发送SQL语句都是新建一个连接,这相当消耗资源,于是在这里我们又引入了数据库连接池。那个时候还不怎么回用maven,处理依赖,那个时候总是会去找许多jar包粘贴在lib目录下。
那个时候的一个数据处理链条大致是这样的:

然后很快我们就发现代码越写越多了,于是我们就是开始划分结构,也就是MVC,上面的图就被切成了下面这样:

view是视图层提供给用户,供用户操作,是程序的外壳。Controller根据用户输入的指令,选取对应的model,读取,然后进行相应的操作,返回给用户。
这三层是紧密联系在一起的,但又是互相独立的,每一层内部的变化不影响其他层。每一层都对外提供接口(Interface),供上面一层调用,这样一来,软件就可以实现模块化,修改外观和变更数据都不用修改其他层,大大方便了软件的维护和升级。
但是还不是很完善,现在来看我们的代码结构变得清晰了。
软件设计的一个目标就是写的代码越来越少,原生的JDBC还是有些繁琐,那么能否更进一步,更加简洁一些呢? 这是许多Java界的开发者提出的问题,答案就是ORM(Object Relational Mapping)框架, 比较有代表性的就是MyBatis、Hibernate。
这是在减少模型层的代码量,但是目前整合ORM框架、连接池还是编码,整合的优劣受制于编写代码人的水平。现在我们引入的框架之后,我们的引入的框架之间的关系就变成了就变成了下面这样:

看起来似乎结构很整齐的样子,但并不是我们想要的目标,因为通常一个WEB工程并不止会引入一个框架,我们会陆陆续续引入其他框架,比如定时任务框架,日志框架,监控框架,权限控制框架。像下面这样:

混乱而无序,也许会为每个框架准备一个工具类,封装常用的方法。但是这仍不是我们想要的目标,因为代码不是一成不变的,比如ORM框架,可能你在和数据库连接池整合的时候是一个版本,但是后续发现要升级,这个时候如果你是硬编码的话,你就得该代码,可能还需要思索一番 ,那么我们能不能不硬编码呢?统一管理这些框架之间的依赖关系,做到可配置化。

这也就是工厂模式,Spring的前身,当然Spring不只是工厂模式。

如何取对象 - IOC与DI绪论

在Java的世界里,我们获得对象的方式主要是new,对于简单的对象来说,这一点问题都没有。但是有些对象创建起来,比较复杂,我们希望能够隐藏这些细节。注意强调一遍,我们希望对于对象的使用者来说,隐藏这些细节。
这是框架采用工厂模式的一个主要目的,我们来看一下MyBatis中的工厂:

我们通过SqlSessionFactory对象很容易就拿到了SqlSession对象,但是SqlSession对象的就比较复杂,我们来看下源码:


这是工厂模式的一种典型的应用场景。
Java的世界是对象的世界,我们关注的问题很多时候都是在创建对象、使用对象上。假如你想用一个HashMap,大可不必用上设计模式,直接new就可以了。但是不是所有的对象都像HashMap那样,简单。在我对设计模式和Spring框架还不是很了解之前,没用Spring系的框架,在Service层调用Dao层的时候,还是通过new,每个Service的方法调用Dao,都是通过new,事实上在这个Service类,同样类的对象可以只用一个的,也就是单例模式。不懂什么叫单例模式的,可以参看我这篇博客:
今天我们来聊聊单例模式和枚举

那么这个时候我们的愿景是什么呢? 在面向对象的世界里,我们主要的关注的就是取(创建)对象和存对象,就像数据结构一样,我们主要关注的两点是如何存进去,如何取出来。创建的对象也有着不同的场景,上面我们提到的有些对象创建起来比较复杂,我们希望对对象的使用者屏蔽掉这类细节。这类场景在Java中是很常见的,比如线程池的创建:

虽然我们并不推荐直接使用这种方式创建线程池。那么还有一种情况就是扩展性设计,比如说刚开始我定义了一种对象,将该对象对应的操作方法放置到了一个接口中,这也就是面向对象的设计理念,做什么用接口,是什么用对象。但是随着发展,原来的对象已经无法满足我的要求了,但是这个对象附属于某一类库,原来的对象对应的现实实体还在使用,为了向前兼容,我只得又建立了一个类,之前的操作还是共同的。你可能觉得我说的有点抽象,这里举一个比较典型的例子,Java的POI库,用于操纵excel,使用频率是很高的,excel是有几种不同的版本的,Excel 2003文件后缀为.xls, Excel 2007及以上版本文件后缀为.xlsx。excel-2003的最大行数是65536行,这个行数有些时候是无法满足需求的,在2007版突破了65536行,最大为1048576行。POI也考虑到了这个,扩展性还是比较高的,将对excel的操作抽象出来放在workbook中,专属于某个版本的操作放在对应的excel类中,像下面这样:

HSSFWorkbook对应03版,Xssfworkbook对应07版,SXSSFWorkbook为了解决读取大型excel数据内存溢出而推出的。多数情况下,我们要使用的其实是workbook中的方法,没必要还要去看看当前读取的excel是哪个版本的,这也就是WorkbookFactory的初衷,你读文件需要文件对象,你将这个文件对象交给我,我来向你返回对应的Workbook对象。

这里抽象一点的描述就是,使用该对象不再通过new,而是向工厂传递标识符,由工厂来返回对应的对象。这也就是简单工厂模式。很多讲解简单工厂模式的博客或者视频(讲Spring框架入门的或者讲设计模式的)通常会从解耦合的角度出发,我在看视频或者看博客的时候,心里就会有一个问题,你解的是谁和谁的耦合,如果是从上面的介绍的WorkbookFactory来说的话,没用这种简单工厂模式,那么我认为就是希望使用WorkBook对象的编码者和具体的Excel版本的类耦合了起来,像下面这样:

public class ExcelDemo {
    public static void main(String[] args) throws IOException {
        XSSFWorkbook xssfWorkbook = new XSSFWorkbook("");
        HSSFWorkbook hssfWorkbook = new HSSFWorkbook();
        SXSSFWorkbook sxssfWorkbook = new SXSSFWorkbook();
    }
}

我在刚开始做Excel导入的时候,就是先去根据Excel版本去找对应的类,然后做读取数据工作。在写完之后我又想了想,这样不是强制限定了用户上传的Excel版本了吗? 假设用户上传的不是这个版本,我应该给个异常提示? 这样似乎有点不友好吧,03版本的Excel也有人再用啊。我想了想,把我的代码改成了下面这样:

 public void uploadExcel(MultipartFile file) throws IOException {
        Workbook workbook = null;
        if (file.getName().endsWith(".xls")){
             workbook = new HSSFWorkbook(file.getInputStream());
        }else if (file.getName().endsWith(".xlsx")){
             workbook = new XSSFWorkbook(file.getInputStream());
        }
        // 做真正的业务处理
    }

这样我的代码就和用户上传的excel版本解除了耦合,用户上传哪个版本我都能做导入,那个时候我还不知道工厂模式,只是下意识的完成需求的时候,想让上传的Excel版本不受限制。然后某天我偶然看到了WorkbookFactory这个类,这个类解除了WorkBook对象和具体Excel版本文件对象的耦合。
用代码来演示上面介绍的工厂模式就是:

public interface   AnimalBehavior {
    /**
     * 所有动物都会睡觉
     */
    void sleep();
    /**
     * 都要吃饭
     */
    void eat();
}
public class Cat implements AnimalBehavior {
    @Override
    public void sleep() {
        System.out.println("我是猫,喜欢睡觉");
    }

    @Override
    public void eat() {
        System.out.println("我是猫,喜欢吃鱼");
    }
}
public class Dog implements  AnimalBehavior {
    @Override
    public void sleep() {
        System.out.println("我是狗,睡觉喜欢在狗窝睡");
    }

    @Override
    public void eat() {
        System.out.println("我是狗,喜欢啃骨头");
    }
}
public class AnimalBehaviorFactory {

    public static AnimalBehavior create(String name){
        AnimalBehavior animalBehavior = null;
        if ("dog".equals(name)){
            animalBehavior = new Dog();
        }else if ("cat".equals(name)){
            animalBehavior = new Cat();
        }
        return  animalBehavior;
    }
}
public class Test {
    public static void main(String[] args) {
        AnimalBehavior dog = AnimalBehaviorFactory.create("dog");
        dog.eat();
        AnimalBehavior cat = AnimalBehaviorFactory.create("cat");
        cat.eat();
    }
}

我刚开始学习简单工厂模式的时候就是有人写了类似的例子,告诉我这叫解耦合,我当时心里的想法是,这是啥呀,完全看不出来这个设计模式好在哪里啊? 这就是设计模式? 这就是解耦合? 你倒是告诉我,谁跟谁解耦合了啊? 就这? 我学了这玩意之后,完全感受不到用武之地啊? 甚至我跟别人解释简单工厂模式是这? 我都感觉不好意思。
在写了一些代码之后,代码量之后,我可以给出答案,谁和谁解耦的答案,AnimalBehavior实例的调用者和具体的动物对象(Cat、Dog)解除了耦合,只需要将某些标识传递给工厂即可,工厂向你返回对应的AnimalBehavior实例,同时可以屏蔽对应对象创建的复杂细节。真实的使用场景就是POI的WorkBookFactory。简单工厂模式我们讲的差不多了,这里UML图我也不画了,因为我觉得有同学可能还不会看UML图(后面会专门出介绍UML的文章),另一方面我觉得我讲的已经足够清晰了。

在面向对象的世界里,我们始终关心的是如何取对象,简单点,再简单点。在取对象的路上我们面临的另一个比较复杂的场景就是,对象之间有复杂的依赖关系,比如我们的ORM框架MyBatis,依赖于连接池,而连接池又依赖于对应的数据库驱动,于是创建一个SqlSession对象就是一组对象的创建和注入,自己一个的去new吗? 这台麻烦了吧? 能不能统一管理呢? 因为哪天我可能更换连接池啊? 这就是Spring IOC思路,你将对象先放入IOC容器中,配置好他们之间的依赖关系,然后IOC容器帮你new就可以了,你就直接只关心取就可以了。

还有一种情况就是你知道怎么创建一个对象,但是无法把控创建的时机,你需要把"如何创建"的代码塞给"负责什么时候创建"的代码。后者在适当的实际,就调用对应的创建对象的函数。

除此之外,在一些语言比如C++的构造函数是不允许抛出异常的,因为这将产生一个没有被完整构造的对象,从而导致对应的内存没有正确释放。在Java中,虽然语言上支持在构造函数中抛异常,但是这是一种并不推荐的做法,由于这不是本篇的主题,这里不做讨论,具体的可以参考这篇博客:

但是业务上要求在创建对象的时候,抛出一个异常,我们该如何处理,我们也可以通过工厂模式,未必是上面的简单工厂模式,但这也是工厂模式思路的一种应用。

工厂模式的本质就是对获取对象过程的抽象,所以接下来介绍的工厂方法模式和抽象工厂模式会比较抽象,我们需要慢慢的体会这两种设计模式的好处,需要一定的代码量才能体会。

工厂方法模式

工厂方法模式 Factory Method,在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体创建工作交给子类去做。该核心类称为一个抽象工厂角色,仅仅负责给出具体工厂子类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。还是上面的例子,我们就可以转变成了下面:

// 定义顶层的核心工厂类
public interface AbstractAnimalFactory {
    AnimalBehavior  getBehavior();
}
//具体的子类负责对应的创建
public class CatFactory implements  AbstractAnimalFactory {
    @Override
    public AnimalBehavior getBehavior() {
        return new Cat();
    }
}
public class DogFactory implements AbstractAnimalFactory {
    @Override
    public AnimalBehavior getBehavior() {
        return new Dog();
    }
}
public class Test {
    public static void main(String[] args) {
        AbstractAnimalFactory abstractAnimalFactory = new DogFactory();
        AnimalBehavior dog = abstractAnimalFactory.getBehavior();
        dog.eat();
    }
}

好处就是,不用硬编码,假设在上面的工厂模式中,我们要添加个猪对象,然后上面的简单工厂就得改,需要再加个判断。
我们并不希望动老代码,即使你小心翼翼的去修改,也无法做到万无一失。那么对于这种工厂方法模式来说,我们只需要在加个猪厂就行了,复合设计原则,对修改关闭,对添加开放。
再加一个猪场:

public class Pig implements  AnimalBehavior {
    @Override
    public void sleep() {
        System.out.println("猪睡了");
    }

    @Override
    public void eat() {
        System.out.println("猪啥都吃");
    }
}
public class PigFactory implements  AbstractAnimalFactory {
    @Override
    public AnimalBehavior getBehavior() {
        return new Pig();
    }
}

抽象工厂模式

工厂模式的本质就是对获取对象过程的抽象,我们再强调一遍,但是不是一种抽象就可以应付所有创建对象的场景,因为站在不同的角度,你就会看见不同的场景。
从很多博客对抽象工厂模式的实现上来说,相对于工厂方法模式,顶层的核心工厂不再只是一个方法,顶层的核心工厂会出现若干个抽象方法,也就是顶层的核心工厂所能生产的对象是一族的产品,每个子工厂生产的产品也不止有一个,这样的例子在现实世界是常见的,比如小米不仅出品手机,还出品只能家居和笔记本电脑。不仅是小米,苹果也做智能家居和电脑。

从这个角度来看的话,工厂方法模式更像是抽象工厂模式的削弱版本。我们首先我们准备两类产品,第一个是手机接口,描述了生产手机的规范,第二个是PC接口,描述了生产PC的规范。你当然也可以用其他去举例,比如高端口罩和低端口罩,但最终表达的意思是一致的。

public interface PersonalComputer {
    //知道电脑
    void makePc();
}
public interface Phone {
   // 制造手机
   void makePhone();
}

然后下面是核心工厂:

public interface AbstractEmFactory {
    PersonalComputer createPersonalComputer();
    Phone createPhone();
}

然后是对应生产产品的工厂:

public class MiEmFactory implements AbstractEmFactory {
    @Override
    public PersonalComputer createPersonalComputer() {
        return new MiPC();
    }

    @Override
    public Phone createPhone() {
        return new MiPhone();
    }
}
public class OppoEmFactory implements AbstractEmFactory {
    @Override
    public PersonalComputer createPersonalComputer() {
        return new OppoPC();
    }

    @Override
    public Phone createPhone() {
        return new OppoPhone();
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        OppoEmFactory oppoEmFactory = new OppoEmFactory();
        PersonalComputer personalComputer = oppoEmFactory.createPersonalComputer();
        personalComputer.makePc();
        Phone oppoPhone = oppoEmFactory.createPhone();
        oppoPhone.makePhone();
    }
}

相对于工厂方法模式来说,抽象工厂方法模式的优点是不必没多一类产品,我就来个工厂去制造它,我们可以根据特点将他们归纳成一族,这样的话也减少了工厂子类,更容易维护。

总结一下

工厂模式并不是一个独立的设计模式,而是三种功能接近的设计模式的统称,这三种设计模式分别是简单工厂模式、工厂方法模式、抽象工厂模式。事实上在《设计模式之禅》这本书分了两章节讲工厂模式,并没有将我们上面讲的简单工厂模式单独拎出来讲,而是将我们上面提到的简单工厂模式算在工厂方法模式中了。但是往上大部分资料都是将工厂模式分为三种:

  • 简单/静态工厂模式
  • 工厂方法模式
  • 抽象工厂模式

在《Effective Java》中作者提倡使用静态工厂模式代替构造器,为什么这种提倡呢,这种提倡是建立在你有多个构造函数的前提下的,我们知道构造函数我们是没有办法改名的,我们能不能通过见名知义原则向调用该对象的人暴露更多的信息呢?
也就是告诉调用者,此时产生的是什么对象。这是静态工厂模式的另一种使用场景。

我们再来审视以下: 简单工厂模式、工厂方法模式、抽象工厂模式。共同的作用还是希望调用者能够更方便的拿到需要使用的对象,也就是解除耦合,同时也方便集中管理。我们希望的是使用对象的人尽可能简单的获取到想要使用的对象,而不是去直接尝试去寻找这个类,然后用构造函数去产生,在面向对象的世界,遍地都是对象,尽可能的归纳对象,收拢对象,这是工厂模式要解决的问题。

简单工厂模式根据调用者传递的标识符来向调用者返回对应的对象,解除了调用者与实际类的耦合,但并不符合开闭原则(对修改关闭,对添加开放)。假设我再要多一类,那么简单工厂模式就可能需要再多一个判断,优点是相对于工厂方法模式来说,我们就不需要知道对应的工厂了。但我们切记不要生搬硬套,只是单纯的使用某一类模式,要根据情况去组合使用。
工厂方法模式复合开闭原则,但是对于调用者来说要去尝试了解对应的工厂,才能产生对应的对象。那么随着产品的增多,这种只能生产一种产品的工厂很快就不能在满足我们的需要,因为每种产品都需要一个厂子,这么多的厂子对于维护的人来说也是一个负担, 我们根据产品的特点将他们划分为一族,抽象工厂生产的就是一族的产品。

没有完美的设计模式,没有哪种设计模式适应所有的状况,我们需要根据实际情况去选择对应的模式,但是也可以在实践中对对应的设计模式加以改造,以便达到最佳的解耦合效果。比如WorkbookFactory这个类也不是一开始就添加到了POI中,也是在4.0版本引入的。

不管你用什么语言,创建什么资源。当你开始为“创建”本身写代码的时候,就是在使用"工厂模式"了。

参考资料:

查看原文

赞 0 收藏 0 评论 0

北冥有只鱼 发布了文章 · 2月9日

Netty学习笔记(一)初遇篇

本篇的学习需要懂NIO的知识,不懂的可以参看下面的文章:

除此之外,还需要大概懂一点网络协议,比如HTTP。

开始之前

这里我们在复习一下简单的复习NIO的三个重要的核心知识点,Selector(选择器)、Channel(通道)、Buffer(缓冲区)。这三个概念是从I/O多路复用抽象而来,网络通信的开始是建立连接(对于TCP协议来说),在建立连接后,双方开始互相发送数据。那么如果是在很多客户端要和服务端进行通信,那么就会有很多连接,但是就算是很多客户端发起请求想要和服务端建立连接,那么发起的请求也是有先后顺序的,更为准确的说,和服务端建立连接也是有先后顺序的,多路复用的思路在Java的实现就是选择器管理通道,连接建立完成,也就是认为可以通信了,但是数据可能还没准备好,选择器可以在数据到达完成的时候才处理数据。

我们可以认为Channel是对连接的抽象,TCP是面向连接的,那么连接建立之后,我们可以认为就通信的双方就建立了一条信道,就可以用来传输数据了,数据我们存放在缓冲区中。选择器负责管理通道,当通道中有选择器感兴趣的事件(可以读或者可以写)之后,选择器就可以选中这个通道,由程序做对应的数据处理,或者读或者写。

缘起

Nowadays we use general purpose applications or libraries to communicate with each other. For example, we often use an HTTP client library to retrieve information from a web server and to invoke a remote procedure call via web services. However, a general purpose protocol or its implementation sometimes does not scale very well. It is like how we don't use a general purpose HTTP server to exchange huge files, e-mail messages, and near-realtime messages such as financial information and multiplayer game data. What's required is a highly optimized protocol implementation that is dedicated to a special purpose. For example, you might want to implement an HTTP server that is optimized for AJAX-based chat application, media streaming, or large file transfer. You could even want to design and implement a whole new protocol that is precisely tailored to your need. Another inevitable case is when you have to deal with a legacy proprietary protocol to ensure the interoperability with an old system. What matters in this case is how quickly we can implement that protocol while not sacrificing the stability and performance of the resulting application

如今我们用不同的应用或者库来进行交流,例如,我们经常使用HTTP Client的库从WEB服务器检索信息,通过RPC去调用WEB服务。然而,通用协议的扩展性是不高的,就像我们不会使用通用的HTTP协议去实现交换绝大的文件,Email信息,近乎实时的消息,比如财务信息和多人游戏的数据。我们需要的是一个高性能的协议实现,专门用于一个特殊的目的。比如,你可能想要实现一个HTTP服务器用于Ajax的聊天应用程序、媒体流、大型文件传输。甚至想自己设计实现一个协议。即使你实现了新协议,你也得保证与旧系统的兼容性。在这种情况下,重要的是我们能够以多快的速度实现该协议,同时又不会对最终的应用程序的稳定性和性能产生影响。

总结

Netty为低延时、自定义协议打造,通用的协议扩展性不高,实时性不高。有的时候我们想单独定制一个网络协议,又想和系统中使用的旧有协议保持兼容。那么就需要用到NIO了,但是JDK原生的NIO存在问题,写起来繁琐。我们希望简单一些,这就是Netty的缘起。

简介

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.
'Quick and easy' doesn't mean that a resulting application will suffer from a maintainability or a performance issue. Netty has been designed carefully with the experiences earned from the implementation of a lot of protocols such as FTP, SMTP, HTTP, and various binary and text-based legacy protocols. As a result, Netty has succeeded to find a way to achieve ease of development, performance, stability, and flexibility without a compromise.

Netty是一个用来快速开发网络应用程序(网络协议的客户端和服务端)的NIO框架,Netty能够让开发网络应用程序(比如TCP和UDP的服务端)更为简单和高效
快速和简单并不意味这Netty会有性能和可靠性上的问题。Netty是经过精心设计的,在实现许多的网络协议,比如FTP、SMTP、HTTP、各种二进制和基于文本的协议,有着良好的实践。原因在于,Netty已经成功地找到了一种方法来实现开发的易用性、性能、稳定性和灵活性。

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

Netty是一个异步事件驱动的网络通信框架,用于快速开发可维护的高性能协议服务器和客户端。
到这里我们就可以大致给Netty定性,Netty首先是一个NIO框架,性能好,稳定性强,可以被用来做应用服务器(比如Tomcat)。可以用来实现网络协议。

NIO 学习笔记(二)相识篇中我们实现了一个聊天室,写起来十分复杂,在JDK的某些版本(有人说JDK在1.8已经解决了这个问题,但是有人说还没解决,有兴趣的可以去搜一搜JDK NIO 空轮询),还存在一些BUG。我们都喜欢简单的东西,于是Netty应运而生,简化了NIO的编程,解决了JDK原生NIO编程的BUG。

在哪些地方用到了

  • Dubbo

    • 引入的依赖用到了Netty(RPC(远程过程调用,也可以理解为通信))
  • RocketMQ - 经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨节点通信,它的 Netty Service 基于 Netty 框架二次封装实现。

基本上在Java语言内,需要用到网络通信的,都有Netty的影子。

特点

  • 稳定

常年都在4.x版本,虽然出了个5.0版本,但是还被作者废弃掉了。
  • 设计

    • Unified API for various transport types - blocking and non-blocking socket
    统一的API,适用于不同的协议(阻塞和非阻塞)
    • Based on a flexible and extensible event model which allows clear separation of concerns
    基于灵活的、可扩展的事件驱动模型。
    • Highly customizable thread model - single thread, one or more thread pools such as SEDA
    高度可定制的线程模型,一个或者多个线程池,比如SEDA。
    • True connectionless datagram socket support (since 3.1)
可靠的无连接数据的Socket支持
  • 易用性(Ease of use)

    • Well-documented Javadoc, user guide and examples
    完善的JavaDoc,用户指南和样例
    • No additional dependencies, JDK 5 (Netty 3.x) or 6 (Netty 4.x) is enough.
    不需要的额外的依赖,3.x版本(只需要JDK5),4.x版本(只需要JDK6就足够了)

辅助测试工具 curl

curl 是常用的命令行工具, 用来请求 Web 服务器。它的名字就是客户端(client)的 URL 工具的意思。
在Netty系列的学习笔记中,我们用Netty来实现一个简单的服务器,我们使用curl和浏览器来测试这个服务器。
如何安装参考Windows下安装使用curl命令

当然是从Hello World开始了啊

简单实例

我们从一个例子来介绍Netty的使用,首先我们还是一个maven工程,然后引入对应的依赖:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.56.Final</version>
</dependency>
public class NettyDemo01 {
 public static void main(String[] args) throws InterruptedException {
        // 创建事件循环组  接收连接
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        // 接收连接 并分发给worker线程
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        // 启动Netty
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        ChannelFuture channelFuture
                = serverBootstrap.group(bossGroup, workerGroup).
                channel(NioServerSocketChannel.class).
                childHandler(new NettyServerInitHandler()).bind(8080).sync();
        // Wait until the server socket is closed.
        // 等待直到服务端的socket关闭,在这个例子中,服务端的Socket永远不会关闭,但是你可以优雅的关闭
        // 你的服务
        // In this example, this does not happen, but you can do that to gracefully
        // shut down your server.       
        channelFuture.channel().closeFuture().sync();
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }
  }
// 综合初始化器
public class NettyServerInitHandler extends ChannelInitializer<SocketChannel> {
    // 通道被初次注册执行此方法等价于连接被建立
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // 将处理器加入
        pipeline.addLast("HttpServerCodec",new HttpServerCodec());
        // 将自定义的处理加入
        pipeline.addLast("NettyServerHandler",new NettyServerHandler());
    }
}
public class NettyServerHandler extends SimpleChannelInboundHandler<HttpObject> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        //如果消息的类型是Http请求,才处理
        if (msg instanceof HttpRequest){
            // 统一编码
            ByteBuf content = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
            // 发送HTTP响应
            DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
            // 设置请求头 
            response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain");
            // 
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes());
            // 返回响应
            ctx.writeAndFlush(response);
        }
    }
}    

在浏览器输入localhost:8080 的测试结果:

在curl输入localhost:8080的测试结果:

浏览器的测试结果字很小,我调大了才这么显示。

基本分析

还记得Java的网络编程吗? 我们要写Java的NIO做网络编程,通常是下面这样:

   Selector selector = Selector.open();
    //创建服务端的通道
    ServerSocketChannel serverSocketChannel1 = ServerSocketChannel.open();
    // 将该通道设置为非阻塞方式
    serverSocketChannel1.configureBlocking(false);
    // 创建Socket
    ServerSocket serverSocket = serverSocketChannel1.socket();
    // 绑定端口
    serverSocket.bind(new InetSocketAddress(8888));
    // 为ServerSocketChannel注册对对客户端请求连接事件感兴趣
    // 此时该channel处于选择器得管理之下
    serverSocketChannel1.register(selector, SelectionKey.OP_ACCEPT);

基本上核心的思想是先生成选择器对象(Selector),然后将通道设置为非阻塞模式,然后监听端口,然后将通道交给我们产生的选择器管理。然后获取选择器上就绪的事件,根据不同的就绪事件做不同的数据处理。
Netty将其简化了,提供了统一的API,EventLoopGroup用于接收连接和处理连接,我们需要两个EventLoopGroup,一个用于管理连接,另一个处理连接。在ServerBootstrap指定哪个管理连接,哪个处理连接。最后我们还是要处理数据,也就是在ServerBootstrap指定数据的处理者。
Netty处理的基本流程:

相对于原生的NIO,逻辑又清晰了很多。

总结一下使用Netty编码的基本流程

  • 入口类/主程序类

    • 配置一些参数(端口,谁处理连接)
  • 内置初始化器
    -调用一些内置的类(编码、解码)
  • 自定义初始化器

    • 编写一些自定义的类(处理数据)

    Netty内部提供了非常强大的类库(内置初始化器),每个初始化器都可以完成一个小功能,以后在开发时,我们第一步需要先将需要完成的功能分解成若干部,第二部只需要在netty类库中寻找,看哪些已有类能够帮助我们直接实现;第三步,如果某个功能Netty没有提供,则编写自定义的初始化器。

基本API梳理

  • 继承链



注意InBound和OutBound。

  • 常用的API

我们结合继承连来说常用的API,从SimpleChannelInboundHandler开始吧:

ChannelHandlerAdapter有两个空方法,是从ChannelHandler(是一个接口)中而来。

然后方法上没有说明,我们去看对应的接口上的方法说明:

 /**
    * Gets called after the {@link ChannelHandler} was added to the actual context 
        and it's ready to handle events.
        在对应的ChannelHandler(处理器)被加入到实际的上下文,
            该处理器准备去处理一些事件时            被调用。
     *
     */
   void handlerAdded(ChannelHandlerContext ctx) throws Exception;

同样的handlerRemoved是在对应的处理器被移除之后,被调用。

void channelRegistered 通道被注册时触发
void channelActive 通道被激活触发
void channelInactive 通道失活触发
void channelUnregistered 通道取消注册
我们按照多路复用的I/O模型再来梳理一下哈,首先我们将通道等同于连接,根据我们上面讲的bossGroup负责管理连接的状态,当连接建立后,bossGroup将连接交给workerGroup去处理连接。但是连接建立完成之后,并不代表数据到来了,所以Netty的设计者将这些都抽象了出来,其实也是在抽象网络通信的过程:
  • 连接建立 (通道被注册)
  • 有数据到来 (通道被激活)
  • 数据被读取完毕(通道失活)
  • 连接取消(通道取消注册)

我们重写这些方法来再次看下HTTP协议

再度审视HTTP协议

public class NettyServerHandler extends SimpleChannelInboundHandler<HttpObject> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        if (msg instanceof HttpRequest) {
            HttpRequest httpRequest = (HttpRequest) msg;
            URI uri = new URI(httpRequest.uri());
            // 浏览器请求的时候会请求两次,会默认请求一次icon
            // 这里先不处理这次请求。
            if (!"/favicon.ico".equals(uri.getPath())) {
                System.out.println(httpRequest.method().name());
                ByteBuf content = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
                DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
                response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
                response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
                ctx.writeAndFlush(response);
            }
        }
    }
     // 增加处理时,自动触发
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        System.out.println("1.增加处理器");
        super.handlerAdded(ctx);
    }
    // 当把通道注册到选择器触发
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("2.通道被注册");
        super.channelRegistered(ctx);
    }
    // 通道被激活,触发另一个方法
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("3.通道被激活");
        super.channelActive(ctx);
    }
    // 激活的通道,失去激活状态
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("4.通道失活");
        super.channelInactive(ctx);
    }
    // 通道取消注册
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("5.通道取消注册");
        super.channelUnregistered(ctx);
    }
    // 失败捕获
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("6.异常捕获");
        super.exceptionCaught(ctx, cause);
    }
}

这里我认为你已经装上了curl了,我们启动服务端来测试一下。

  • curl的测试结果



对应网络请求一次请求,一次响应。
我们现在用浏览器来测试一下:

有可能你出现的是这种情况:

我们首先来分析一下为什么会出现这种状况:

首先是浏览器发起了两次请求,一次请求就是一个连接。

所以是两次,那为什么两次结果都不一样呢?
这就跟建立连接的先后顺序有关系了,第一种结果就是两次连接建立起来的时间间隔比较短造成的。所以是112233.
而第二种就是在第一个连接建立完毕的时候,第二个请求才建立起连接。
然后我们清空控制台,再来请求一下看一下结果:


这种情况是为什么呢? 因为我们对于浏览器发起的favicon.ico请求没有响应,这个请求还一直在等待中。
我们再次请求的时候,浏览器会将上一次的请求取消,也就是连接取消建立,再度请求一次。
所以会出现1、2、3、4、5这种情况。
那么为什么我们处理了的请求,为很么连接没有取消呢?
这就涉及到HTTP协议了,我们知道HTTP是无状态的,也就是说每次客户端和服务端建立连接都是要TCP建立连接,再传送数据,但有的时候这样开销有点大,我们并不希望每次HTTP请求都建立TCP连接,那能不能保持上一次的连接呢?
这就是keep-alive,保持上一次的连接,我们上面指定的HTTP的协议是默认开启的,所以对于处理了的请求,并不会断开连接。在关闭网页之后就会断掉连接。

再度总结

我们现在已经对Netty已经有一个大致的认知,知道这是一个高性能的、高扩展性的NIO框架。也能通过一些Netty提供的接口来网络编程了。使用Netty进行网络编程的流程是类似的,像JDBC一样,先是在在ServerBootstrap 中设定监测连接的和处理连接的,然后在ServerBootstrap 设定综合处理器(也就是NettyServerInitHandler),在综合处理器中加入Netty提供的处理器和自定义的处理器。在自定义的处理器中处理请求和响应。如果你还想在连接建立的时候,做一些工作,Netty也能满足你的需求。刚开始可能比较晕哈,这个正常,可以先大致记住这个流程

事件驱动机制与Reactor模式简介

Netty将自身定义为异步事件驱动的网络编程框架,那什么是事件驱动,通俗的说,就是在Netty中某事件发生会触发某些方法,在上面的基本API梳理中,我们已经发现了,连接建立触发一个方法,取消连接触发一个方法。更为准确的说法是Netty在收到Socket数据流的时候,会在pipeline上的处理器上传播,某些事件被触发即为调用对应的方法。
那Reactor(反应器)模式呢? 这是Netty的高性能就在于Reactor(反应器模式)模式上,Reactor是这样一种模式,它要求主线程(I/O处理单元)只负责监听文件描述上是否有事件发生,有的话立刻将该事件通知工作线程。

用Netty编写点对点的聊天

这个其实只是一个示例,我们用来加深对Netty的理解而已,本身打算放在GitHub上,想了想还是放在这里吧。使用Netty编写代码的套路是一致的:

  • 在入口类设置监测连接的和真正处理连接的
  • 绑定端口
  • 设置初始化处理器(NettyServerInitHandler ),在这个处理器中加入Netty自带的和自己编写的
  • 在自己编写的处理器中,做数据处理工作。

所以上面我们编写的NettyDemo01也可以接着复用,这里我们要再讲一下ChannelPipeline,我们上面已经讲了,Netty在收到Socket数据流的时候会在pipeline上的处理器上传播。然后我们上面调用的都是addlast。所以我们自定义的处理器总是最后被传播上。
在编写对应的代码之前,我们再来一点点网络编程的知识,网络协议中有一个报文的概念,限制了一次发送信息最多发送多少个,TCP协议发送接收数据时,如果数据过大,接收到的数据会是分包的, 这就是通信的双方发送的消息可能是经过了拆分和合并,尽管使用微信聊天的我们可能并没有感觉。
Netty为我们提供拆分和并的类我们只需要将其加入到处理器中就行。除了拆分和合并,我们还得统一编码,不用担心Netty也提供了。
所以上面的初始化处理器就被改造成了这样:

public class MyNettyServerInit extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // 加入拆分
        pipeline.addLast("decoder",new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,8,0,8));
        // 加入合并
        pipeline.addLast("prepender",new LengthFieldPrepender(8));
        // 加入解码 
        pipeline.addLast("StringDecoder",new StringDecoder(CharsetUtil.UTF_8));
        // 加入编码
        pipeline.addLast("StringEncoder",new StringEncoder(CharsetUtil.UTF_8));
        // 加入自定义的处理器
        pipeline.addLast("MyNettyServerHandler",new MyNettyServerHandler());
    }
}

LengthFieldBasedFrameDecoder这个类我们简单解释一下,一般我们称这个类为拆包器,我们用的是这个构造函数:

public LengthFieldBasedFrameDecoder(int maxFrameLength, 
int lengthFieldOffset, 
int lengthFieldLength,
int lengthAdjustment, 
int initialBytesToStrip)

maxFrameLength: 最大的报文长度,然后发送过来的信息超过这个,就会报异常,因此通信的双方需要约定一次说多少,
lengthFieldOffset: 一段报文,从哪开始读
LengthFieldBasedFrameDecoder关于这个类我们要细讲,恐怕这篇文章得再加上8000字。我们姑且就这么理解。

public class MyNettyServerHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        //接收消息
        System.out.println("服务端 接收到了" + ctx.channel().remoteAddress() + ",消息是:" + msg);
        //发送消息
        System.out.println("请输入内容:");
        String send = new Scanner(System.in).nextLine();
        ctx.channel().writeAndFlush(send);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush("开始聊天吧...");
    }
}

上面我们讲过channelActive这个方法,连接真正建立的时候触发这个方法,为什么这样做呢? 因为我们这个处理器继承的还是SimpleChannelInboundHandler,先读后写,注意这里带的In,就是处理读的,有In就有Out。
聊天的客户端要稍微改动一下:

public class MyNettyClient {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        // 客户端,所以不是ServerBootStrap
        Bootstrap bootstrap = new Bootstrap();
        ChannelFuture channelFuture = bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class).
                handler(new MyNettyClientInit()).connect("127.0.0.1", 8080).sync();
        channelFuture.channel().closeFuture().sync();
        eventLoopGroup.shutdownGracefully();
    }
}

MyNettyClientInit可以复用服务端的:

public class MyNettyClientInit extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast("decoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 8));
        pipeline.addLast("pretender", new LengthFieldPrepender(8));
        pipeline.addLast("StringDecoder", new StringDecoder(CharsetUtil.UTF_8));
        pipeline.addLast("StringEncoder", new StringEncoder(CharsetUtil.UTF_8));
        pipeline.addLast("MyNettyClientHandler", new MyNettyClientHandler());
    }
}

客户端处理器

public class MyNettyClientHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println("客户端 接收到了消息:  " + ctx.channel().remoteAddress() + ",消息是" + msg);
        System.out.println("请输入内容:");
        String send = new Scanner(System.in).nextLine();
        ctx.channel().writeAndFlush(send);
    }
}

注意上面的建立连接时,发送的信息只能由一段发出,如果两端同时有,则两端再次陷入等待对方发送信息的状态。

有什么用

心跳机制

微服务正在称为常态的今天,微服务有个核心的组件也被我们所熟知,就是注册中心,注册中心该怎么知道某服务是活着的呢?我们通过心跳来判断人是否活着,那么注册中心也是通过心跳,每隔一段时间和服务进行通信,活着发送或者接收。
在一段时间内没收到信息,那么注册中心就可以认为该服务因为某些原因下线了。Netty也为我们提供了这样的类库,也是Netty内置的一个处理器IdleStateHandler。

 public IdleStateHandler( int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds)

readerIdleTimeSeconds: 每隔几秒读一下
writerIdleTimeSeconds: 每隔几秒写一下
allIdleTimeSeconds: 读或写超时时间
注意是两次间隔,客户端发送过来一次请求,服务端即完成了一次读写。
示例:

public class MyNettyServerInit extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast("idleStateEvent", new IdleStateHandler(2,3,6));
        pipeline.addLast("myIdleStatteHandler",new MyIdleStateHandler());
    }
}
public class MyIdleStateHandler extends SimpleChannelInboundHandler<Object> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {

    }
    // 读超时 或 写超时触发该方法
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent){
            String eventType = null;
            IdleStateEvent event = (IdleStateEvent)evt;
            switch (event.state()){
                case READER_IDLE:
                    // 客户端发送的两次请求间隔超过两秒触发
                    eventType = "读超时";
                    break;
                case WRITER_IDLE:
                   // 客户端发送的两次请求间隔超过三秒触发
                    eventType = "写超时";
                    break;
                case ALL_IDLE:
                    // 六秒内读或写都没触发,认为读写超时
                    eventType = "读写超时";
                    break;
            }
            System.out.println(eventType);
            ctx.channel().close();
        }
    }
}

下面我们用curl测试下:


写超时不再测试

netty实现webSocket

WebSocket简介

HTTP协议一向是客户端发起请求,服务端回应,但有的情况下,我们希望服务端主动的向客户端推送。这就是WebSocket协议。在之前服务端要想向客户端主动推送信息,要么是轮询(隔断时间问一下),要么一直是监听(要么是一直不挂电话),有响应之后,再度重新建立HTTP连接。这种方式相当消耗资源。WebSocket就能够做到一次HTTP信息,多次数据发送。

示例

java端示例:

public class NettyServerInitHandler extends ChannelInitializer<SocketChannel> {
    // 通道被注册执行此方法
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast("HttpServerCodec",new HttpServerCodec());
        // 我们用POST方式请求服务器的时候,对应的参数信息是保存在message body中的,如果只是单纯的用HttpServerCodec
        // 是无法完全的解析Http POST请求的,因为HttpServerCodec只能获取uri中参数,所以需要加上
        // HttpObjectAggregator.
        pipeline.addLast("HttpObjectAggregator", new HttpObjectAggregator(4096));
        // 设定地址
        pipeline.addLast("WebSocketServerProtocolHandler",new WebSocketServerProtocolHandler("/myWebSocket"));
        pipeline.addLast("NettyServerHandler",new MyWebSocketServerHandler());
    }
}
public class MyWebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println("server 接收到的客户端消息: " + msg.text());
        ctx.channel().writeAndFlush(new TextWebSocketFrame("hello client"));
    }

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("连接建立....");
    }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("连接取消建立....");
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript">
        // websocket是ws开头
        var webSocket = new WebSocket("ws://localhost:8080/myWebSocket");
        function sendMessage(msg) {
            if (webSocket.readyState == webSocket.OPEN)
                webSocket.send(msg);
        }

        webSocket.onmessage = function (event) {
            document.getElementById("tip").innerText = "接收的消息: " + event.data;
        }

        webSocket.onopen = function (event) {
            document.getElementById("tip").innerText = "连接开启";
        }
        webSocket.onclose = function (event) {
            document.getElementById("tip").innerText = "连接关闭";
        }

    </script>
</head>
<body>
<form>
              <textarea name="message">
              </textarea>
    <input type="button" onclick="sendMessage(this.form.message.value)" value="发送">
</form>

<div id="tip"></div>
</body>
</html>

效果:

顺带讲一下Netty官方指南

这里讲下,Netty开发者指南写的还是挺不错的:



但是对刚写了没多少行代码的初学者来说,看这个就有点晕了,不仅需要良好的英文阅读能力,还需要对网络协议有一定的了解,同时Java基础要扎实,对网络编程要有一定的了解。不然不光看我这篇教程,你也十分晕。
Developer guide 开发者指南

参考资料:

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 15 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-06-10
个人主页被 2.3k 人浏览