带你彻底吃透Spring

Posted 潘潘和他的朋友们

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带你彻底吃透Spring相关的知识,希望对你有一定的参考价值。

框架的意义

对于程序员来说,我们通常知道很多概念,例如组件、模块、系统、框架、架构等,而本文我们重点说 框架

  • 框架,本质上是一些实用经验集合。即是前辈们在实际开发过程中积攒下来的实战经验,累积成一套实用工具,避免你在开发过程中重复去造轮子,特别是帮你把日常中能遇到的场景或问题都给屏蔽掉,框架的意义在于屏蔽掉开发的基础复杂度、屏蔽掉此类共性的东西,同时建立严格的编码规范,让框架使用者开箱即用,并且只需要关注差异面,即业务层面的实现。简而言之,框架只干一件事,那就是 简化开发。然后在此基础上,可能会再考虑一些安全性、效率、性能、弹性、管理、拓展、解耦等等。

理解 Spring 核心

Spring 作为一个框架,目的也是:简化开发 ,只不过在简化开发的过程中 Spring 做了一个特别的设计,那就是 Bean管理,这也是 Spring 的设计核心,而 Bean 生命周期管理的设计巧妙的 解耦 了 Bean 之间的关系。

因此 Spring 核心特性就是 解耦简化

Spring 框架图示展示得很清晰,基本描绘出 Spring 框架的核心:

  • 内核
  • 外延

简单说,就是 Spring 设计了一个 核心容器 Core Container,这里头主要就是管理 Bean 生命周期,然后为了服务这些业务 Bean ,引入了 Core , Context , SpEL 等工具到核心容器中。然后在核心容器基础上,又为了把更多的能力集成进来,例如为了拓展 数据访问 能力加入了 JDBC 、ORM 、OXM 、JMS 、Transactions 等,为了拓展 Web 能力加入了 WebSocket 、Servlet、Web、Portlet 等,其中为了把 RequestMapping 或 Servlet 等这些使用集成到业务 Bean 上,引入了 AOP ,包括还有引入(最终是提供) Aspects、Instrumentation、Messageing 等增强方式。

所以仔细一看,Spring 就是把像数据库访问、Web支持、缓存、消息发送等等这些能力集成到业务 Bean 上,并提供一些测试支持。总结来说理解 Spring 就两点:

  1. Bean管理: 解耦Bean关系。理解为内核,从 Bean 的定义、创建、管理等,这是业务Bean。

  2. 功能增强: 解耦功能、声明式简化。理解为外延,在业务Bean基础上,需要访库等能力,那就是功能增强。

基本体现的就是两个核心特性,一个 解耦、一个 简化

Bean管理 本身就是在做 解耦,解除耦合,这个解耦指 Bean 和 Bean 之间的关联关系,Bean 之间通过接口协议互相串联起来的,至于每个接口有多少个实现类,那都不会有任何影响,Bean 之间只保留单点通道,通过接口相互隔离,关系都交给 Spring 管理,这样就避免了实现类和实现类之间出现一些耦合,就算方法增减了、引用变更了也不至于互相污染。

功能增强 本身就是在做 简化,例如声明式简化,像声明式编程,使用者只需要告诉框架他要什么,不用管框架是如何实现的。另外简化方面还有 约定优于配置 (当然这个确切的说是 SpringBoot 里的设计),约定优于配置其实就是约定好了无需去做复杂的配置,例如你引入一个什么组件或能力就像 redis 或 kafka,你不需要提前配置,因为 springboot 已经为你默认配置,开箱即用。

因此 Spring 框架特性怎么理解?就 解耦简化

而 SpringBoot,简单理解就是在 Spring 框架基础上添加了一个 SPI 可拓展机制版本管理,让易用性更高,简化升级。

而 SpringCloud,简单理解就是,由于 SpringBoot 的 依赖 可以被很好的管理,拓展 可以被可插拔的拓展,因此在 SpringBoot 基础上集成了很多跟微服务架构相关的能力,例如集成了很多组件,便有了 SpringCloud 全生态。

基本了解了 Spring 特性之后,我们回到 Spring 的核心设计 IoC 与 AOP

IoC

我们说了 Spring 的其一特性是 解耦,那到底是使用什么来解耦?

控制反转(Inversion of Control,缩写为 IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称 DI),还有一种方式叫“依赖查找”(Dependency Lookup,EJB 和 Apache Avalon 都使用这种方式)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

简单来说,就是原本 Bean 与 Bean 之间的这种互相调用,变成了由 IoC 容器去统一调配。如果没使用 IoC 容器统一管理业务 Bean,你的应用在部署、修改、迭代的时候,业务 Bean 是会侵入代码实现并互相调用的。

那么问题来了,所有系统都需要引入 IOC 吗?

IoC 容器是面向 迭代 起作用,如果你的应用就 不存在迭代 的情况,即系统是万年不变的,那没必要引入 IoC,因为你每引入一项技术,都势必会增加复杂度,所以额外引入 IoC 也一样会增加你整体应用的复杂度,所以假如 不存在迭代,大可直接写死A类引用B类,B类又写死引用C类,无需引入 IoC。一定要理解每一项技术背后是为了解决什么问题,同时在做架构设计的时候记住两个原则:合适简单。当然,实际上我们大部分应用是 持续迭代 的,在类实现上、互相引用上、甚至接口协议上都有可能变化,所以一般引入 IoC 是合适的(如果是接口协议变化,即参数或返回值发生变化,那还是需要改动类间的代码的)。

具体的,IoC 相当于是把 Bean 实例的创建过程交给 Spring 管理,无论是通过 XML、JavaConfig,还是注解方式,最终都是把实例化的工作交给 Spring 负责,之后 Bean 之间通过接口相互调用,而实例化过程中就涉及到 注入,无论采用什么方式来实例化 Bean,注入 的类别就两种:

  • Setter注入 : 通过 setter 来设置,发生在对象 实例化之后 设置。
  • 构造器注入 : 通过构造器注入,发生在对象 实例化之前 就得把参数/实例准备好。

setter注入:

  1. 与传统的 JavaBean 的写法更相似,程序开发人员更容易理解、接受。通过 setter 方法设定依赖关系显得更加直观、自然。
  2. 对于复杂的依赖关系,如果采用构造注入,会导致构造器过于臃肿,难以阅读。Spring 在创建 Bean 实例时,需要同时实例化其依赖的全部实例,因而导致性能下降。而使用设值注入,则能避免这些问题。
  3. 尤其在某些成员变量可选的情况下,多参数的构造器更加笨重。

构造器注入:

  1. 构造器注入可以在构造器中决定依赖关系的注入顺序,优先依赖的优先注入。
  2. 对于依赖关系无需变化的 Bean ,构造注入更有用处。因为没有 setter 方法,所有的依赖关系全部在构造器内设定,无须担心后续的代码对依赖关系产生破坏。
  3. 依赖关系只能在构造器中设定,则只有组件的创建者才能改变组件的依赖关系,对组件的调用者而言,组件内部的依赖关系完全透明,更符合高内聚的原则。

而这两种方式的注入方式都使用了 反射

反射

了解反射相关类以及含义:

  • java.lang.Class: 代表整个字节码。代表一个类型,代表整个类。
  • java.lang.reflect.Method: 代表字节码中的方法字节码。代表类中的方法。
  • java.lang.reflect.Constructor: 代表字节码中的构造方法字节码。代表类中的构造方法。
  • java.lang.reflect.Field: 代表字节码中的属性字节码。代表类中的成员变量(静态变量+实例变量)。

java.lang.reflect 包提供了许多反射类,用于获取或设置实例对象。简单来说,反射能够:

  1. 在运行时 判断任意一个对象所属的类;
  2. 在运行时构造任意一个类的对象;
  3. 在运行时判断任意一个类所具有的成员变量和方法;
  4. 在运行时调用任意一个对象的方法;
  5. 生成动态代理

IoC反射,只是把 Bean 的实例创建处理完,而后续还有 功能增强,功能增强靠的就是 AOP

AOP

AOP全名 Aspect-Oriented Programming ,中文直译为面向切面编程,当前已经成为一种比较成熟的编程思想,可以用来很好的解决应用系统中分布于各个模块的交叉关注点问题。在轻量级的J2EE中应用开发中,使用AOP来灵活处理一些具有 横切性质 的系统级服务,如事务处理、安全检查、缓存、对象池管理等,已经成为一种非常适用的解决方案。

为什么需要AOP

当我们要进行一些日志记录、权限控制、性能统计等时,在传统应用程序当中我们可能在需要的对象或方法中进行编码,而且比如权限控制、性能统计大部分是重复的,这样代码中就存在大量 重复代码,即使有人说我把通用部分提取出来,那必然存在调用还是存在重复,像性能统计我们可能只是在必要时才进行,在诊断完毕后要删除这些代码;还有日志记录,比如记录一些方法访问日志、数据访问日志等等,这些都会渗透到各个要访问方法中;还有权限控制,必须在方法执行开始进行审核,想想这些是多么可怕而且是多么无聊的工作。如果采用 Spring,这些日志记录、权限控制、性能统计从业务逻辑中分离出来,通过 Spring 支持的面向切面编程,在需要这些功能的地方动态添加这些功能,无需渗透到各个需要的方法或对象中;有人可能说了,我们可以使用“代理设计模式”或“包装器设计模式”,你可以使用这些,但还是需要通过编程方式来创建代理对象,还是要 耦合 这些代理对象,而采用 Spring 面向 切面 编程能提供一种更好的方式来完成上述功能,一般通过 配置 方式,而且不需要在现有代码中添加任何额外代码,现有代码专注业务逻辑。

所以,AOP 以横截面的方式插入到主流程中,Spring AOP 面向切面编程能帮助我们无耦合的实现:

  • 性能监控,在方法调用前后记录调用时间,方法执行太长或超时报警。
  • 缓存代理,缓存某方法的返回值,下次执行该方法时,直接从缓存里获取。
  • 软件破解,使用 AOP 修改软件的验证类的判断逻辑。
  • 记录日志,在方法执行前后记录系统操作日志。
  • 工作流系统,工作流系统需要将业务代码和流程引擎代码混合在一起执行,那么我们可以使用AOP将其分离,并动态挂接业务。
  • 权限验证,方法执行前验证是否有权限执行当前方法,没有则抛出没有权限执行异常,有业务代码捕捉。
  • 等等

AOP 其实就是从应用中划分出来了一个切面,然后在这个切面里面插入一些 “增强”,最后产生一个增加了新功能的 代理对象,注意,是代理对象,这是Spring AOP 实现的基础。这个代理对象只不过比原始对象(Bean)多了一些功能而已,比如 Bean预处理Bean后处理异常处理 等。 AOP 代理的目的就是 将切面织入到目标对象

AOP如何实现

前面我们说 IoC 的实现靠反射,然后解耦,那 AOP 靠啥实现?

AOP,简单来说就是给对象增强一些功能,我们需要看 Java 给我们预留了哪些口或者在哪些阶段,允许我们去织入某些增强功能。

我们可以从几个层面来实现AOP。

  • 编译期

    • 原理:在编译器编译之前注入源代码,源代码被编译之后的字节码自然会包含这部分注入的逻辑。
    • 代表作如:lombok, mapstruct(编译期通过 pluggable annotation processing API 修改的)。
  • 运行期,字节码加载前

    • 原理:字节码要经过 classloader(类加载器)加载,那我们可以通过 自定义类加载器 的方式,在字节码被自定义类加载器 加载前 给它修改掉。
    • 代表作如:javasist, java.lang.instrument ,ASM(操纵字节码)。
    • 许多 agent 如 Skywaking, Arthas 都是这么搞,注意区分 静态agent动态agent
    • JVMTI 是 JVM 提供操作 native 方法的工具,Instrument 就是提供给你操纵 JVMTI 的 java 接口,详情见 java.lang.instrument.Instrumentation
  • 运行期,字节码加载后

    • 原理:字节码被类加载器加载后,动态构建字节码文件生成目标类的 子类,将切面逻辑加入到子类中。
    • 代表作如:jdk proxy, cglib。

按照类别分类,基本可以理解为:

类别原理优点缺点
静态AOP在编译期,切面直接以字节码的形式编译到目标字节码文件中对系统无性能影响灵活度不够
动态AOP在运行期,目标类加载后,为接口动态生成代理类,将切面织入到代理类中动态代理方式,相对于静态AOP更加灵活切入的关注点需要实现接口,对系统有一点性能影响
动态字节码生成在运行期,目标类加载后,动态构建字节码文件生成目标类的 子类,将切面逻辑加入到子类中没有接口也可以织入扩展类的实例方法为final时,则无法进行织入。性能基本是最差的,因为需要生成子类嵌套一层,spring用的cglib就是这么搞的,所以性能比较差
自定义类加载器在运行期,在字节码被自定义类加载器加载前,将切面逻辑加到目标字节码里,例如阿里的Pandora可以对绝大部分类进行织入代码中如果使用了其他类加载器,则这些类将不会被织入
字节码转换在运行期,所有类加载器加载字节码前,进行拦截可以对所有类进行织入-

当然,理论上是越早织入,性能越好,像 lombok,mapstruct 这类静态AOP,基本在编译期之前都修改完,所以性能很好,但是灵活性方面当然会比较差,获取不到运行时的一些信息情况,所以需要权衡比较。

简单说明5种类别:

当然我整理了一份详细的脑图,可以直接在网页上打开。

《脑图:Java实现AOP思路》:

https://www.processon.com/embed/62333d1ce0b34d074452eec2

1、静态AOP

发生在 编译期,通过 Pluggable Annotation Processing API 修改源码。

在 javac 进行编译的时候,会根据源代码生成抽象语法树(AST),而 java 通过开放 Pluggable Annotation Processing API 允许你参与修改源代码,最终生成字节码。典型的代表就是 lombok

2、动态AOP (动态代理

发生在 运行期,于 字节码加载后,类、方法已经都被加载到方法区中了。

典型的代表就是 JDK Proxy


    public static void main(String[] args) 

        // 需要代理的接口,被代理类实现的多个接口,都必须在这里定义
        Class[] proxyInterface = new Class[]IBusiness.class,IBusiness2.class;
        
        // 构建AOP的Advice,这里需要传入业务类的实例
        LogInvocationHandler handler = new LogInvocationHandler(new Business());
        
        // 生成代理类的字节码加载器
        ClassLoader classLoader = DynamicProxyDemo.class.getClassLoader();
        
        // 织入器,织入代码并生成代理类
        IBusiness2 proxyBusiness = 
            (IBusiness2)Proxy.newProxyInstance(classLoader, proxyInterface, handler);
        
        // 使用代理类的实例来调用方法
        proxyBusiness.doSomeThing2();
        ((IBusiness)proxyBusiness).doSomeThing();
    


其中代理实现 InvocationHandler 接口,最终实现逻辑在 invoke 方法中。生成代理类之后,只要目标对象的方法被调用了,都会优先进入代理类 invoke 方法,进行增强验证等行为。


    public class LogInvocationHandler implements InvocationHandler

        private Object target;  // 目标对象

        LogInvocationHandler(Object target)
            this.target = target;
        

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 

            // 执行原有逻辑
            Object rev = method.invoke(target,args);

            // 执行织入的日志,你可以控制那些方法执行切入逻辑
            if (method.getName().equals("doSomeThing2"))
                // 记录日志
            
            return rev;
        
    


当然动态代理相对也是性能差,毕竟也多走了一层代理,每多走一层就肯定是越难以优化。

虽然,动态代理在运行期通过接口动态生成代理类,这为其带来了一定的灵活性,但这个灵活性却带来了两个问题:

  1. 第一代理类必须实现一个接口,如果没实现接口会抛出一个异常。
  2. 第二性能影响,因为动态代理使用反射的机制实现的,首先反射肯定比直接调用要慢,经过测试大概每个代理类比静态代理多出10几毫秒的消耗。其次使用反射大量生成类文件可能引起 Full GC 造成性能影响,因为字节码文件加载后会存放在JVM运行时区的方法区(或者叫持久代,JDK1.8 之后已经在元空间)中,当方法区满的时候,会引起 Full GC ,所以当你大量使用动态代理时,可以将持久代设置大一些,减少 Full GC 次数。

关于动态代理的详细原理和流程,推荐阅读《一文读懂Java动态代理》

3、动态字节码生成

发生在 运行期,于 字节码加载后 ,生成目标类的子类,将切面逻辑加入到子类中,所以使用Cglib实现AOP不需要基于接口。

此时类、方法同样已经都被加载到方法区中了。

典型的代表就是 Cglib(底层也是基于ASM操作字节码), Cglib 是一个强大的,高性能的 Code 生成类库,它可以在运行期间扩展Java类和实现Java接口,它封装了 Asm,所以使用 Cglib 前需要引入 Asm 的jar。

    public static void main(String[] args)    
        byteCodeGe();   
       
  
    /**  
     * 动态字节码生成  
     */  
    public static void byteCodeGe()    
        //创建一个织入器   
        Enhancer enhancer = new Enhancer();   
        //设置父类   
        enhancer.setSuperclass(Business.class);   
        //设置需要织入的逻辑   
        enhancer.setCallback(new LogIntercept());   
        //使用织入器创建子类   
        IBusiness2 newBusiness = (IBusiness2) enhancer.create();   
        newBusiness.doSomeThing2();   
       
  
    /**  
     * 记录日志  
     */   
    public static class LogIntercept implements MethodInterceptor    
  
        @Override   
        public Object intercept(
			Object target, 
			Method method, 
			Object[] args, 
			MethodProxy proxy) throws Throwable    
            
			//执行原有逻辑,注意这里是invokeSuper   
            Object rev = proxy.invokeSuper(target, args);   
            //执行织入的日志   
            if (method.getName().equals("doSomeThing"))    
                System.out.println("recordLog");   
               
            return rev;   
           
      

Spring 默认采取 JDK 动态代理 机制实现 AOP,当动态代理不可用时(代理类无接口)会使用 CGlib 机制,缺点是:

  1. 只能对方法进行切入,不能对接口、字段、static静态代码块、private私有方法进行切入。

  2. 同类中的互相调用方法将不会使用代理类。因为要使用代理类必须从Spring容器中获取Bean。同类中的互相调用方法是通过 this 关键字来调用,spring 基本无法去修改 jvm 里面的逻辑。

  3. 使用 CGlib 无法对 final 类进行代理,因为无法生成子类了。

4、自定义类加载器

发生在 运行期,于 字节码加载前,在类加载到JVM之前直接修改某些类的 方法,并将 切入逻辑 织入到这个方法里,然后将修改后的字节码文件交给虚拟机运行。

典型的代表就是 javasist,它可以获得指定方法名的方法、执行前后插入代码逻辑。

Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法。这比使用Cglib实现AOP更加高效,并且没太多限制,实现原理如下图:

我们使用系统类加载器启动我们自定义的类加载器,在这个类加载器里加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑,我们再看看使用Javassist 实现 AOP 的代码:

/***启动自定义的类加载器****/

//获取存放CtClass的容器ClassPool   
ClassPool cp = ClassPool.getDefault();   
//创建一个类加载器   
Loader cl = new Loader();   
//增加一个转换器   
cl.addTranslator(cp, new MyTranslator());   
//启动MyTranslator的main函数   
cl.run("jsvassist.JavassistAopDemo$MyTranslator", args);  
// 类加载监听器
public static class MyTranslator implements Translator    
	public void start(ClassPool pool) throws 
				NotFoundException, CannotCompileException    
	     
  
    /**  
     * 类装载到JVM前进行代码织入  
     */  
	public void onLoad(ClassPool pool, String classname)    
		if (!"model$Business".equals(classname))    
			return;   
		   
		//通过获取类文件   
		try    
			CtClass  cc = pool.get(classname);   
			//获得指定方法名的方法   
			CtMethod m = cc.getDeclaredMethod("doSomeThing");   
			//在方法执行前插入代码   
			m.insertBefore(" System.out.println(\\"recordLog\\"); ");   
		 catch (NotFoundException e)    
		 catch (CannotCompileException e)    
		   
	   

	public static void main(String[] args)    
		Business b = new Business();   
		b.doSomeThing2();   
		b.doSomeThing();   
	   
 

CtClass 是一个class文件的抽象描述。也可以使用 insertAfter() 在方法的末尾插入代码,或者使用 insertAt() 在指定行插入代码。

使用自定义的类加载器实现AOP在性能上要优于动态代理和Cglib,因为它不会产生新类,但是它仍然存在一个问题,就是如果其他的类加载器来加载类的话,这些类将不会被拦截。

5、字节码转换

自定义的类加载器实现AOP只能拦截自己加载的字节码,那么有没有一种方式能够监控所有类加载器加载字节码呢?有,使用Instrumentation,它是 Java 5 提供的新特性,使用 Instrumentation,开发者可以构建一个字节码转换器,在字节码加载前进行转换。

发生在 运行期 ,于 字节码加载前Java 1.5 开始提供的 Instrumentation APIInstrumentation API 就像是 JVM 预先放置的后门,它可以拦截在JVM上运行的程序,修改字节码。

这种方式是 Java API 天然提供的,在 java.lang.instrumentation ,就算 javasist 也是基于此实现。

一个代理实现 ClassFileTransformer 接口用于改变运行时的字节码(class File),这个改变发生在 jvm 加载这个类之前,对所有的类加载器有效。class File 这个术语定义于虚拟机规范3.1,指的是字节码的 byte 数组,而不是文件系统中的 class 文件。接口中只有一个方法:

	/**  
     * 字节码加载到虚拟机前会进入这个方法  
     */   
    @Override   
    public byte[] transform(  
		        ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;

// 把 classBeingRedefined 重定义之后再交还回去

ClassFileTransformer 需要添加到 Instrumentation 实例中才能生效。

安全点注意

当对 JVM 中的字节码进行修改的时候,虚拟机也会通知所有线程通过安全点的方式停下来,因为修改会影响到类结构。

启动流程

Bean生命周期管理,基本从无到有(IoC),从有到增强(AOP)。

任何Bean在Spring容器中只有三种形态,定义实例增强

从Bean定义信息观察,通过 xml 定义 bean关系propertiesyamljson定义 属性,bean关系和属性就构成Bean的定义,其中BeanDefinitionReader负责扫描定义信息生成Bean定义对象 BeanDefinition。在此基础上,允许对 BeanDefinition 定义进行增强(Mybatis与Spring存在很多使用场景)。

Bean定义完成之后,开始通过反射实例化对象、填充属性等,同时又再次预留了很多增强的口,最终生成一个完整的对象。

实例化流程与三级缓存

从定义到扩展,然后反射实例化,到增强,每种状态都会存在引用。

所以Spring设计 三级缓存,说白了是对应存储Bean生命周期的三种形态:

  • 定义
  • 实例
  • 增强

总结

Spring 就是 反射 + 字节码增强

  • 反射,为了 IoC 和 解耦

  • 字节码增强,为了 简化 和声明式编程

深刻理解 Spring 这两部分核心特性,关于 spring、springboot、springcloud 的所有语法糖设计与使用,就自然清楚。

参考

首发订阅

这里记录技术内容,不定时发布,首发在

(本篇完)

带你彻底吃透Java NIO

目录

一、Java思维导图

二、I/O模型

I/O模型的本质是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
Java共支持三种网络编程模型:BIO、NIO、AIO

  1. BIO:同步并阻塞,服务实现模式为一个连接一个线程,即客户端有一个连接请求时,服务端就需要启动一个线程进行处理。
  2. NIO: 同步非阻塞,服务器实现模式为一个线程处理多个请求连接,即客户端发送的请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
  3. AIO:异步非阻塞,AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端。

三、BIO、NIO、AIO应用场景

  1. BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高, 并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
  2. NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕 系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。
  3. AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分 调用OS参与并发操作,编程比较复杂,JDK7开始支持

四、BIO编程简单流程

  1. 服务器端启动一个ServerSocket;
  2. 客户端启动Socket对服务器进行通 信,默认情况下服务器端需要对每 个客户 建立一个线程与之通讯;
  3. 客户端发出请求后, 先咨询服务器 是否有线程响应,如果没有则会等 待,或者被拒绝;
  4. 如果有响应,客户端线程会等待请 求结束后,在继续执行;

五、NIO核心

NIO 有三大核心部分:Selector(选择器)、Channel(通道)、Buffer(缓冲区)。
NIO是面向缓冲区,或者说面向块编程,数据读取到一个 它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就 增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求 的数量比HTTP1.1大了好几个数量级。
简而言之,NIO可以一个线程处理多个请求。

六、BIO与NIO比较

  1. BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多;
  2. BIO 是阻塞的,NIO 则是非阻塞的;
  3. BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进 行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因 此使用单个线程就可以监听多个客户端通道。

七、NIO 三大核心原理示意图


流程图说明:

  1. Selector 对应一个线程, 一个线程对应多个channel(连接);
  2. 该图反应了有三个channel 注册到 该selector //程序;
  3. 每个channel 都会对应一个Buffer;
  4. 程序切换到哪个channel 是有事件决定的, Event 就是一个重要的概念;
  5. Selector 会根据不同的事件,在各个通道上切换;
  6. Buffer 就是一个内存块 , 底层是有一个数组;
  7. 数据的读取写入是通过Buffer, 这个和BIO , BIO 中要么是输入流,或者是 输出流, 不能双向,但是NIO的Buffer 是可以读也可以写, 需要 flip 方法切换;
  8. channel 是双向的, 可以返回底层操作系统的情况, 比如Linux , 底层的操作系统 通道就是双向的;

八、缓冲区(buffer)

缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个 容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对 象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、 网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类。

1、常用Buffer子类一览

  1. ByteBuffer,存储字节数据到缓冲区;
  2. ShortBuffer,存储字符串数据到缓冲区;
  3. CharBuffer,存储字符数据到缓冲区;
  4. IntBuffer,存储整数数据到缓冲区;
  5. LongBuffer,存储长整型数据到缓冲区;
  6. DoubleBuffer,存储小数到缓冲区;
  7. FloatBuffer,存储小数到缓冲区;

2、buffer四大属性

  1. mark:标记
  2. position:位置,下一个要被读或写的元素的索引, 每次读写缓冲区数据时都会改变改值, 为下次读写作准备。
  3. limit:表示缓冲区的当前终点,不能对缓冲区 超过极限的位置进行读写操作。且极限 是可以修改的
  4. capacity:容量,即可以容纳的最大数据量;在缓 冲区创建时被设定并且不能改变。

3、buffer常用api

JDK1.4时,引入的api

  • public final int capacity( )//返回此缓冲区的容量
  • public final int position( )//返回此缓冲区的位置
  • public final Buffer position (int newPositio)//设置此缓冲区的位置
  • public final int limit( )//返回此缓冲区的限制
  • public final Buffer limit (int newLimit)//设置此缓冲区的限制
  • public final Buffer mark( )//在此缓冲区的位置设置标记
  • public final Buffer reset( )//将此缓冲区的位置重置为以前标记的位置
  • public final Buffer clear( )//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
  • public final Buffer flip( )//反转此缓冲区
  • public final Buffer rewind( )//重绕此缓冲区
  • public final int remaining( )//返回当前位置与限制之间的元素数
  • public final boolean hasRemaining( )//告知在当前位置和限制之间是否有元素
  • public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区

JDK1.6时引入的api

  • public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
  • public abstract Object array();//返回此缓冲区的底层实现数组
  • public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
  • public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区

九、通道(channel)

1、基本介绍

(1)NIO的通道类似于流

  • 通道可以同时进行读写,而流只能读或者只能写;
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读数据,也可以写数据到缓冲

(2)BIO 中的 stream 是单向的,例如 FileInputStream 对 象只能进行读取数据的操作,而 NIO 中的通道 (Channel)是双向的,可以读操作,也可以写操作。
(3)Channel在NIO中是一个接口
(4)常用的 Channel 类有:FileChannel、 DatagramChannel、ServerSocketChannel 和 SocketChannel。ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket。
(5)FileChannel 用于文件的数据读写, DatagramChannel 用于 UDP 的数据读写, ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。

2、FileChannel

FileChannel主要用来对本地文件进行 IO 操作,常见的方法有:

  1. read,从通道读取数据并放到缓冲区中
  2. write,把缓冲区的数据写到通道中
  3. transferFrom,从目标通道 中复制数据到当前通道
  4. transferTo,把数据从当 前通道复制给目标通道

3、关于Buffer 和 Channel的注意事项和细节

  1. ByteBuffer 支持类型化的put 和 get, put 放入的是什么数据类型,get就应该使用 相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。
  2. 可以将一个普通Buffer 转成只读Buffer。
  3. NIO 还提供了 MappedByteBuffer, 可以让文件直接在内存(堆外的内存)中进 行修改, 而如何同步到文件由NIO 来完成。
  4. NIO 还支持 通过多个 Buffer (即 Buffer 数组) 完成读写操作,即 Scattering 和 Gathering。

十、Selector(选择器)

1、基本介绍

  1. Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连 接,就会使用到Selector(选择器)。
  2. Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然 后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个 通道,也就是管理多个连接和请求。
  3. 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少 了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
  4. 避免了多线程之间的上下文切换导致的开销。

2、selector的相关方法

  1. open();//得到一个选择器对象
  2. select(long timeout);//监控所有注册的通道,当其 中有 IO 操作可以进行时,将 对应的 SelectionKey 加入到内部集合中并返回,参数用来 设置超时时间
  3. selectedKeys();//从内部集合中得 到所有的 SelectionKey。

3、注意事项

NIO中的 ServerSocketChannel功能类似ServerSocket,SocketChannel功能类 似Socket。

十一、通过NIO实现简单的服务端客户端通信

1、服务端

package com.nezha.guor.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;


public class NioServer 
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private static final int PORT = 8080;

    public NioServer() 
        try 
            //获得选择器
            selector = Selector.open();
            serverSocketChannel =  ServerSocketChannel.open();
            //绑定端口
            serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
            //设置非阻塞模式
            serverSocketChannel.configureBlocking(false);
            //将该ServerSocketChannel 注册到selector
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        catch (IOException e) 
            System.out.println("NioServer error:"+e.getMessage());
        
    

    public void listen() 

        System.out.println("监听线程启动: " + Thread.currentThread().getName());
        try 
            while (true) 
                int count = selector.select();
                if(count > 0) 
                    //遍历得到selectionKey集合
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) 
                        SelectionKey key = iterator.next();

                        if(key.isAcceptable()) 
                            SocketChannel sc = serverSocketChannel.accept();
                            sc.configureBlocking(false);
                            sc.register(selector, SelectionKey.OP_READ);
                            System.out.println(sc.getRemoteAddress() + " 上线 ");
                        
                        //通道发送read事件,即通道是可读的状态
                        if(key.isReadable()) 
                            getDataFromChannel(key);
                        
                        //当前的key 删除,防止重复处理
                        iterator.remove();
                    
                 else 
                    System.out.println("等待中");
                
            
        catch (Exception e) 
            System.out.println("listen error:"+e.getMessage());
        
    

    private void getDataFromChannel(SelectionKey key) 
        SocketChannel channel = null;
        try 
            channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            int count = channel.read(buffer);
            //根据count的值做处理
            if(count > 0) 
                String msg = new String(buffer.array());
                System.out.println("来自客户端: " + msg);

                //向其它的客户端转发消息(排除自己)
                sendInfoToOtherClients(msg, channel);
            
        catch (IOException e) 
            try 
                System.out.println(channel.getRemoteAddress() + " 离线了");
                //取消注册
                key.cancel();
            catch (IOException ex) 
                System.out.println("getDataFromChannel error:"+ex.getMessage());
            
        finally 
            try 
                channel.close();
            catch (IOException ex) 
                System.out.println("channel.close() error:"+ex.getMessage());
            
        
    

    //转发消息给其它客户(通道)
    private void sendInfoToOtherClients(String msg, SocketChannel self ) throws  IOException
        System.out.println("服务器转发消息中...");
        System.out.println("服务器转发数据给客户端线程: " + Thread.currentThread().getName());
        //遍历 所有注册到selector 上的 SocketChannel,并排除 self
        for(SelectionKey key: selector.keys()) 
            Channel targetChannel = key.channel();

            //排除自己
            if(targetChannel instanceof  SocketChannel && targetChannel != self) 
                SocketChannel dest = (SocketChannel)targetChannel;
                //将信息存储到buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                //将buffer数据写入通道
                dest.write(buffer);
            
        
    

    public static void main(String[] args) 
        //创建服务器对象
        NioServer nioServer = new NioServer();
        nioServer.listen();
    


2、客户端

package com.nezha.guor.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;

public class NioClient 
    private final int PORT = 8080; //服务器端口
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    public NioClient() throws IOException 
        selector = Selector.open();
        socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //将channel注册到selector
        socketChannel.register(selector, SelectionKey.OP_READ);
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(username + " is ok...");
    

    //向服务器发送消息
    public void sendInfo(String info) 
        info = username + " 说:" + info;
        try 
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        catch (IOException e) 
            System.out.println("sendInfo error:"+e.getMessage());
        
    

    //读取从服务器端回复的消息
    public void readInfo() 
        try 
            int readChannels = selector.select();
            if(readChannels > 0) 
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) 
                    SelectionKey key = iterator.next();
                    if(key.isReadable()) 
                        //得到相关的通道
                        SocketChannel sc = (SocketChannel) key.channel();
                        //得到一个Buffer
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        //读取
                        sc.read(buffer);
                        //把读到的缓冲区的数据转成字符串
                        String msg = new String(buffer.array());
                        System.out.println(msg.trim());
                    
                
                iterator.remove(); //删除当前的selectionKey, 防止重复操作
             else 
                System.out.println("没有可以用的通道...");
            
        catch (Exception e) 
            System.out.println("readInfo error:"+e.getMessage());
        
    

    public static void main(String[] args) throws Exception 
        NioClient nioClient = new NioClient();
        new Thread() 
            public void run() 
                while (true) 
                    nioClient.readInfo();
                    try 
                        Thread.currentThread().sleep(2000);
                    catch (InterruptedException e) 
                        System.out.println("sleep error:"+e.getMessage());
                    
                
            
        .start();

        //发送数据给服务器端
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNextLine()) 
            nioClient.sendInfo(scanner.nextLine());
        
    

3、控制台输出


十二、我愿称你为最强

Java学习路线总结(思维导图篇)
【Java基础知识 1】Java入门级概述
【Java基础知识 2】配置java环境变量
【Java基础知识 3】为何要配置环境变量?
【Java基础知识 4】秒懂数组拷贝,感知新境界
【Java基础知识 5】装箱和拆箱
【Java基础知识 6】Java异常详解
【Java基础知识 7】toString()、String.valueOf、(String)强转
【Java基础知识 8】String、StringBuilder、StringBuffer详解
【Java基础知识 9】序列化与反序列化
【Java基础知识 10】Java IO流详解
【Java基础知识 11】java泛型方法的定义和使用
【Java基础知识 12】java枚举详解
【Java基础知识 13】java注解详解
【Java基础知识 14】java动态代理原理
【Java基础知识 15】java反射机制原理详解
【Java基础知识 16】java内部类使用场景
更多精彩内容,尽在哪吒

以上是关于带你彻底吃透Spring的主要内容,如果未能解决你的问题,请参考以下文章

带你彻底吃透Java NIO

带你彻底吃透Spring

带你彻底吃透Spring

带你彻底吃透Spring

带你彻底吃透·算法复杂度

从冰箱装大象到女娲造人,带你彻底吃透Python面向对象编程