石的三次方

石的三次方 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

@公众号:石的三次方

个人动态

石的三次方 赞了文章 · 1月13日

Spring源码剖析5:JDK和cglib动态代理原理详解

AOP的基础是Java动态代理,了解和使用两种动态代理能让我们更好地理解 AOP,在讲解AOP之前,让我们先来看看Java动态代理的使用方式以及底层实现原理。

转自https://www.jianshu.com/u/668...

本文是基于jdk1.8来对动态代理的底层机制进行探究的

Java代理介绍

Java中代理的实现一般分为三种:JDK静态代理、JDK动态代理以及CGLIB动态代理。在Spring的AOP实现中,主要应用了JDK动态代理以及CGLIB动态代理。但是本文着重介绍JDK动态代理机制,CGLIB动态代理后面会接着探究。

代理一般实现的模式为JDK静态代理:创建一个接口,然后创建被代理的类实现该接口并且实现该接口中的抽象方法。之后再创建一个代理类,同时使其也实现这个接口。在代理类中持有一个被代理对象的引用,而后在代理类方法中调用该对象的方法。

其实就是代理类为被代理类预处理消息、过滤消息并在此之后将消息转发给被代理类,之后还能进行消息的后置处理。代理类和被代理类通常会存在关联关系(即上面提到的持有的被带离对象的引用),代理类本身不实现服务,而是通过调用被代理类中的方法来提供服务。

静态代理

接口

被代理类

代理类

测试类以及输出结果

我们可以看出,使用JDK静态代理很容易就完成了对一个类的代理操作。但是JDK静态代理的缺点也暴露了出来:由于代理只能为一个类服务,如果需要代理的类很多,那么就需要编写大量的代理类,比较繁琐。

下面我们使用JDK动态代理来做同样的事情

JDK动态代理

接口

被代理类

代理类

测试类以及输出结果

JDK动态代理实现原理

JDK动态代理其实也是基本接口实现的。因为通过接口指向实现类实例的多态方式,可以有效地将具体实现与调用解耦,便于后期的修改和维护。

通过上面的介绍,我们可以发现JDK静态代理与JDK动态代理之间有些许相似,比如说都要创建代理类,以及代理类都要实现接口等。但是不同之处也非常明显----在静态代理中我们需要对哪个接口和哪个被代理类创建代理类,所以我们在编译前就需要代理类实现与被代理类相同的接口,并且直接在实现的方法中调用被代理类相应的方法;但是动态代理则不同,我们不知道要针对哪个接口、哪个被代理类创建代理类,因为它是在运行时被创建的。

让我们用一句话来总结一下JDK静态代理和JDK动态代理的区别,然后开始探究JDK动态代理的底层实现机制:
JDK静态代理是通过直接编码创建的,而JDK动态代理是利用反射机制在运行时创建代理类的。
其实在动态代理中,核心是InvocationHandler。每一个代理的实例都会有一个关联的调用处理程序(InvocationHandler)。对待代理实例进行调用时,将对方法的调用进行编码并指派到它的调用处理器(InvocationHandler)的invoke方法。所以对代理对象实例方法的调用都是通过InvocationHandler中的invoke方法来完成的,而invoke方法会根据传入的代理对象、方法名称以及参数决定调用代理的哪个方法。

我们从JDK动态代理的测试类中可以发现代理类生成是通过Proxy类中的newProxyInstance来完成的,下面我们进入这个函数看一看:

Proxy类中的newProxyInstance

 public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        //如果h为空将抛出异常
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();//拷贝被代理类实现的一些接口,用于后面权限方面的一些检查
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            //在这里对某些安全权限进行检查,确保我们有权限对预期的被代理类进行代理
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * 下面这个方法将产生代理类
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * 使用指定的调用处理程序获取代理类的构造函数对象
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            //假如代理类的构造函数是private的,就使用反射来set accessible
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            //根据代理类的构造函数来生成代理类的对象并返回
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

所以代理类其实是通过getProxyClass方法来生成的:

 /**
     * 生成一个代理类,但是在调用本方法之前必须进行权限检查
     */
    private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        //如果接口数量大于65535,抛出非法参数错误
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }

        // 如果在缓存中有对应的代理类,那么直接返回
        // 否则代理类将有 ProxyClassFactory 来创建
        return proxyClassCache.get(loader, interfaces);
    }

那么ProxyClassFactory是什么呢?

   /**
     *  里面有一个根据给定ClassLoader和Interface来创建代理类的工厂函数  
     *
     */
    private static final class ProxyClassFactory
        implements BiFunction<ClassLoader, Class<?>[], Class<?>>
    {
        // 代理类的名字的前缀统一为“$Proxy”
        private static final String proxyClassNamePrefix = "$Proxy";

        // 每个代理类前缀后面都会跟着一个唯一的编号,如$Proxy0、$Proxy1、$Proxy2
        private static final AtomicLong nextUniqueNumber = new AtomicLong();

        @Override
        public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

            Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
            for (Class<?> intf : interfaces) {
                /*
                 * 验证类加载器加载接口得到对象是否与由apply函数参数传入的对象相同
                 */
                Class<?> interfaceClass = null;
                try {
                    interfaceClass = Class.forName(intf.getName(), false, loader);
                } catch (ClassNotFoundException e) {
                }
                if (interfaceClass != intf) {
                    throw new IllegalArgumentException(
                        intf + " is not visible from class loader");
                }
                /*
                 * 验证这个Class对象是不是接口
                 */
                if (!interfaceClass.isInterface()) {
                    throw new IllegalArgumentException(
                        interfaceClass.getName() + " is not an interface");
                }
                /*
                 * 验证这个接口是否重复
                 */
                if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                    throw new IllegalArgumentException(
                        "repeated interface: " + interfaceClass.getName());
                }
            }

            String proxyPkg = null;     // 声明代理类所在的package
            int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

            /*
             * 记录一个非公共代理接口的包,以便在同一个包中定义代理类。同时验证所有非公共
             * 代理接口都在同一个包中
             */
            for (Class<?> intf : interfaces) {
                int flags = intf.getModifiers();
                if (!Modifier.isPublic(flags)) {
                    accessFlags = Modifier.FINAL;
                    String name = intf.getName();
                    int n = name.lastIndexOf('.');
                    String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                    if (proxyPkg == null) {
                        proxyPkg = pkg;
                    } else if (!pkg.equals(proxyPkg)) {
                        throw new IllegalArgumentException(
                            "non-public interfaces from different packages");
                    }
                }
            }

            if (proxyPkg == null) {
                // 如果全是公共代理接口,那么生成的代理类就在com.sun.proxy package下
                proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
            }

            /*
             * 为代理类生成一个name  package name + 前缀+唯一编号
             * 如 com.sun.proxy.$Proxy0.class
             */
            long num = nextUniqueNumber.getAndIncrement();
            String proxyName = proxyPkg + proxyClassNamePrefix + num;

            /*
             * 生成指定代理类的字节码文件
             */
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
            try {
                return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                /*
                 * A ClassFormatError here means that (barring bugs in the
                 * proxy class generation code) there was some other
                 * invalid aspect of the arguments supplied to the proxy
                 * class creation (such as virtual machine limitations
                 * exceeded).
                 */
                throw new IllegalArgumentException(e.toString());
            }
        }
    }

字节码生成

由上方代码byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);可以看到,其实生成代理类字节码文件的工作是通过 ProxyGenerate类中的generateProxyClass方法来完成的。

 public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {
        ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
       // 真正用来生成代理类字节码文件的方法在这里
        final byte[] var4 = var3.generateClassFile();
       // 保存代理类的字节码文件
        if(saveGeneratedFiles) {
            AccessController.doPrivileged(new PrivilegedAction() {
                public Void run() {
                    try {
                        int var1 = var0.lastIndexOf(46);
                        Path var2;
                        if(var1 > 0) {
                            Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar), 
                                                                                   new String[0]);
                            Files.createDirectories(var3, new FileAttribute[0]);
                            var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
                        } else {
                            var2 = Paths.get(var0 + ".class", new String[0]);
                        }

                        Files.write(var2, var4, new OpenOption[0]);
                        return null;
                    } catch (IOException var4x) {
                        throw new InternalError("I/O exception saving generated file: " + var4x);
                    }
                }
            });
        }

        return var4;
    }

下面来看看真正用于生成代理类字节码文件的generateClassFile方法:

private byte[] generateClassFile() {
        //下面一系列的addProxyMethod方法是将接口中的方法和Object中的方法添加到代理方法中(proxyMethod)
        this.addProxyMethod(hashCodeMethod, Object.class);
        this.addProxyMethod(equalsMethod, Object.class);
        this.addProxyMethod(toStringMethod, Object.class);
        Class[] var1 = this.interfaces;
        int var2 = var1.length;

        int var3;
        Class var4;
       //获得接口中所有方法并添加到代理方法中
        for(var3 = 0; var3 < var2; ++var3) {
            var4 = var1[var3];
            Method[] var5 = var4.getMethods();
            int var6 = var5.length;

            for(int var7 = 0; var7 < var6; ++var7) {
                Method var8 = var5[var7];
                this.addProxyMethod(var8, var4);
            }
        }

        Iterator var11 = this.proxyMethods.values().iterator();
        //验证具有相同方法签名的方法的返回类型是否一致
        List var12;
        while(var11.hasNext()) {
            var12 = (List)var11.next();
            checkReturnTypes(var12);
        }

        //后面一系列的步骤用于写代理类Class文件
        Iterator var15;
        try {
             //生成代理类的构造函数
            this.methods.add(this.generateConstructor());
            var11 = this.proxyMethods.values().iterator();

            while(var11.hasNext()) {
                var12 = (List)var11.next();
                var15 = var12.iterator();

                while(var15.hasNext()) {
                    ProxyGenerator.ProxyMethod var16 = (ProxyGenerator.ProxyMethod)var15.next();
                    //将代理类字段声明为Method,并且字段修饰符为 private static.
                   //因为 10 是 ACC_PRIVATE和ACC_STATIC的与运算 故代理类的字段都是 private static Method ***
                    this.fields.add(new ProxyGenerator.FieldInfo(var16.methodFieldName, 
                                   "Ljava/lang/reflect/Method;", 10));
                   //生成代理类的方法
                    this.methods.add(var16.generateMethod());
                }
            }
           //为代理类生成静态代码块对某些字段进行初始化
            this.methods.add(this.generateStaticInitializer());
        } catch (IOException var10) {
            throw new InternalError("unexpected I/O Exception", var10);
        }

        if(this.methods.size() > '\uffff') { //代理类中的方法数量超过65535就抛异常
            throw new IllegalArgumentException("method limit exceeded");
        } else if(this.fields.size() > '\uffff') {// 代理类中字段数量超过65535也抛异常
            throw new IllegalArgumentException("field limit exceeded");
        } else {
            // 后面是对文件进行处理的过程
            this.cp.getClass(dotToSlash(this.className));
            this.cp.getClass("java/lang/reflect/Proxy");
            var1 = this.interfaces;
            var2 = var1.length;

            for(var3 = 0; var3 < var2; ++var3) {
                var4 = var1[var3];
                this.cp.getClass(dotToSlash(var4.getName()));
            }

            this.cp.setReadOnly();
            ByteArrayOutputStream var13 = new ByteArrayOutputStream();
            DataOutputStream var14 = new DataOutputStream(var13);

            try {
                var14.writeInt(-889275714);
                var14.writeShort(0);
                var14.writeShort(49);
                this.cp.write(var14);
                var14.writeShort(this.accessFlags);
                var14.writeShort(this.cp.getClass(dotToSlash(this.className)));
                var14.writeShort(this.cp.getClass("java/lang/reflect/Proxy"));
                var14.writeShort(this.interfaces.length);
                Class[] var17 = this.interfaces;
                int var18 = var17.length;

                for(int var19 = 0; var19 < var18; ++var19) {
                    Class var22 = var17[var19];
                    var14.writeShort(this.cp.getClass(dotToSlash(var22.getName())));
                }

                var14.writeShort(this.fields.size());
                var15 = this.fields.iterator();

                while(var15.hasNext()) {
                    ProxyGenerator.FieldInfo var20 = (ProxyGenerator.FieldInfo)var15.next();
                    var20.write(var14);
                }

                var14.writeShort(this.methods.size());
                var15 = this.methods.iterator();

                while(var15.hasNext()) {
                    ProxyGenerator.MethodInfo var21 = (ProxyGenerator.MethodInfo)var15.next();
                    var21.write(var14);
                }

                var14.writeShort(0);
                return var13.toByteArray();
            } catch (IOException var9) {
                throw new InternalError("unexpected I/O Exception", var9);
            }
        }
    }

代理类的方法调用

下面是将接口与Object中一些方法添加到代理类中的addProxyMethod方法:

private void addProxyMethod(Method var1, Class<?> var2) {
        String var3 = var1.getName();//获得方法名称
        Class[] var4 = var1.getParameterTypes();//获得方法参数类型
        Class var5 = var1.getReturnType();//获得方法返回类型
        Class[] var6 = var1.getExceptionTypes();//异常类型
        String var7 = var3 + getParameterDescriptors(var4);//获得方法签名
        Object var8 = (List)this.proxyMethods.get(var7);//根据方法前面获得proxyMethod的value
        if(var8 != null) {//处理多个代理接口中方法重复的情况
            Iterator var9 = ((List)var8).iterator();

            while(var9.hasNext()) {
                ProxyGenerator.ProxyMethod var10 = (ProxyGenerator.ProxyMethod)var9.next();
                if(var5 == var10.returnType) {
                    ArrayList var11 = new ArrayList();
                    collectCompatibleTypes(var6, var10.exceptionTypes, var11);
                    collectCompatibleTypes(var10.exceptionTypes, var6, var11);
                    var10.exceptionTypes = new Class[var11.size()];
                    var10.exceptionTypes = (Class[])var11.toArray(var10.exceptionTypes);
                    return;
                }
            }
        } else {
            var8 = new ArrayList(3);
            this.proxyMethods.put(var7, var8);
        }

        ((List)var8).add(new ProxyGenerator.ProxyMethod(var3, var4, var5, var6, var2, null));
    }

这就是最终真正的代理类,它继承自Proxy并实现了我们定义的Subject接口。我们通过

HelloInterface helloInterface = (HelloInterface ) Proxy.newProxyInstance(loader, interfaces, handler);
  • 1

得到的最终代理类对象就是上面这个类的实例。那么我们执行如下语句:

helloInterface.hello("Tom");
  • 1

实际上就是执行上面类的相应方法,也就是:

 public final void hello(String paramString)
  {
    try
    {
      this.h.invoke(this, m3, new Object[] { paramString });
      //就是调用我们自定义的InvocationHandlerImpl的 invoke方法:
      return;
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }

注意这里的this.h.invoke中的h,它是类Proxy中的一个属性

 protected InvocationHandler h;

因为这个代理类继承了Proxy,所以也就继承了这个属性,而这个属性值就是我们定义的

InvocationHandler handler = new InvocationHandlerImpl(hello);
  • 1

同时我们还发现,invoke方法的第一参数在底层调用的时候传入的是this,也就是最终生成的代理对象ProxySubject,这是JVM自己动态生成的,而不是我们自己定义的代理对象。

深入理解CGLIB动态代理机制

Cglib是什么

Cglib是一个强大的、高性能的代码生成包,它广泛被许多AOP框架使用,为他们提供方法的拦截。下图是我网上找到的一张Cglib与一些框架和语言的关系:

对此图总结一下:

  • 最底层的是字节码Bytecode,字节码是Java为了保证“一次编译、到处运行”而产生的一种虚拟指令格式,例如iload_0、iconst_1、if_icmpne、dup等
  • 位于字节码之上的是ASM,这是一种直接操作字节码的框架,应用ASM需要对Java字节码、Class结构比较熟悉
  • 位于ASM之上的是CGLIB、Groovy、BeanShell,后两种并不是Java体系中的内容而是脚本语言,它们通过ASM框架生成字节码变相执行Java代码,这说明在JVM中执行程序并不一定非要写Java代码----只要你能生成Java字节码,JVM并不关心字节码的来源,当然通过Java代码生成的JVM字节码是通过编译器直接生成的,算是最“正统”的JVM字节码
  • 位于CGLIB、Groovy、BeanShell之上的就是Hibernate、Spring AOP这些框架了,这一层大家都比较熟悉
  • 最上层的是Applications,即具体应用,一般都是一个Web项目或者本地跑一个程序

本文是基于CGLIB 3.1进行探究的

cglib is a powerful, high performance and quality Code Generation Library, It is used to extend JAVA classes and implements interfaces at runtime.

在Spring AOP中,通常会用它来生成AopProxy对象。不仅如此,在Hibernate中PO(Persistant Object 持久化对象)字节码的生成工作也要靠它来完成。

本文将深入探究CGLIB动态代理的实现机制,配合下面这篇文章一起食用口味更佳:
深入理解JDK动态代理机制

CGLIB动态代理示例

下面由一个简单的示例开始我们对CGLIB动态代理的介绍:

为了后续编码的顺利进行,我们需要使用Maven引入CGLIB的包

图1.1 被代理类

图1.2 实现MethodInterceptor接口生成方法拦截器

图1.3 生成代理类对象并打印在代理类对象调用方法之后的执行结果

JDK代理要求被代理的类必须实现接口,有很强的局限性。而CGLIB动态代理则没有此类强制性要求。简单的说,CGLIB会让生成的代理类继承被代理类,并在代理类中对代理方法进行强化处理(前置处理、后置处理等)。在CGLIB底层,其实是借助了ASM这个非常强大的Java字节码生成框架。

生成代理类对象

从图1.3中我们看到,代理类对象是由Enhancer类创建的。Enhancer是CGLIB的字节码增强器,可以很方便的对类进行拓展,如图1.3中的为类设置Superclass。

创建代理对象的几个步骤:

  • 生成代理类的二进制字节码文件;
  • 加载二进制字节码,生成Class对象( 例如使用Class.forName()方法 );
  • 通过反射机制获得实例构造,并创建代理类对象

我们来看看将代理类Class文件反编译之后的Java代码

package proxy;

import java.lang.reflect.Method;
import net.sf.cglib.core.ReflectUtils;
import net.sf.cglib.core.Signature;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class HelloServiceImpl$EnhancerByCGLIB$82ef2d06
  extends HelloServiceImpl
  implements Factory
{
  private boolean CGLIB$BOUND;
  private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
  private static final Callback[] CGLIB$STATIC_CALLBACKS;
  private MethodInterceptor CGLIB$CALLBACK_0;
  private static final Method CGLIB$sayHello$0$Method;
  private static final MethodProxy CGLIB$sayHello$0$Proxy;
  private static final Object[] CGLIB$emptyArgs;
  private static final Method CGLIB$finalize$1$Method;
  private static final MethodProxy CGLIB$finalize$1$Proxy;
  private static final Method CGLIB$equals$2$Method;
  private static final MethodProxy CGLIB$equals$2$Proxy;
  private static final Method CGLIB$toString$3$Method;
  private static final MethodProxy CGLIB$toString$3$Proxy;
  private static final Method CGLIB$hashCode$4$Method;
  private static final MethodProxy CGLIB$hashCode$4$Proxy;
  private static final Method CGLIB$clone$5$Method;
  private static final MethodProxy CGLIB$clone$5$Proxy;

  static void CGLIB$STATICHOOK1()
  {
    CGLIB$THREAD_CALLBACKS = new ThreadLocal();
    CGLIB$emptyArgs = new Object[0];
    Class localClass1 = Class.forName("proxy.HelloServiceImpl$EnhancerByCGLIB$82ef2d06");
    Class localClass2;
    Method[] tmp95_92 = ReflectUtils.findMethods(new String[] { "finalize", "()V", "equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;" }, (localClass2 = Class.forName("java.lang.Object")).getDeclaredMethods());
    CGLIB$finalize$1$Method = tmp95_92[0];
    CGLIB$finalize$1$Proxy = MethodProxy.create(localClass2, localClass1, "()V", "finalize", "CGLIB$finalize$1");
    Method[] tmp115_95 = tmp95_92;
    CGLIB$equals$2$Method = tmp115_95[1];
    CGLIB$equals$2$Proxy = MethodProxy.create(localClass2, localClass1, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$2");
    Method[] tmp135_115 = tmp115_95;
    CGLIB$toString$3$Method = tmp135_115[2];
    CGLIB$toString$3$Proxy = MethodProxy.create(localClass2, localClass1, "()Ljava/lang/String;", "toString", "CGLIB$toString$3");
    Method[] tmp155_135 = tmp135_115;
    CGLIB$hashCode$4$Method = tmp155_135[3];
    CGLIB$hashCode$4$Proxy = MethodProxy.create(localClass2, localClass1, "()I", "hashCode", "CGLIB$hashCode$4");
    Method[] tmp175_155 = tmp155_135;
    CGLIB$clone$5$Method = tmp175_155[4];
    CGLIB$clone$5$Proxy = MethodProxy.create(localClass2, localClass1, "()Ljava/lang/Object;", "clone", "CGLIB$clone$5");
    tmp175_155;
    Method[] tmp223_220 = ReflectUtils.findMethods(new String[] { "sayHello", "()V" }, (localClass2 = Class.forName("proxy.HelloServiceImpl")).getDeclaredMethods());
    CGLIB$sayHello$0$Method = tmp223_220[0];
    CGLIB$sayHello$0$Proxy = MethodProxy.create(localClass2, localClass1, "()V", "sayHello", "CGLIB$sayHello$0");
    tmp223_220;
    return;
  }

  final void CGLIB$sayHello$0()
  {
    super.sayHello();
  }

  public final void sayHello()
  {
    MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
    if (tmp4_1 == null)
    {
      tmp4_1;
      CGLIB$BIND_CALLBACKS(this);
    }
    if (this.CGLIB$CALLBACK_0 != null) {
      return;
    }
    super.sayHello();
  }

  final void CGLIB$finalize$1()
    throws Throwable
  {
    super.finalize();
  }

  protected final void finalize()
    throws Throwable
  {
    MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
    if (tmp4_1 == null)
    {
      tmp4_1;
      CGLIB$BIND_CALLBACKS(this);
    }
    if (this.CGLIB$CALLBACK_0 != null) {
      return;
    }
    super.finalize();
  }

  final boolean CGLIB$equals$2(Object paramObject)
  {
    return super.equals(paramObject);
  }

  public final boolean equals(Object paramObject)
  {
    MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
    if (tmp4_1 == null)
    {
      tmp4_1;
      CGLIB$BIND_CALLBACKS(this);
    }
    MethodInterceptor tmp17_14 = this.CGLIB$CALLBACK_0;
    if (tmp17_14 != null)
    {
      Object tmp41_36 = tmp17_14.intercept(this, CGLIB$equals$2$Method, new Object[] { paramObject }, CGLIB$equals$2$Proxy);
      tmp41_36;
      return tmp41_36 == null ? false : ((Boolean)tmp41_36).booleanValue();
    }
    return super.equals(paramObject);
  }

  final String CGLIB$toString$3()
  {
    return super.toString();
  }

  public final String toString()
  {
    MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
    if (tmp4_1 == null)
    {
      tmp4_1;
      CGLIB$BIND_CALLBACKS(this);
    }
    MethodInterceptor tmp17_14 = this.CGLIB$CALLBACK_0;
    if (tmp17_14 != null) {
      return (String)tmp17_14.intercept(this, CGLIB$toString$3$Method, CGLIB$emptyArgs, CGLIB$toString$3$Proxy);
    }
    return super.toString();
  }

  final int CGLIB$hashCode$4()
  {
    return super.hashCode();
  }

  public final int hashCode()
  {
    MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
    if (tmp4_1 == null)
    {
      tmp4_1;
      CGLIB$BIND_CALLBACKS(this);
    }
    MethodInterceptor tmp17_14 = this.CGLIB$CALLBACK_0;
    if (tmp17_14 != null)
    {
      Object tmp36_31 = tmp17_14.intercept(this, CGLIB$hashCode$4$Method, CGLIB$emptyArgs, CGLIB$hashCode$4$Proxy);
      tmp36_31;
      return tmp36_31 == null ? 0 : ((Number)tmp36_31).intValue();
    }
    return super.hashCode();
  }

  final Object CGLIB$clone$5()
    throws CloneNotSupportedException
  {
    return super.clone();
  }

  protected final Object clone()
    throws CloneNotSupportedException
  {
    MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
    if (tmp4_1 == null)
    {
      tmp4_1;
      CGLIB$BIND_CALLBACKS(this);
    }
    MethodInterceptor tmp17_14 = this.CGLIB$CALLBACK_0;
    if (tmp17_14 != null) {
      return tmp17_14.intercept(this, CGLIB$clone$5$Method, CGLIB$emptyArgs, CGLIB$clone$5$Proxy);
    }
    return super.clone();
  }

  public static MethodProxy CGLIB$findMethodProxy(Signature paramSignature)
  {
    String tmp4_1 = paramSignature.toString();
    switch (tmp4_1.hashCode())
    {
    case -1574182249: 
      if (tmp4_1.equals("finalize()V")) {
        return CGLIB$finalize$1$Proxy;
      }
      break;
    }
  }

  public HelloServiceImpl$EnhancerByCGLIB$82ef2d06()
  {
    CGLIB$BIND_CALLBACKS(this);
  }

  public static void CGLIB$SET_THREAD_CALLBACKS(Callback[] paramArrayOfCallback)
  {
    CGLIB$THREAD_CALLBACKS.set(paramArrayOfCallback);
  }

  public static void CGLIB$SET_STATIC_CALLBACKS(Callback[] paramArrayOfCallback)
  {
    CGLIB$STATIC_CALLBACKS = paramArrayOfCallback;
  }

  private static final void CGLIB$BIND_CALLBACKS(Object paramObject)
  {
    82ef2d06 local82ef2d06 = (82ef2d06)paramObject;
    if (!local82ef2d06.CGLIB$BOUND)
    {
      local82ef2d06.CGLIB$BOUND = true;
      Object tmp23_20 = CGLIB$THREAD_CALLBACKS.get();
      if (tmp23_20 == null)
      {
        tmp23_20;
        CGLIB$STATIC_CALLBACKS;
      }
      local82ef2d06.CGLIB$CALLBACK_0 = (// INTERNAL ERROR //

对委托类进行代理

我们上面贴出了生成的代理类源码。以我们上面的例子为参考,下面我们总结一下CGLIB在进行代理的时候都进行了哪些工作呢

  • 生成的代理类HelloServiceImpl$EnhancerByCGLIB$82ef2d06继承被代理类HelloServiceImpl。在这里我们需要注意一点:如果委托类被final修饰,那么它不可被继承,即不可被代理;同样,如果委托类中存在final修饰的方法,那么该方法也不可被代理;
  • 代理类会为委托方法生成两个方法,一个是重写的sayHello方法,另一个是CGLIB$sayHello$0方法,我们可以看到它是直接调用父类的sayHello方法;
  • 当执行代理对象的sayHello方法时,会首先判断一下是否存在实现了MethodInterceptor接口的CGLIB$CALLBACK_0;,如果存在,则将调用MethodInterceptor中的intercept方法,如图2.1。

图2.1 intercept方法

图2.2 代理类为每个委托方法都会生成两个方法

在intercept方法中,我们除了会调用委托方法,还会进行一些增强操作。在Spring AOP中,典型的应用场景就是在某些敏感方法执行前后进行操作日志记录。

我们从图2.1中看到,调用委托方法是通过代理方法的MethodProxy对象调用invokeSuper方法来执行的,下面我们看看invokeSuper方法中的玄机:

图2.3 invokeSuper方法

在这里好像不能直接看出代理方法的调用。没关系,我会慢慢介绍。
我们知道,在JDK动态代理中方法的调用是通过反射来完成的。如果有对此不太了解的同学,可以看下我之前的博客----深入理解JDK动态代理机制。但是在CGLIB中,方法的调用并不是通过反射来完成的,而是直接对方法进行调用:FastClass对Class对象进行特别的处理,比如将会用数组保存method的引用,每次调用方法的时候都是通过一个index下标来保持对方法的引用。比如下面的getIndex方法就是通过方法签名来获得方法在存储了Class信息的数组中的下标。

图2.4 getIndex方法

图2.5 FastClassInfo类中持有两个FastClass对象的引用.png

以我们上面的sayHello方法为例,f1指向委托类对象,f2指向代理类对象,i1和i2分别代表着sayHello方法以及CGLIB$sayHello$0方法在对象信息数组中的下标。

到此为止CGLIB动态代理机制就介绍完了,下面给出三种代理方式之间对比。

代理方式实现优点缺点特点
JDK静态代理代理类与委托类实现同一接口,并且在代理类中需要硬编码接口实现简单,容易理解代理类需要硬编码接口,在实际应用中可能会导致重复编码,浪费存储空间并且效率很低好像没啥特点
JDK动态代理代理类与委托类实现同一接口,主要是通过代理类实现InvocationHandler并重写invoke方法来进行动态代理的,在invoke方法中将对方法进行增强处理不需要硬编码接口,代码复用率高只能够代理实现了接口的委托类底层使用反射机制进行方法的调用
CGLIB动态代理代理类将委托类作为自己的父类并为其中的非final委托方法创建两个方法,一个是与委托方法签名相同的方法,它在方法中会通过super调用委托方法;另一个是代理类独有的方法。在代理方法中,它会判断是否存在实现了MethodInterceptor接口的对象,若存在则将调用intercept方法对委托方法进行代理可以在运行时对类或者是接口进行增强操作,且委托类无需实现接口不能对final类以及final方法进行代理底层将方法全部存入一个数组中,通过数组索引直接进行方法调用

图片描述

微信公众号【黄小斜】作者是蚂蚁金服 JAVA 工程师,专注于 JAVA
后端技术栈:SpringBoot、SSM全家桶、MySQL、分布式、中间件、微服务,同时也懂点投资理财,坚持学习和写作,相信终身学习的力量!关注公众号后回复”架构师“即可领取
Java基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的Java学习指南、Java程序员面试指南等干货资源
查看原文

赞 1 收藏 0 评论 0

石的三次方 关注了用户 · 1月12日

ChiuCheng @chiucheng

Talk is cheap, show me the code!

关注 158

石的三次方 赞了文章 · 1月12日

FutureTask源码解析(2)——深入理解FutureTask

前言

系列文章目录

有了上一篇对预备知识的了解之后,分析源码就容易多了,本篇我们就直接来看看FutureTask的源码。

本文的源码基于JDK1.8。

Future和Task

在深入分析源码之前,我们再来拎一下FutureTask到底是干嘛的。人如其名,FutureTask包含了FutureTask两部分。

我们上一篇说过,FutureTask实现了RunnableFuture接口,即Runnable接口和Future接口。
其中Runnable接口对应了FutureTask名字中的Task,代表FutureTask本质上也是表征了一个任务。而Future接口就对应了FutureTask名字中的Future,表示了我们对于这个任务可以执行某些操作,例如,判断任务是否执行完毕,获取任务的执行结果,取消任务的执行等。

所以简单来说,FutureTask本质上就是一个“Task”,我们可以把它当做简单的Runnable对象来使用。但是它又同时实现了Future接口,因此我们可以对它所代表的“Task”进行额外的控制操作。

Java并发工具类的三板斧

关于Java并发工具类的三板斧,我们在分析AQS源码的时候已经说过了,即:

状态,队列,CAS

以这三个方面为切入点来看源码,有助于我们快速的看清FutureTask的概貌:

状态

首先是找状态。

在FutureTask中,状态是由state属性来表示的,不出所料,它是volatile类型的,确保了不同线程对它修改的可见性:

private volatile int state;
private static final int NEW          = 0;
private static final int COMPLETING   = 1;
private static final int NORMAL       = 2;
private static final int EXCEPTIONAL  = 3;
private static final int CANCELLED    = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED  = 6;

state属性是贯穿整个FutureTask的最核心的属性,该属性的值代表了任务在运行过程中的状态,随着任务的执行,状态将不断地进行转变,从上面的定义中可以看出,总共有7种状态:包括了1个初始态,2个中间态和4个终止态。

虽说状态有这么多,但是状态的转换路径却只有四种:

state of FutureTask

  • 任务的初始状态都是NEW, 这一点是构造函数保证的,我们后面分析构造函数的时候再讲;
  • 任务的终止状态有4种:

    • NORMAL:任务正常执行完毕
    • EXCEPTIONAL:任务执行过程中发生异常
    • CANCELLED:任务被取消
    • INTERRUPTED:任务被中断
  • 任务的中间状态有2种:

    • COMPLETING 正在设置任务结果
    • INTERRUPTING 正在中断运行任务的线程

值得一提的是,任务的中间状态是一个瞬态,它非常的短暂。而且任务的中间态并不代表任务正在执行,而是任务已经执行完了,正在设置最终的返回结果,所以可以这么说:

只要state不处于 NEW 状态,就说明任务已经执行完毕

注意,这里的执行完毕是指传入的Callable对象的call方法执行完毕,或者抛出了异常。所以这里的COMPLETING的名字显得有点迷惑性,它并不意味着任务正在执行中,而意味着call方法已经执行完毕,正在设置任务执行的结果。

而将一个任务的状态设置成终止态只有三种方法:

  • set
  • setException
  • cancel

我们将在下文的源码解析中分析这三个方法。

队列

接着我们来看队列,在FutureTask中,队列的实现是一个单向链表,它表示所有等待任务执行完毕的线程的集合。我们知道,FutureTask实现了Future接口,可以获取“Task”的执行结果,那么如果获取结果时,任务还没有执行完毕怎么办呢?那么获取结果的线程就会在一个等待队列中挂起,直到任务执行完毕被唤醒。这一点有点类似于我们之前学习的AQS中的sync queue,在下文的分析中,大家可以自己对照它们的异同点。

我们前面说过,在并发编程中使用队列通常是将当前线程包装成某种类型的数据结构扔到等待队列中,我们先来看看队列中的每一个节点是怎么个结构:

static final class WaitNode {
    volatile Thread thread;
    volatile WaitNode next;
    WaitNode() { thread = Thread.currentThread(); }
}

可见,相比于AQS的sync queue所使用的双向链表中的Node,这个WaitNode要简单多了,它只包含了一个记录线程的thread属性和指向下一个节点的next属性。

值得一提的是,FutureTask中的这个单向链表是当做来使用的,确切来说是当做Treiber栈来使用的,不了解Treiber栈是个啥的可以简单的把它当做是一个线程安全的栈,它使用CAS来完成入栈出栈操作(想进一步了解的话可以看这篇文章)。为啥要使用一个线程安全的栈呢,因为同一时刻可能有多个线程都在获取任务的执行结果,如果任务还在执行过程中,则这些线程就要被包装成WaitNode扔到Treiber栈的栈顶,即完成入栈操作,这样就有可能出现多个线程同时入栈的情况,因此需要使用CAS操作保证入栈的线程安全,对于出栈的情况也是同理。

由于FutureTask中的队列本质上是一个Treiber栈,那么使用这个队列就只需要一个指向栈顶节点的指针就行了,在FutureTask中,就是waiters属性:

/** Treiber stack of waiting threads */
private volatile WaitNode waiters;

事实上,它就是整个单向链表的头节点。

综上,FutureTask中所使用的队列的结构如下:
Treiber stack

CAS操作

CAS操作大多数是用来改变状态的,在FutureTask中也不例外。我们一般在静态代码块中初始化需要CAS操作的属性的偏移量:

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long stateOffset;
    private static final long runnerOffset;
    private static final long waitersOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = FutureTask.class;
            stateOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("state"));
            runnerOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("runner"));
            waitersOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("waiters"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

从这个静态代码块中我们也可以看出,CAS操作主要针对3个属性,包括staterunnerwaiters,说明这3个属性基本是会被多个线程同时访问的。其中state属性代表了任务的状态,waiters属性代表了指向栈顶节点的指针,这两个我们上面已经分析过了。runner属性代表了执行FutureTask中的“Task”的线程。为什么需要一个属性来记录执行任务的线程呢?这是为了中断或者取消任务做准备的,只有知道了执行任务的线程是谁,我们才能去中断它。

定义完属性的偏移量之后,接下来就是CAS操作本身了。在FutureTask,CAS操作最终调用的还是Unsafe类的compareAndSwapXXX方法,关于这一点,我们上一篇预备知识中已经讲过了,这里不再赘述。

核心属性

前面我们以java并发编程工具类的“三板斧”为切入点分析了FutureTask的状态,队列和CAS操作,对这个工具类有了初步的认识。接下来,我们就要开始进入源码分析了。首先我们先来看看FutureTask的几个核心属性:

    /**
     * The run state of this task, initially NEW.  The run state
     * transitions to a terminal state only in methods set,
     * setException, and cancel.  During completion, state may take on
     * transient values of COMPLETING (while outcome is being set) or
     * INTERRUPTING (only while interrupting the runner to satisfy a
     * cancel(true)). Transitions from these intermediate to final
     * states use cheaper ordered/lazy writes because values are unique
     * and cannot be further modified.
     *
     * Possible state transitions:
     * NEW -> COMPLETING -> NORMAL
     * NEW -> COMPLETING -> EXCEPTIONAL
     * NEW -> CANCELLED
     * NEW -> INTERRUPTING -> INTERRUPTED
     */
    private volatile int state;
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;

    /** The underlying callable; nulled out after running */
    private Callable<V> callable;
    /** The result to return or exception to throw from get() */
    private Object outcome; // non-volatile, protected by state reads/writes
    /** The thread running the callable; CASed during run() */
    private volatile Thread runner;
    /** Treiber stack of waiting threads */
    private volatile WaitNode waiters;

可以看出,FutureTask的核心属性只有5个:

  • state
  • callable
  • outcome
  • runner
  • waiters

关于 statewaitersrunner三个属性我们上面已经解释过了。剩下的callable属性代表了要执行的任务本身,即FutureTask中的“Task”部分,为Callable类型,这里之所以用Callable而不用Runnable是因为FutureTask实现了Future接口,需要获取任务的执行结果。outcome属性代表了任务的执行结果或者抛出的异常,为Object类型,也就是说outcome可以是任意类型的对象,所以当我们将正常的执行结果返回给调用者时,需要进行强制类型转换,返回由Callable定义的V类型。这5个属性综合起来就完成了整个FutureTask的工作,使用关系如下:

  • 任务本尊:callable
  • 任务的执行者:runner
  • 任务的结果:outcome
  • 获取任务的结果:state + outcome + waiters
  • 中断或者取消任务:state + runner + waiters

构造函数

介绍完核心属性之后,我们来看看FutureTask的构造函数:

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}
public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

FutureTask共有2个构造函数,这2个构造函数一个是直接传入Callable对象, 一个是传入一个Runnable对象和一个指定的result, 然后通过Executors工具类将它适配成callable对象, 所以这两个构造函数的本质是一样的:

  1. 用传入的参数初始化callable成员变量
  2. 将FutureTask的状态设为NEW

(关于将Runnable对象适配成Callable对象的方法Executors.callable(runnable, result)我们在上一篇预备知识中已经讲过了,不记得的同学可以倒回去再看一下)

接口实现

上一篇我们提过,FutureTask实现了RunnableFuture接口:

public class FutureTask<V> implements RunnableFuture<V> {
    ...
}

因此,它必须实现Runnable和Future接口的所有方法。

Runnable接口实现

要实现Runnable接口, 就得覆写run方法, 我们看看FutureTask的run方法干了点啥:

public void run() {
    if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

首先我们看到,在run方法的一开始,就检查当前状态是不是New, 并且使用CAS操作将runner属性设置位当前线程,即记录执行任务的线程。compareAndSwapObject的用法在上一篇预备知识中已经介绍过了,这里不再赘述。可见,runner属性是在运行时被初始化的。

接下来,我们就调用Callable对象的call方法来执行任务,如果任务执行成功,就使用set(result)设置结果,否则,用setException(ex)设置抛出的异常。

我们先来看看set(result)方法:

protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

这个方法一开始通过CAS操作将state属性由原来的NEW状态修改为COMPLETING状态,我们在一开始介绍state状态的时候说过,COMPLETING是一个非常短暂的中间态,表示正在设置执行的结果。

状态设置成功后,我们就把任务执行结果赋值给outcome, 然后直接把state状态设置成NORMAL,注意,这里是直接设置,没有先比较再设置的操作,由于state属性被设置成volatile, 结合我们上一篇预备知识的介绍,这里putOrderedInt应当和putIntVolatile是等价的,保证了state状态对其他线程的可见性。

在这之后,我们调用了 finishCompletion()来完成执行结果的设置。

接下来我们再来看看发生了异常的版本setException(ex)

protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = t;
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        finishCompletion();
    }
}

可见,除了将outcome属性赋值为异常对象,以及将state的终止状态修改为EXCEPTIONAL,其余都和set方法类似。在方法的最后,都调用了 finishCompletion()来完成执行结果的设置。那么我们就来看看 finishCompletion()干了点啥:

private void finishCompletion() {
    // assert state > COMPLETING;
    for (WaitNode q; (q = waiters) != null;) {
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            for (;;) {
                Thread t = q.thread;
                if (t != null) {
                    q.thread = null;
                    LockSupport.unpark(t);
                }
                WaitNode next = q.next;
                if (next == null)
                    break;
                q.next = null; // unlink to help gc
                q = next;
            }
            break;
        }
    }

    done();

    callable = null;        // to reduce footprint
}

这个方法事实上完成了一个“善后”工作。我们先来看看if条件语句中的CAS操作:

UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)

该方法是将waiters属性的值由原值设置为null, 我们知道,waiters属性指向了Treiber栈的栈顶节点,可以说是代表了整个Treiber栈,将该值设为null的目的就是清空整个栈。如果设置不成功,则if语句块不会被执行,又进行下一轮for循环,而下一轮for循环的判断条件又是waiters!=null ,由此我们知道,虽然最外层的for循环乍一看好像是什么遍历节点的操作,其实只是为了确保waiters属性被成功设置成null,本质上相当于一个自旋操作。

将waiters属性设置成null以后,接下了 for (;;)死循环才是真正的遍历节点,可以看出,循环内部就是一个普通的遍历链表的操作,我们前面讲属性的时候说过,Treiber栈里面存放的WaitNode代表了当前等待任务执行结束的线程,这个循环的作用也正是遍历链表中所有等待的线程,并唤醒他们。

将Treiber栈中所有挂起的线程都唤醒后,下面就是执行done方法:

/**
 * Protected method invoked when this task transitions to state
 * {@code isDone} (whether normally or via cancellation). The
 * default implementation does nothing.  Subclasses may override
 * this method to invoke completion callbacks or perform
 * bookkeeping. Note that you can query status inside the
 * implementation of this method to determine whether this task
 * has been cancelled.
 */
protected void done() { }

这个方法是一个空方法,从注释上看,它是提供给子类覆写的,以实现一些任务执行结束前的额外操作。

done方法之后就是callable属性的清理了(callable = null)。

至此,整个run方法分析完了。

真的吗???

并没有!别忘了run方法最后还有一个finally块呢:

finally {
    // runner must be non-null until state is settled to
    // prevent concurrent calls to run()
    runner = null;
    // state must be re-read after nulling runner to prevent
    // leaked interrupts
    int s = state;
    if (s >= INTERRUPTING)
        handlePossibleCancellationInterrupt(s);
}

在finally块中,我们将runner属性置为null,并且检查有没有遗漏的中断,如果发现s >= INTERRUPTING, 说明执行任务的线程有可能被中断了,因为s >= INTERRUPTING 只有两种可能,state状态为INTERRUPTINGINTERRUPTED

有的同学可能就要问了,咱前面已经执行过的set方法或者setException方法不是已经将state状态设置成NORMAL或者EXCEPTIONAL了吗?怎么会出现INTERRUPTING或者INTERRUPTED状态呢?别忘了,咱们在多线程的环境中,在当前线程执行run方法的同时,有可能其他线程取消了任务的执行,此时其他线程就可能对state状态进行改写,这也就是我们在设置终止状态的时候用putOrderedInt方法,而没有用CAS操作的原因——我们无法确信在设置state前是处于COMPLETING中间态还是INTERRUPTING中间态。

关于任务取消的操作,我们后面讲Future接口的实现的时候再讲,回到现在的问题,我们来看看handlePossibleCancellationInterrupt方法干了点啥:

/**
 * Ensures that any interrupt from a possible cancel(true) is only
 * delivered to a task while in run or runAndReset.
 */
private void handlePossibleCancellationInterrupt(int s) {
    // It is possible for our interrupter to stall before getting a
    // chance to interrupt us.  Let's spin-wait patiently.
    if (s == INTERRUPTING)
        while (state == INTERRUPTING)
            Thread.yield(); // wait out pending interrupt
}

可见该方法是一个自旋操作,如果当前的state状态是INTERRUPTING,我们在原地自旋,直到state状态转换成终止态。

至此,run方法的分析就真的结束了。我们来总结一下:

run方法重点做了以下几件事:

  1. 将runner属性设置成当前正在执行run方法的线程
  2. 调用callable成员变量的call方法来执行任务
  3. 设置执行结果outcome, 如果执行成功, 则outcome保存的就是执行结果;如果执行过程中发生了异常, 则outcome中保存的就是异常,设置结果之前,先将state状态设为中间态
  4. 对outcome的赋值完成后,设置state状态为终止态(NORMAL或者EXCEPTIONAL)
  5. 唤醒Treiber栈中所有等待的线程
  6. 善后清理(waiters, callable,runner设为null)
  7. 检查是否有遗漏的中断,如果有,等待中断状态完成。

这里再插一句,我们前面说“state只要不是NEW状态,就说明任务已经执行完成了”就体现在这里,因为run方法中,我们是在c.call()执行完毕或者抛出了异常之后才开始设置中间态和终止态的。

Future接口的实现

Future接口一共定义了5个方法,我们一个个来看:

cancel(boolean mayInterruptIfRunning)

既然上面在分析run方法的最后,我们提到了任务可能被别的线程取消,那我们就趁热打铁,看看怎么取消一个任务的执行:

public boolean cancel(boolean mayInterruptIfRunning) {
    if (!(state == NEW && UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {    // in case call to interrupt throws exception
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    t.interrupt();
            } finally { // final state
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        finishCompletion();
    }
    return true;
}

还记得我们上一篇在介绍Future接口的时候对cancel方法的说明吗?

关于cancel方法,这里要补充说几点:
首先有以下三种情况之一的,cancel操作一定是失败的:

  1. 任务已经执行完成了
  2. 任务已经被取消过了
  3. 任务因为某种原因不能被取消

其它情况下,cancel操作将返回true。值得注意的是,cancel操作返回true并不代表任务真的就是被取消了,这取决于发动cancel状态时,任务所处的状态:

  1. 如果发起cancel时任务还没有开始运行,则随后任务就不会被执行;
  2. 如果发起cancel时任务已经在运行了,则这时就需要看mayInterruptIfRunning参数了:

    • 如果mayInterruptIfRunning 为true, 则当前在执行的任务会被中断
    • 如果mayInterruptIfRunning 为false, 则可以允许正在执行的任务继续运行,直到它执行完

我们来看看FutureTask是怎么实现cancel方法的这几个规范的:

首先,对于“任务已经执行完成了或者任务已经被取消过了,则cancel操作一定是失败的(返回false)”这两条,是通过简单的判断state值是否为NEW实现的,因为我们前面说过了,只要state不为NEW,说明任务已经执行完毕了。从代码中可以看出,只要state不为NEW,则直接返回false。

如果state还是NEW状态,我们再往下看:

UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED)

这一段是根据mayInterruptIfRunning的值将state的状态由NEW设置成INTERRUPTING或者CANCELLED,当这一操作也成功之后,就可以执行后面的try语句了,但无论怎么,该方法最后都返回了true

我们再接着看try块干了点啥:

try {    // in case call to interrupt throws exception
    if (mayInterruptIfRunning) {
        try {
            Thread t = runner;
            if (t != null)
                t.interrupt();
        } finally { // final state
            UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
        }
    }
} finally {
    finishCompletion();
}

我们知道,runner属性中存放的是当前正在执行任务的线程,因此,这个try块的目的就是中断当前正在执行任务的线程,最后将state的状态设为INTERRUPTED,当然,中断操作完成后,还需要通过finishCompletion()来唤醒所有在Treiber栈中等待的线程。

我们现在总结一下,cancel方法实际上完成以下两种状态转换之一:

  1. NEW -> CANCELLED (对应于mayInterruptIfRunning=false)
  2. NEW -> INTERRUPTING -> INTERRUPTED (对应于mayInterruptIfRunning=true)

对于第一条路径,虽说cancel方法最终返回了true,但它只是简单的把state状态设为CANCELLED,并不会中断线程的执行。但是这样带来的后果是,任务即使执行完毕了,也无法设置任务的执行结果,因为前面分析run方法的时候我们知道,设置任务结果有一个中间态,而这个中间态的设置,是以当前state状态为NEW为前提的。

对于第二条路径,则会中断执行任务的线程,我们在倒回上面的run方法看看:

public void run() {
    if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

虽然第二条路径中断了当前正在执行的线程,但是,响不响应这个中断是由执行任务的线程自己决定的,更具体的说,这取决于c.call()方法内部是否对中断进行了响应,是否将中断异常抛出。

那call方法中是怎么处理中断的呢?从上面的代码中可以看出,catch语句处理了所有的Throwable的异常,这自然也包括了中断异常。

然而,值得一提的是,即使这里进入了catch (Throwable ex){}代码块,setException(ex)的操作一定是失败的,因为在我们取消任务执行的线程中,我们已经先把state状态设为INTERRUPTING了,而setException(ex)的操作要求设置前线程的状态为NEW。所以这里响应cancel方法所造成的中断最大的意义不是为了对中断进行处理,而是简单的停止任务线程的执行,节省CPU资源。

那读者可能会问了,既然这个setException(ex)的操作一定是失败的,那放在这里有什么用呢?事实上,这个setException(ex)是用来处理任务自己在正常执行过程中产生的异常的,在我们没有主动去cancel任务时,任务的state状态在执行过程中就会始终是NEW,如果任务此时自己发生了异常,则这个异常就会被setException(ex)方法成功的记录到outcome中。

反正无论如何,run方法最终都会进入finally块,而这时候它会发现s >= INTERRUPTING,如果检测发现s = INTERRUPTING,说明cancel方法还没有执行到中断当前线程的地方,那就等待它将state状态设置成INTERRUPTED。到这里,对cancel方法的分析就和上面对run方法的分析对接上了。

cancel方法到这里就分析完了,如果你一条条的去对照Future接口对于cancel方法的规范,它每一条都是实现了的,而它实现的核心机理,就是对state的当前状态的判断和设置。由此可见,state属性是贯穿整个FutureTask的最核心的属性。

isCancelled()

说完了cancel,我们再来看看 isCancelled()方法,相较而言,它就简单多了:

public boolean isCancelled() {
    return state >= CANCELLED;
}

那么state >= CANCELLED 包含了那些状态呢,它包括了: CANCELLEDINTERRUPTINGINTERRUPTED

我们再来回忆下上一篇讲的Future接口对于isCancelled()方法的规范:

该方法用于判断任务是否被取消了。如果一个任务在正常执行完成之前被Cancel掉了, 则返回true

再对比state的状态图:

isCancelled
可见选取这三个状态作为判断依据是很合理的, 因为只有调用了cancel方法,才会使state状态进入这三种状态。

isDone()

与 isCancelled方法类似,isDone方法也是简单地通过state状态来判断。

public boolean isDone() {
    return state != NEW;
}

关于这一点,其实我们之前已经说过了,只要state状态不是NEW,则任务已经执行完毕了,因为state状态不存在类似“任务正在执行中”这种状态,即使是短暂的中间态,也是发生在任务已经执行完毕,正在设置任务结果的时候。

get()

最后我们来看看获取执行结果的get方法,先来看看无参的版本:

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

该方法其实很简单,当任务还没有执行完毕或者正在设置执行结果时,我们就使用awaitDone方法等待任务进入终止态,注意,awaitDone的返回值是任务的状态,而不是任务的结果。任务进入终止态之后,我们就根据任务的执行结果来返回计算结果或者抛出异常。

我们先来看看等待任务完成的awaitDone方法,该方法是获取任务结果最核心的方法,它完成了获取结果,挂起线程,响应中断等诸多操作:

private int awaitDone(boolean timed, long nanos) throws InterruptedException {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }
        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            q = new WaitNode();
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                 q.next = waiters, q);
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        else
            LockSupport.park(this);
    }
}

在具体分析它的源码之前,有一点我们先特别说明一下,FutureTask中会涉及到两类线程,一类是执行任务的线程,它只有一个,FutureTask的run方法就由该线程来执行;一类是获取任务执行结果的线程,它可以有多个,这些线程可以并发执行,每一个线程都是独立的,都可以调用get方法来获取任务的执行结果。如果任务还没有执行完,则这些线程就需要进入Treiber栈中挂起,直到任务执行结束,或者等待的线程自身被中断。

理清了这一点后,我们再来详细看看awaitDone方法。可以看出,该方法的大框架是一个自旋操作,我们一段一段来看:

for (;;) {
    if (Thread.interrupted()) {
        removeWaiter(q);
        throw new InterruptedException();
    }
    // ...
}

首先一开始,我们先检测当前线程是否被中断了,这是因为get方法是阻塞式的,如果等待的任务还没有执行完,则调用get方法的线程会被扔到Treiber栈中挂起等待,直到任务执行完毕。但是,如果任务迟迟没有执行完毕,则我们也有可能直接中断在Treiber栈中的线程,以停止等待。

当检测到线程被中断后,我们调用了removeWaiter:

private void removeWaiter(WaitNode node) {
    if (node != null) {
        ...
    }
}

removeWaiter的作用是将参数中的node从等待队列(即Treiber栈)中移除。如果此时线程还没有进入Treiber栈,则 q=null,那么removeWaiter(q)啥也不干。在这之后,我们就直接抛出了InterruptedException异常。

接着往下看:

for (;;) {
    /*if (Thread.interrupted()) {
        removeWaiter(q);
        throw new InterruptedException();
    }*/
    int s = state;
    if (s > COMPLETING) {
        if (q != null)
            q.thread = null;
        return s;
    }
    else if (s == COMPLETING) // cannot time out yet
        Thread.yield();
    else if (q == null)
        q = new WaitNode();
    else if (!queued)
        queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                             q.next = waiters, q);
    else if (timed) {
        nanos = deadline - System.nanoTime();
        if (nanos <= 0L) {
            removeWaiter(q);
            return state;
        }
        LockSupport.parkNanos(this, nanos);
    }
    else
        LockSupport.park(this);
}
  • 如果任务已经进入终止态(s > COMPLETING),我们就直接返回任务的状态;
  • 否则,如果任务正在设置执行结果(s == COMPLETING),我们就让出当前线程的CPU资源继续等待
  • 否则,就说明任务还没有执行,或者任务正在执行过程中,那么这时,如果q现在还为null, 说明当前线程还没有进入等待队列,于是我们新建了一个WaitNode, WaitNode的构造函数我们之前已经看过了,就是生成了一个记录了当前线程的节点;
  • 如果q不为null,说明代表当前线程的WaitNode已经被创建出来了,则接下来如果queued=false,表示当前线程还没有入队,所以我们执行了:
queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q);

这行代码的作用是通过CAS操作将新建的q节点添加到waiters链表的头节点之前,其实就是Treiber栈的入栈操作,写的还是很简洁的,一行代码就搞定了,如果大家还是觉得晕乎,下面是它等价的伪代码:

q.next = waiters; //当前节点的next指向目前的栈顶元素
//如果栈顶节点在这个过程中没有变,即没有发生并发入栈的情况
if(waiters的值还是上面q.next所使用的waiters值){ 
    waiters = q; //修改栈顶的指针,指向刚刚入栈的节点
}

这个CAS操作就是为了保证同一时刻如果有多个线程在同时入栈,则只有一个能够操作成功,也即Treiber栈的规范。

如果以上的条件都不满足,则再接下来因为现在是不带超时机制的get,timed为false,则else if代码块跳过,然后来到最后一个else, 把当前线程挂起,此时线程就处于阻塞等待的状态。

至此,在任务没有执行完毕的情况下,获取任务执行结果的线程就会在Treiber栈中被LockSupport.park(this)挂起了。

那么这个挂起的线程什么时候会被唤醒呢?有两种情况:

  1. 任务执行完毕了,在finishCompletion方法中会唤醒所有在Treiber栈中等待的线程
  2. 等待的线程自身因为被中断等原因而被唤醒。

我们接下来就继续看看线程被唤醒后的情况,此时,线程将回到for(;;)循环的开头,继续下一轮循环:

for (;;) {
    if (Thread.interrupted()) {
        removeWaiter(q);
        throw new InterruptedException();
    }

    int s = state;
    if (s > COMPLETING) {
        if (q != null)
            q.thread = null;
        return s;
    }
    else if (s == COMPLETING) // cannot time out yet
        Thread.yield();
    else if (q == null)
        q = new WaitNode();
    else if (!queued)
        queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                             q.next = waiters, q);
    else if (timed) {
        nanos = deadline - System.nanoTime();
        if (nanos <= 0L) {
            removeWaiter(q);
            return state;
        }
        LockSupport.parkNanos(this, nanos);
    }
    else
        LockSupport.park(this); // 挂起的线程从这里被唤醒
}

首先自然还是检测中断,所不同的是,此时q已经不为null了,因此在有中断发生的情况下,在抛出中断之前,多了一步removeWaiter(q)操作,该操作是将当前线程从等待的Treiber栈中移除,相比入栈操作,这个出栈操作要复杂一点,这取决于节点是否位于栈顶。下面我们来仔细分析这个出栈操作:

private void removeWaiter(WaitNode node) {
    if (node != null) {
        node.thread = null;
        retry:
        for (;;) {          // restart on removeWaiter race
            for (WaitNode pred = null, q = waiters, s; q != null; q = s) {
                s = q.next;
                if (q.thread != null)
                    pred = q;
                else if (pred != null) {
                    pred.next = s;
                    if (pred.thread == null) // check for race
                        continue retry;
                }
                else if (!UNSAFE.compareAndSwapObject(this, waitersOffset, q, s))
                    continue retry;
            }
            break;
        }
    }
}

首先,我们把要出栈的WaitNode的thread属性设置为null, 这相当于一个标记,是我们后面在waiters链表中定位该节点的依据。

(1) 要移除的节点就在栈顶

我们先来看看该节点就位于栈顶的情况,这说明在该节点入栈后,并没有别的线程再入栈了。由于一开始我们就将该节点的thread属性设为了null,因此,前面的q.thread != nullpred != null都不满足,我们直接进入到最后一个else if 分支:

else if (!UNSAFE.compareAndSwapObject(this, waitersOffset, q, s))
    continue retry;

这一段是栈顶节点出栈的操作,和入栈类似,采用了CAS比较,将栈顶元素设置成原栈顶节点的下一个节点。

值得注意的是,当CAS操作不成功时,程序会回到retry处重来,但即使CAS操作成功了,程序依旧会遍历完整个链表,找寻node.thread == null 的节点,并将它们一并从链表中剔除。

(2) 要移除的节点不在栈顶

当要移除的节点不在栈顶时,我们会一直遍历整个链表,直到找到q.thread == null的节点,找到之后,我们将进入

else if (pred != null) {
    pred.next = s;
    if (pred.thread == null) // check for race
        continue retry;
}

这是因为节点不在栈顶,则其必然是有前驱节点pred的,这时,我们只是简单的让前驱节点指向当前节点的下一个节点,从而将目标节点从链表中剔除。

注意,后面多加的那个if判断是很有必要的,因为removeWaiter方法并没有加锁,所以可能有多个线程在同时执行,WaitNode的两个成员变量threadnext都被设置成volatile,这保证了它们的可见性,如果我们在这时发现了pred.thread == null,那就意味着它已经被另一个线程标记了,将在另一个线程中被拿出waiters链表,而我们当前目标节点的原后继节点现在是接在这个pred节点上的,因此,如果pred已经被其他线程标记为要拿出去的节点,我们现在这个线程再继续往后遍历就没有什么意义了,所以这时就调到retry处,从头再遍历。

如果pred节点没有被其他线程标记,那我们就接着往下遍历,直到整个链表遍历完。

至此,将节点从waiters链表中移除的removeWaiter操作我们就分析完了,我们总结一下该方法:

在该方法中,会传入一个需要移除的节点,我们会将这个节点的thread属性设置成null,以标记该节点。然后无论如何,我们会遍历整个链表,清除那些被标记的节点(只是简单的将节点从链表中剔除)。如果要清除的节点就位于栈顶,则还需要注意重新设置waiters的值,指向新的栈顶节点。所以可以看出,虽说removeWaiter方法传入了需要剔除的节点,但是事实上它可能剔除的不止是传入的节点,而是所有已经被标记了的节点,这样不仅清除操作容易了些(不需要专门去定位传入的node在哪里),而且提升了效率(可以同时清除所有已经被标记的节点)。

我们再回到awaitDone方法里:

private int awaitDone(boolean timed, long nanos) throws InterruptedException {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
        if (Thread.interrupted()) {
            removeWaiter(q); // 刚刚分析到这里了,我们接着往下看
            throw new InterruptedException();
        }

        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            q = new WaitNode();
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                 q.next = waiters, q);
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        else
            LockSupport.park(this);
    }
}

如果线程不是因为中断被唤醒,则会继续往下执行,此时会再次获取当前的state状态。所不同的是,此时q已经不为null, queued已经为true了,所以已经不需要将当前节点再入waiters栈了。

至此我们知道,除非被中断,否则get方法会在原地自旋等待(用的是Thread.yield,对应于s == COMPLETING)或者直接挂起(对应任务还没有执行完的情况),直到任务执行完成。而我们前面分析run方法和cancel方法的时候知道,在run方法结束后,或者cancel方法取消完成后,都会调用finishCompletion()来唤醒挂起的线程,使它们得以进入下一轮循环,获取任务执行结果。

最后,等awaitDone函数返回后,get方法返回了report(s),以根据任务的状态,汇报执行结果:

@SuppressWarnings("unchecked")
private V report(int s) throws ExecutionException {
    Object x = outcome;
    if (s == NORMAL)
        return (V)x;
    if (s >= CANCELLED)
        throw new CancellationException();
    throw new ExecutionException((Throwable)x);
}

可见,report方法非常简单,它根据当前state状态,返回正常执行的结果,或者抛出指定的异常。

至此,get方法就分析结束了。

值得注意的是,awaitDone方法和get方法都没有加锁,这在多个线程同时执行get方法的时候会不会产生线程安全问题呢?通过查看方法内部的参数我们知道,整个方法内部用的大多数是局部变量,因此不会产生线程安全问题,对于全局的共享变量waiters的修改时,也使用了CAS操作,保证了线程安全,而state变量本身是volatile的,保证了读取时的可见性,因此整个方法调用虽然没有加锁,它仍然是线程安全的。

get(long timeout, TimeUnit unit)

最后我们来看看带超时版本的get方法:

public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
    if (unit == null)
        throw new NullPointerException();
    int s = state;
    if (s <= COMPLETING && (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
        throw new TimeoutException();
    return report(s);
}

它和上面不带超时时间的get方法很类似,只是在awaitDone方法中多了超时检测:

else if (timed) {
    nanos = deadline - System.nanoTime();
    if (nanos <= 0L) {
        removeWaiter(q);
        return state;
    }
    LockSupport.parkNanos(this, nanos);
}

即,如果指定的超时时间到了,则直接返回,如果返回时,任务还没有进入终止状态,则直接抛出TimeoutException异常,否则就像get()方法一样,正常的返回执行结果。

总结

FutureTask实现了Runnable和Future接口,它表示了一个带有任务状态和任务结果的任务,它的各种操作都是围绕着任务的状态展开的,值得注意的是,在所有的7个任务状态中,只要不是NEW状态,就表示任务已经执行完毕或者不再执行了,并没有表示“任务正在执行中”的状态。

除了代表了任务的Callable对象、代表任务执行结果的outcome属性,FutureTask还包含了一个代表所有等待任务结束的线程的Treiber栈,这一点其实和各种锁的等待队列特别像,即如果拿不到锁,则当前线程就会被扔进等待队列中;这里则是如果任务还没有执行结束,则所有等待任务执行完毕的线程就会被扔进Treiber栈中,直到任务执行完毕了,才会被唤醒。

FutureTask虽然为我们提供了获取任务执行结果的途径,遗憾的是,在获取任务结果时,如果任务还没有执行完成,则当前线程会自旋或者挂起等待,这和我们实现异步的初衷是相违背的,我们后面将继续介绍另一个同步工具类CompletableFuture, 它解决了这个问题。

(完)

系列文章目录

查看原文

赞 16 收藏 6 评论 8

石的三次方 发布了文章 · 2020-11-17

SpringMVC执行流程还不清楚?

MVC总结

1. 概述

还是之前的三个套路

1.1 是什么?

Spring提供一套视图层的处理框架,他基于Servlet实现,可以通过XML或者注解进行我们需要的配置。

他提供了拦截器,文件上传,CORS等服务。

1.2 为什么用?

原生Servlet在大型项目中需要进过多重封装,来避免代码冗余,其次由于不同接口需要的参数不同,我们需要自己在Servlet层 封装我们需要的参数,这对于开发者来说是一种重复且枯燥的工作,于是出现了视图层框架,为我们进行参数封装等功能。让开发者的注意力全部放在逻辑架构中,不需要考虑参数封装等问题。

1.3 怎么用

再聊怎么用之前,我们需要了解一下MVC的工作原理。

他基于一个DispatcherServlet类实现对各种请求的转发,即前端的所有请求都会来到这个Servlet中,然后这个类进行参数封装和请求转发,执行具体的逻辑。(第二章我们细聊)

1.3.1 XML
  • 根据上面的原理,我们需要一个DispatcherServlet来为我们提供基础的Servlet服务,我们可以通过servlet规范的web.xml文件,对该类进行初始化。并且声明该类处理所有的请求,然后通过这个类实现请求转发。
  • 另外,我们还需要一个配置文件,用来配置我们需要的相关的mvc信息。

下面来看一个完整的web.xml配置

<web-app>

    <servlet>
    <servlet-name>dispatchServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:springmvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatchServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

</web-app>
1.3.2 注解

注解方式也是现在主流,SpringBoot基于JavaConfig实现了自动配置

实现方式:

Servlet3.0的时候定义了一个规范SPI规范。

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。也就是在服务启动的时候会Servlet会自动加载该文件定义的类

我们看一眼这个文件里的内容。他内部定义了SpringServletContainerInitializer容器初始化类,也就是说在Servlet启动的时候会自动初始化这个类,这个类也是注解实现的关键。

这个类中存在一个onStartup方法,这个也是当容器初始化的时候调用的方法,这个方法有两参数

  • Set<Class<?>> webAppInitializerClasses他代表了当前我们的Spring容器中存在的web初始化类。我们自己可以通过实现WebApplicationInitializer类来自定义Servlet初始化的时候执行的方法。
  • ServletContext servletContex代表了Servlet上下文对象
org.springframework.web.SpringServletContainerInitializer

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> webAppInitializerClasses,         
 ServletContext servletContext)    throws ServletException {
        //启动逻辑
    }
}

具体看一下注解配置方式:

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletCxt) {

        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        //一个配置类,@Configuration
        ac.register(AppConfig.class);
        //spring的那个refresh方法
        ac.refresh();

        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }
}

通过实现WebApplicationInitializer接口,来作为MVC的配置类,在加载SpringServletContainerInitializer的时候加载这个类。


不过在具体的实现中,Spring不建议我们这样做,他建议将SpringSpringMvc分开,看个图

他在Spring之上加了一层Web环境配置。相当于在Spring的外面包装了一层Servlet

看一下此时的代码

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    
    //Spring配置文件
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };
    }

    //SpringMVC的配置文件
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { App1Config.class };
    }
    
    //指定DispatcherServlet可以拦截的路径
    @Override
    protected String[] getServletMappings() {
        return new String[] { "/app1/*" };
    }
}

通过AbstractAnnotationConfigDispatcherServletInitializer

可以看到他实现了WebApplicationInitializer接口,即在Servlet初始化的时候会加载这个类。

AbstractContextLoaderInitializer类,他初始化了Spring

AbstractDispatcherServletInitializer类,初始化了DispatcherServlet

AbstractAnnotationConfigDispatcherServletInitializer类,将两个类整合到一起

2. 实现原理

聊这个原理之前,先来聊聊他要干什么?

需求:请求分发;参数封装;结果返回

那如果我们自己来实现,该怎么办?(单说注解,先来看看我们怎么使用MVC)

  • 一个@Controller注解,标识当前类为控制层接口,
  • 一个RequestMapping标识这个方法的URI和请求方式等信息
  • 一个@ResponseBody标识这个方法的返回类型为JSON
  • 一个test01标识这个方法用来处理/test请求
@Controller
public class UserController {

    @GetMapping("/test")
    @ResponseBody
    public String test01(){
        return "success" ;

    }
}

接下来,我们通过我们已有的东西,看一下我们自己去处理请求的逻辑

先来想一下我们的请求过程:

  • 前端发送一个Http请求,通过不同的uri实现不同逻辑的处理
  • 而这个uri和我们后端的定义的@RequestMapping中的value值相同
  • 即我们可以通过一个Map结构,将value作为key,将methodClass对象作为一个value存到一个MappingRegister
  • 请求来了以后,通过URI从这个Map中获取相应的Method执行,如果没有对应的Method给一个404.

2.1 Spring加载

在上面的怎么用中提到了,他通过AbstractContextLoaderInitializer来加载Spring配置文件的。

此时关于Spring的东西已经加载好了,但并未进行初始化

2.2 MVC加载

同样也是通过AbstractDispatcherServletInitializer类实现

2.2.1 DispatcherServlet

接下来我们具体看一下在这个期间,DispatcherServlet如何处理请求的

作用:分发所有的请求

类继承结构图

可以看到他继承了HttpServlet类,属于一个Servlet,而在之前我们配置了这个Servlet的拦截路径。他会将所有的请求拦截,然后做一个分发。

下面这个图各位看官应该非常熟悉:

其实DispatcherServlet处理所有请求的方式在这个图里完全都体现了。

接下来聊一下他的设计思路吧。

当一个请求来的时候,进入doDispatch方法中,然后处理这个请求,也是返回一个执行链

Spring提供了三种方式的处理器映射器来处理不同的请求。

  • BeanNameUrlHandlerMapping处理单独Bean的请求。适用于实现ControllerHttpRequestHandler接口的类
@Component("/test02")
public class HttpController  implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("HttpController执行");
        return null;
    }
}
@Component("/test01")
public class HandlerController implements HttpRequestHandler {

    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("handlerRequest");
    }
}
  • RequestMappingHandlerMapping适用于方法类型的处理器映射。
@Controller
public class UserController {

    @GetMapping("/test")
    public String test01(){
        System.out.println("执行了");
        return "success" ;
    }
}
  • RouterFunctionMapping,MVC提供的一个处理通过函数式编程定义控制器的一个映射器处理器。需要直接添加到容器中,然后 通过路由一个地址,返回对应的数据
@Configuration
@ComponentScan("com.bywlstudio.controller")
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp("/WEB-INF/pages/",".jsp");
    }

    @Bean
    public RouterFunction<?> routerFunctionA() {
        return RouterFunctions.route()
                .GET("/person/{id}", request1 -> ServerResponse.ok().body("Hello World"))
                .build();
    }

}

聊完了处理器映射器,再来聊一下处理器适配器

不同的请求方式,需要不同的处理方式,这也是Spring为什么要提供一个适配器的原因。

  • RequestMappingHandlerAdapter用来处理所有的方法请求,即通过@Controller注解定义的
  • HandlerFunctionAdapter用来处理函数式的映射,即通过RouterFunctionMapping定义的
  • HttpRequestHandlerAdapter用来处理实现了HttpRequestHandler接口的
  • SimpleControllerHandlerAdapter用来处理实现了Controller接口的请求

通过处理器适配器拿到适合的处理器,来处理对应的请求。

在处理器执行具体的请求的过程,实际上就是调用我们的方法的过程,于是就会出现返回值

通常对于返回值我们有两种方法:

  • @ResponseBody直接返回JSON数据。
  • 或者返回一个视图,该视图会被视图解析器解析。

对于返回值解析,MVC提供了一个接口用于处理所有的返回值,这里我们仅仅谈上面的两种

  • ModelAndViewMethodReturnValueHandler用于处理返回视图模型的请求
  • RequestResponseBodyMethodProcessor用于处理返回JSON

在我们拿到方法返回值以后,会调用this.returnValueHandlers.handleReturnValue返回值解析器的这个方法,用于对视图模型的返回和JSON数据的回显(直接回显到网页,此时返回的视图对象为null

对于视图对象,通过视图解析器直接解析,进行数据模型渲染,然后回显给前端。

2.2.2 MappingRegistry

这个类存放了method的映射信息。

class MappingRegistry {

   private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

   private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();

   private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();

   private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

   private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

   private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

MVC会从这个类中获取方法和URL的引用。相当于Spring MVC的容器。

3. 面试题

3.1 什么是MVC?什么是MVVM?

答:MVC是一个架构模式,它有三个核心

  • 视图(View)。用户界面
  • 模型(Model)。业务数据
  • 控制器(Controller)。接收用户输入,控制模型和视图进行数据交互

MVVM也是一种架构模式,它也是三个核心

  • 模型(Model)。后端数据
  • 视图模型(ViewModel)。它完成了数据和视图的绑定
  • 视图(View)。用户界面

它的核心思想是:通过ViewModel将数据和视图绑定,用数据操作视图,常见框架为Vue

3.2 Spring Mvc执行流程

  • 用户发送请求至DispatcherServlet
  • DispatcherServelt收到请求以后调用HandlerMapping,找到请求处理器映射器(三选一
  • 通过处理器映射器对应URI的处理器执行链(包含了拦截器,和处理器对象)
  • 调用处理器适配器,找到可以处理该执行链的处理器(四选一)
  • 处理器具体执行,返回ModelAndView对象

    • 如果存在@ResponseBody注解,直接进行数据回显
  • 将返回的ModelAndView对象传给ViewResove视图解析器解析,返回视图
  • DispatcherServletView进行渲染视图
  • 响应用户
更多原创文章请关注笔者公众号@MakerStack,转载请联系作者授权
查看原文

赞 0 收藏 0 评论 0

石的三次方 发布了文章 · 2020-11-12

一种阅读姿势,品读Lock和Synchronized锁

1.Synchronized锁

底层是monitor监视器,每一个对象再创建的时候都会常见一个monitor监视器,在使用synchronized代码块的时候,会在代码块的前后产生一个monitorEnter和monitorexit指令,来标识这是一个同步代码块。

1.1 执行流程

线程遇到同步代码块,给这个对象monitor对象加1,当线程退出当前代码块以后,给这个对象的monitor对象减一,如果monitor指令的值为0则当前线程释放锁。

1.2 反编译源码

同步代码块反编译

public void test01(){
        synchronized (this){
            int num = 1 ;
        }
    }

两次monitorexit的作用是避免同步代码块无法跳出,因此存在两种,正常退出和异常退出

同步方法反编译

public synchronized  void test01(){
            int num = 1 ;
    }

可以发现其没有在同步方法前后添加monitor指令,但是在其底层实际上也是通过monitor指令实现的,只不过相较于同步代码块来说,他是隐式的。

1.3 锁升级

JDK1.5的时候对于synchronzied做了一系列优化操作,增加了诸如:偏向锁,轻量级锁,自旋锁,锁粗化,重量级锁的概念。

1.3.1 偏向锁

在一个线程在执行获取锁的时候,当前线程会在monitor对象中存储指向该线程的ID。当线程再次进入的时候,不需要通过CAS的方法再来进行加锁或者解锁,而是检测偏向锁的ID是不是当前要进行的线程,如果是,直接进入。

偏向锁,适用于一个线程执行任务的情况

JDK1.6中,默认是开启的。可以通过-XX:-UseBiasedLocking=false参数关闭偏向锁

1.3.2 轻量级锁

轻量级锁是指锁为偏向锁的时候,该锁被其他线程尝试获取,此时偏向锁升级为轻量级锁,其他线程会通过自旋的方式尝试获取锁,线程不会阻塞,从而提供性能

升级为轻量级锁的情况有两种:

  • 关闭偏向锁
  • 有多个线程竞争偏向锁的时候

具体实现:

线程进行代码块以后,如果同步对象锁状态为无锁的状态,虚拟机将首先在当前线程的栈帧中创建一个锁记录的空间。这个空间内存储了当前获取锁的对象。

使用情况:

两个线程的互相访问

1.3.3 重量级锁

在有超过2个线程访问同一把锁的时候,锁自动升级为重量级锁,也就是传统的synchronized,此时其他未获取锁的线程会陷入等待状态,不可被中断。

由于依赖于monitor指令,所以其消耗系统资源比较大

上面的三个阶段就是锁升级的过程

1.3.4 锁粗化

当在一个循环中,我们多次使用对同一个代码进行加锁,这个时候,JVM会自动实现锁粗化,即在循环外进行添加同步代码块。

代码案例:

锁粗化之前:

for (int i = 0; i < 10; i++) {
            synchronized (LockBigDemo.class){
                System.out.println();
            }
        }

锁粗化之后:

synchronized (LockBigDemo.class){
            for (int i = 0; i < 10; i++) {
                    System.out.println();
            }
        }

本次关于synchronized的底层原理没有以代码的方式展开,之后笔者会出一篇synchronized底层原理剖析的文章

2. Lock锁

一个类级别的锁,需要手动释放锁。可以选择性的选择设置为公平锁或者不公平锁。等待线程可以被打断。

底层是基于AQS+AOSAQS类完成具体的加锁逻辑,AOS保存获取锁的线程信息

2.1 ReentrantLock

我们以ReentrantLock为例解析一下其加锁的过程。

2.1.1 lock方法

首先通过ReentrantLock的构造方法的布尔值判断创建的锁是公平锁还是非公平锁。

假设现在创建的是非公平锁,他首先会判断锁有没有被获取,如果没有被获取,则直接获取锁;

如果锁已经被获取,执行一次自旋,尝试获取锁。

如果锁已经被获取,则将当前线程封装为AQS队列的一个节点,然后判断当前节点的前驱节点是不是HEAD节点,如果是,尝试获取锁;如果不是。则寻找一个安全点(线程状态位SIGNAL=-1的节点)。

开始不断自旋。判断前节点是不是HEAD节点,如果是获取锁,如果不是挂起。

源码解读:

  • 非公平锁lock
final void lock() {
    //判断是否存在锁
            if (compareAndSetState(0, 1))
                //获取锁
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
//非公平锁的自旋逻辑
protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
        //获取锁状态
            int c = getState();
        //如果锁没被获取,获取锁
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
    //当前线程已经获取到了锁
            else if (current == getExclusiveOwnerThread()) {
                //线程进入次数增加
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
//将线程封装为一个线程节点,传入锁模式,排他或者共享
private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 获取尾节点
        Node pred = tail;
    //如果尾节点不为Null,直接将这个线程节点添加到队尾
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
    //为空,自旋设置尾节点
        enq(node);
        return node;
    }

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //初始化
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                //将头结点和尾结点都设置为当前节点
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
//尝试入队
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取节点的前驱节点,如果前驱节点为head节点,则尝试获取锁
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //如果不是,寻找安全位
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
    //前驱节点已经安全
        if (ws == Node.SIGNAL)
            return true;
    //前驱节点不安全,寻找一个线程状态为`Signal`的节点作为前驱节点
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //否则直接设置这个前驱节点的线程等待状态值
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

//中断线程
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

2.1.2 unlock方法

代码解读:

public void unlock() {
        sync.release(1);
    }
public final boolean release(int arg) {
    //尝试释放锁
        if (tryRelease(arg)) {
            //获取队列头元素,唤醒该线程节点,执行任务
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
    //判断是否为当前线程拥有锁
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
    //释放成功
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
private void unparkSuccessor(Node node) {
    
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
      
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
    //唤醒下一个节点
        if (s != null)
            LockSupport.unpark(s.thread);
    }
2.1.3 Node节点
/** 共享锁,读锁使用 */
        static final Node SHARED = new Node();
        /** 独占锁*/
        static final Node EXCLUSIVE = null;

        /** 不安全线程 */
        static final int CANCELLED =  1;
        /** 需要进行线程唤醒的线程 */
        static final int SIGNAL    = -1;
        /**condition等待中 */
        static final int CONDITION = -2;

        //线程等待状态
        volatile int waitStatus;

        volatile Node prev;

        volatile Node next;

        volatile Thread thread;
        Node nextWaiter;

3. Lock锁和Synchronized的区别

  • Lock锁是API层面,synchronizedCPU源语级别的
  • Lock锁等待线程可以被中断,synchronized等待线程不可以被中断
  • Lock锁可以指定公平锁和非公平锁,synchronized只能为非公平锁
  • Lock锁需要主动释放锁,synchronized执行完代码块以后自动释放锁
更多原创文章请关注公众号@MakerStack
查看原文

赞 0 收藏 0 评论 0

石的三次方 发布了文章 · 2020-11-10

面试重灾区——JVM内存结构和垃圾回收机制

JVM介绍

1. JVM的体系架构(内存模型)

绿色的为线程私有,橘色的为线程共有

2. 类加载器

负责将.class文件加载到内存中,并且将该文件中的数据结构转换为方法区中的数据结构,生成一个Class对象

2.1 类加载器分类

  • 自启动类加载器。Bootstrap ClassLoader类加载器。负责加载jdk自带的包。

    • %JAVA_HOME%/lib/rt.jar%即JDK源码
    • 使用C++编写
    • 在程序中直接获取被该加载器加载的类的类加载器会出现null
  • 扩展类加载器.Extension ClassLoader。负责加载jdk扩展的包

    • 便于未来扩展
    • %JAVA_HOME/lib/ext/*.jar%
  • 应用类加载器或系统类加载器。AppClassLoader或SystemClassLOader

    • 用于加载自定义类的加载器
    • CLASSPATH路径下
  • 自定义类加载器

    • 通过实现ClassLoader抽象类实现

2.2 双亲委派机制

当应用类加载器获取到一个类加载的请求的时候,不会立即处理这个类加载请求,而是将这个请求委派给他的父加载器加载,如果这个父加载器不能够处理这个类加载请求,便将之传递给子加载器。一级一级传递指导可以加载该类的类加载器。

该机制又称沙盒安全机制。防止开发者对JDK加载做破坏

2.3 打破双亲委派机制

  • 自定义类加载器,重写loadClass方法
  • 使用线程上下文类加载器

2.4 Java虚拟机的入口文件

sun.misc.Launcher

3. Execution Engine

执行引擎负责执行解释命令,交给操作系统进行具体的执行

4. 本地接口

4.1 native方法

native方法指Java层面不能处理的操作,只能通过本地接口调用本地的函数库(C函数库)

4.2 Native Interface

一套调用函数库的接口

5. 本地方法栈

在加载native方法的时候,会将执行的C函数库的方法,放在这个栈区域执行

6. 程序计数器

每个线程都有程序计数器,主要作用是存储代码指令,就类似于一个执行计划。

内部维护了多个指针,这些指针指向了方法区中的方法字节码。执行引擎从程序计数器中获取下一次要执行的指令。

由于空间很小,他是当前线程执行代码的一个行号指示器/

不会引发OOM

7. 方法区

供各线程共享的运行时内存区域,存放了各个类的结构信息(一个Class对象),包括:字段,方法,构造方法,运行时常量池。

虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开

主要有:永久代或者元空间。存在GC

元空间中由于直接使用物理内存的影响,所以默认的最大元空间大小为1/4物理内存大小

8. Java栈

主要负责执行各种方法,是线程私有的,随线程的消亡而消亡,不存在垃圾回收的问题。八大数据类型和实例引用都是在函数的栈内存中分配内存的。

默认大小为512~1024K,通过-Xss1024k参数修改

8.1 栈和队列数据结构

FILO:先进后出

队列FIFO:先进先出

8.2 存储的数据

  • 本地变量Local Variable。包括方法的形参和返回值
  • 栈操作Operand Stack。包括各种压栈和出栈操作
  • 栈帧数据Frame Data。就相当于一个个方法。在栈空间中,方法被称为栈帧

8.3 执行流程

栈中执行的单位是栈帧,栈帧就是一个个方法。

  • 首先将main方法压栈,成为一个栈帧
  • 然后调用其他方法,即再次压栈
  • 栈帧中存储了这个方法的局部变量表,操作数栈、动态链接、方法出口等
  • 栈的大小和JVM的实现有关,通常在256K~756K

9. 方法区,栈,堆的关系

10. Heap 堆

10.1 堆内存结构

默认初始大小为物理内存的1/64,默认最大大小为1/4。在实际生产中一般会将这两个值设置为相同,避免垃圾回收器执行完垃圾回收以后还需要进行空间的扩容计算,浪费资源。

堆外内存:内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。使用未公开的Unsafe和NIO包下ByteBuffer来创建堆外内存。

默认的堆外内存大小为,通过-XX:MaxDirectMemorySize=执行堆外内存的大小

10.1.1 JDK1.7

在逻辑上划分为三个区域:

  • 新生区Young Generation Space

    • 伊甸区Eden Space
    • 幸存区Survivor 0 Space
    • 幸存区Survivor 1 Space
  • 养老区Tenure Generation Space
  • 永久区Permanent Space(方法区)

在物理层面划分为两个区域:

  • 新生区
  • 老年区
10.1.1.1 堆内存GC过程

主要流程有三步:

  • Eden满了以后出发一次轻GC(Minor GC),没有死亡的对象,年龄+1,存放到from区域
  • Eden再次满了以后再次触发一次GC,没有死亡的对象放置于to区域,然后将from区域中没有死亡的对象全部置于to区域,年龄+1。之后每一次GC都会出发一次fromto的交换,哪个区域是空的那个区域就是to
  • survivor区域满了以后,再次触发GC,当存在对象的年龄等于15的时候,就会将该对象移入老年区

    • MaxTenuringThreshold通过这个参数设置当年龄为多少的时候移入
  • 老年区满了以后触发一次Full GC,如果老年区无法再存放对象直接报OOM

注意:每一次GC都会给存活的对象的年龄+1

10.1.2 JDK1.8

1.7相比,仅仅是将永久代更替为了元空间。元空间的存放内置是物理内存,而不是JVM中。

这样处理,可以使元空间的大小不再受虚拟机内存大小的影响,而是由系统当前可用的空间来控制。

新生区和老年区的大小比例为1:2,通过-XX:NewRatio=n设置新生代和老年代的比例,n代表老年区所占的比例。

Eden Space和Survivor Space之间的比例默认为8:1,通过-XX:SurvivorRatio设置伊甸区和幸存者区的比例

逻辑层面分层:

  • 新生区Young Generation Space

    • 伊甸区Eden Space
    • 幸存区Survivor 0 Space
    • 幸存区Survivor 1 Space
  • 老年区Tenure Generation Space
  • 元空间(方法区)

物理层面分层:

  • 新生区 他占据堆的1/3
  • 老年区 他占据堆的2/3

10.2 堆参数调优

10.2.1 常用堆参数
参数作用
-Xms设置初始堆大小,默认为物理内存的1/64
-Xmx设置最大堆大小,默认为物理内存的1/4
-XX:+PrintGCDetails输出详细的GC日志

模拟OOM

//设置最大堆内存为10m 
//-Xms10m -Xmx10m -XX:+PrintGCDetails

下面我们具体分析一下GC的过程做了什么,GC日志怎么看

名称:GC以前占用->GC之后占用(总共占用)

//GC 分配失败
GC (Allocation Failure)
    [PSYoungGen: 1585K->504K(2560K)] 1585K->664K(9728K), 0.0009663 secs] //[新生代,以前占用->线程占用(总共空闲)] 堆使用大小->堆现在大小(总大小)
    [Times: user=0.00 sys=0.00, real=0.00 secs] 
    
    
[Full GC (Allocation Failure)
 [PSYoungGen: 0K->0K(2560K)] 
 [ParOldGen: 590K->573K(7168K)] 590K->573K(9728K),
 [Metaspace: 3115K->3115K(1056768K)], 0.0049775 secs] 
 [Times: user=0.00 sys=0.00, real=0.01 secs] 

11. 垃圾回收算法

11.1 垃圾回收类型

  • 普通GC(minor GC)发生在新生区的,很频繁
  • 全局GCmajor GC发生在老年代的垃圾收集动作,出现一次major GC经常会伴随至少一次的Minor GC

11.2 垃圾回收算法分类

11.2.1 引用计数法

主要思想:每存在一个对象引用就给这个对象加一,当这个对象的引用为零的时候,便触发垃圾回收。一般不使用

缺点:

  • 每次新创建对象就需要添加一个计数器,比较浪费
  • 循环引用较难处理
11.2.2 复制算法

主要思想:将对象直接拷贝一份,放置到其他区域

优点:不会产生内存碎片

缺点:占用空间比较大

使用场景:新生区的复制就是通过复制算法来执行的。当Minor Gc以后,就会幸存的对象复制一份放置到to

11.2.3 标记清除算法

主要思想:从引用根节点遍历所有的引用,标记出所有需要清理的对象,然后进行清除。两步完成

缺点:在进行垃圾回收的时候会打断整个代码的运行。会产生内存碎片

11.2.4 标记整理算法

主要思想:和标记清除算法一样,最后添加了一个步骤整理,将整理内存碎片。三步完成

缺点:效率低,需要移动对象。

11.3 各大垃圾回收算法比较

11.3.1 内存效率

复制算法>标记清除法>标记整理法

11.3.2 内存整齐度

复制算法=标记整理法>标记清除法

11.3.3 内存利用率

标记整理法=标记清除法>复制算法

11.3.4 最优算法

通过场景使用不同的算法,来达到最优的目的

年轻代:因为其对象存活时间段,对象死亡率高,所以一般使用复制算法

老年代:区域大,存活率高,一般采用标记清除和标记整理的混合算法。

老年代一般是由标记清除或者是标记清除与标记整理的混合实现。以hotspot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对像的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。

11.3.5 GCRoots

上面我们提到标记清除算法的时候,提到了一个名词,根节点引用。那么什么叫做根节点引用呢?

根节点引用也成GCRoots,他是指垃圾回收算法进行对象遍历的根节点。即从这个对象开始往下遍历,标记需要进行回收的对象。

垃圾回收标记的过程就是:以GCRoots对象开始向下搜索,如果一个对象到GCRoots没有任何的引用链相连时,说明此对象不可用。

就是从GCRoots进行遍历,可以被遍历到的就不是垃圾,没有被遍历到的就是垃圾,判定死亡

11.3.5.1 可达性对象和不可达性对象

可达性对象是指,在对象链路引用的顶层是一个GCRoot引用

不可达对象是指,在对象链路引用的顶层不是一个GCRoot引用

通俗解释:可达性对象就是对象有一个归属,这个归属有一个术语名称叫做GCRoot,不可达性对象就是这些对象没有归属。

11.3.5.2 什么引用可以作为GCRoots
  • 栈内的局部变量引用
  • 元空间中的静态属性引用
  • 元空间中的常量引用
  • 本地方法栈中native修饰的方法

说白了,就是所有暴露给开发者的引用

12. 垃圾回收器

垃圾回收器是基于GC算法实现的。

主要有四种垃圾回收器,不过具体有七种使用方式

12.1 四种垃圾回收器

12.1.1 串行垃圾回收器(Serial)

单线程进行垃圾回收,此时其他的线程全部被暂停

通过-XX:+UseSerialGC

12.1.2 并行垃圾回收器(Parallel)

多线程进行垃圾回收,此时其他的线程全部被暂停

12.1.3 并发垃圾回收器(CMS)

GC线程和用户线程同时运行

12.1.4 G1垃圾回收器

分区垃圾回收。物理上不区分新生区和养老区,将堆内存划分为1024个小的region,每一个占据的空间在2~32M,每一个region都可能是Eden SpaceSurvivor01 SpaceSurvivor02 SpaceOld区。

整体使用了标记整理算法,局部使用了复制算法。通过复制算法将GC后的对象从一个region向另一个region迁移,至于造成了内存碎片问题,通过整体的标记整理算法,避免了内存碎片的诞生

在进行垃圾回收的时候直接对一个region进行回收,保存下来的对象通过复制算法复制到TO区或者Old区。

逻辑上堆有四个区,每一个区的大小不定,按需分配。分为Eden SpaceSurvivor01 SpaceOldHumongous。其中Humongous用来存放大对象,一般是连续存储,当由于连续region不足的时候,会触发Full GC清理周围的Region以存放大对象

G1堆内存示意

G1垃圾回收

出现大对象,三个region不能存放,进行FullGC

执行流程

  • 初始标记。GC多线程,标记GCRoots
  • 并发标记。用户线程和GC线程同时进行。GC线程遍历GCRoots的所有的对象,进行标记
  • 重新标记。修正被并发标记标记的对象,由于用户程序再次调用,而需要取消标记的对象。GC线程
  • 筛选回收。清理被标记的对象。GC线程
  • 用户线程继续运行

12.1.4.1 案例
  • 初始标记。是通过一个大对象引发的G1

  • 并发标记

  • 重新标记、筛选清理和大对象引发的Full GC

12.1.4.2 G1常用参数
-XX:+UseG1GC  开启GC
-XX:G1HeapRegionSize=n : 设置G1区域的大小。值是2的幂,范围是1M到32M。目标是根据最小的Java堆大小划分出约2048个区域
-XX:MaxGCPauseMillis=n : 最大停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿时间小于这个时间
    
-XX:InitiatingHeapOccupancyPercent=n  堆占用了多少的时候就触发GC,默认是45
-XX:ConcGCThreads=n  并发GC使用的线程数
-XX:G1ReservePercent=n 设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%

12.2 常用参数

DefNew      Default New Generation //串行垃圾回收器,新生代叫法
Tenured     Old  //串行垃圾回收器,老年代叫法
ParNew         Parallel New Generation //新生代并行垃圾回收器,新生代叫法
PSYongGen     Parallel Scavenge //新生代和老年代垃圾回收器,叫法
ParOldGen     Parallel Old Generation //新生代和老年代垃圾回收器,叫法

12.3 新生代垃圾回收器

上图显示的是新生区和老年区可以使用垃圾回收器的所有种类,我们一个一个来说明

12.3.1 串行GC(Serial/Serial Coping)

新生代使用Serial Coping垃圾回收器使用复制算法

老年区默认使用Serial Old垃圾回收器,使用标记清除算法和标记整理算法

通过-XX:+UseSerialGC设置

12.3.2 并行GC(ParNew)

新生区使用ParNew垃圾回收器,使用复制算法

老年区使用Serial Old垃圾回收器(不推荐这样使用),使用标记清除算法和标记整理算法

通过-XX:+UseParNewGC启动

12.3.3 并行回收GC(Parallel/Parallel Scavenge)

新生代使用并行垃圾回收

老年代使用并行垃圾回收。Java1.8中默认使用的垃圾回收器

一个问题:Parallel和Parallel Scavenge收集器的区别?

Parallel Scavenge收集器类似于ParNew也是一个新生代的垃圾收集器,使用了复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。

parallel Scavenge是一种自适应的收集器,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数以提供最合适的提顿时间或者最大吞吐量

他关注的点是:

可控制的吞吐量。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),

同时,当新生代选择为Parallel Scavenge的时候,会默认激活老年区使用并行垃圾回收

通过-XX:UseParallelGC或者-XX:UseParallelOldGC两者会互相激活

-XX:ParallelGCThreads=n表示启动多少个GC线程

cpu>8时 N=5或者8

cpu<8时 N=实际个数

12.4 老年代垃圾回收器

12.4.1 串行垃圾回收器(Serial Old/Serial MSC)

Serial OldSerial垃圾收集器老年代版本,是一个单线程的收集器,使用标记整理算法,运行在Client中的年老代垃圾回收算法

与新生代的Serial GC相关联

12.4.2 并行回收(Parallel Old/Parallel MSC)

Parallel Old/采用标记整理算法实现

与新生代的Parallel Scavenge GC相关联

12.4.3 并发标记清除GC

CMS收集器(Concurrent Mark Sweep并发标记清除):一种以获取最短回收停顿时间为目标的收集器

适合应用在互联网站或者B/S系统的服务器上,重视服务器的响应速度

CMS非常适合堆内存大、CPU核数多的服务端应用,也是G1出现之前大型应用的首选收集器

标记的时候,GC线程运行;清除的时候和用户线程一起运行

通过-XX:+UseConcMarkSweepGC指令开启

配合新生区的pallellal New GC回收器使用

当CMS由于CPU压力太大无法使用的时候会使用SerialGC作为备用收集器

12.4.3.1 CMS执行过程
  • 初始标记(CMS initial mark)。遍历寻找到所有的GCRootsGC线程执行,用户线程暂停
  • 并发标记(CMS concurrent mark)和用户线程一起遍历GCRoots,标记需要清除的对象
  • 重新标记(CMS remark)。修正标记期间,对因用户程序继续运行而不需要进行回收的对象进行修正
  • 并发清除(CMS concurrent sweep)和用户线程一起清除所有标记的对象

12.4.3.2 优缺点

优点:

  • 并发收集低停顿

缺点:

  • 并发执行,对CPU资源压力大
  • 采用标记清除算法会导致大量的内存碎片

12.5 垃圾回收器小结

参数(-XX:+……)新生代垃圾回收器新生代算法老年代垃圾回收器老年代算法
UseSerialGCSerialGC复制算法Serial Old GC标整
UseParNewGCParallel New GC复制算法Serial Old GC标整
UseParllelGCParallel Scavenge GC复制算法Parallel GC标整
UseConcMarkSweepGCParallel New GC复制算法CMS和Serial Old GC标清
UseG1GC整体标整局部复制

垃圾回收算法通用逻辑

12.6 CMS和G1的区别

  • G1不会引发内存碎片
  • G1对内存的精准控制,可以精准的去收集垃圾。根据设置的GC处理时间去收集垃圾最多的区域

13. JMM

java内存模型。是一种规范。

线程在操作变量的时候,首先从物理内存中复制一份到自己的工作内存中(栈内存),更新以后再写入物理内存中

特点:

  • 原子性
  • 可见性
  • 有序性

更多原创文章和学习教程请关注笔者同名公众号@MakerStack获取
查看原文

赞 0 收藏 0 评论 0

石的三次方 赞了文章 · 2020-11-09

一篇读懂分布式架构下的负载均衡技术:分类、原理、算法、常见方案等

1、引言

关于“负载均衡”的解释,百度词条里:负载均衡,英文叫Load Balance,意思就是将请求或者数据分摊到多个操作单元上进行执行,共同完成工作任务。

负载均衡(Load Balance)建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。

负载均衡有两方面的含义:

1)首先,大量的并发访问或数据流量分担到多台节点设备上分别处理,减少用户等待响应的时间;

2)其次,单个重负载的运算分担到多台节点设备上做并行处理,每个节点设备处理结束后,将结果汇总,返回给用户,系统处理能力得到大幅度提高。

简单来说就是:

1)其一是将大量的并发处理转发给后端多个节点处理,减少工作响应时间;

2)其二是将单个繁重的工作转发给后端多个节点处理,处理完再返回给负载均衡中心,再返回给用户。

目前负载均衡技术大多数是用于提高诸如在Web服务器、FTP服务器和其它关键任务服务器上的Internet服务器程序的可用性和可伸缩性。

总之,它的目的就通过调度集群,达到最佳化资源使用,最大化吞吐率,最小化响应时间,避免单点过载的问题。

内容概述:本文将从负载均衡技术的分类、技术原理、常见实现算法、常用方案等入手,为您详细讲解负载均衡技术的方方面面。这其中,四层和七层负载均衡技术最为常用,它们也是本文介绍的重点。

内容点评:对于IM或消息推送应用的开发者来说,本文所介绍的传统负载均衡技术,可能对于IM等即时通讯分布式场景来说,没有办法直接套用。原因是IM这类socket长连接场景,所处的网络通信层级比较低,而且即时通讯相关的技术实现跟具体的业务逻辑紧密相关,因而无法像HTTP短连接这样基于标准化的负载均衡方法来实现。但本文所介绍的负载均衡原理、算法和一些方案实现,仍然可以为IM或消息推送应用的开发者带来一些借鉴和参考意义,值得深 入一读。

补充:另一篇《快速理解高性能HTTP服务端的负载均衡技术原理》,也讲述了负载均衡方面的知识,有兴趣也可以阅读之。

(本文同步发布于:http://www.52im.net/thread-24...

2、相关文章

深入阅读以下文章,有助于您更好地理解本篇内容:

《网络编程懒人入门(一):快速理解网络通信协议(上篇)》

《网络编程懒人入门(二):快速理解网络通信协议(下篇)》

《网络编程懒人入门(三):快速理解TCP协议一篇就够》

《腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面》

《快速理解高性能HTTP服务端的负载均衡技术原理》

《新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践》

《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》

《IM开发基础知识补课:正确理解前置HTTP SSO单点登陆接口的原理》

3、负载均衡分类

TCP/IP协议的OSI模型:
图片描述

(本图高清版,请从《计算机网络通讯协议关系图(中文珍藏版)[附件下载]》一文中下载之)

根据OSI模型可将负载均衡分为:

1)二层负载均衡(一般是用虚拟mac地址方式,外部对虚拟MAC地址请求,负载均衡接收后分配后端实际的MAC地址响应);

2)三层负载均衡(一般采用虚拟IP地址方式,外部对虚拟的ip地址请求,负载均衡接收后分配后端实际的IP地址响应);

3)四层负载均衡(在三次负载均衡的基础上,用 ip+port 接收请求,再转发到对应的机器);

4)七层负载均衡(根据虚拟的url或是IP,主机名接收请求,再转向相应的处理服务器)。

这其中,最常见的是四层和七层负载均衡,也是本文接下来介绍的重点。

当客户端发起请求,会经过层层的封装,发给服务器,服务器收到请求后经过层层的解析,获取到对应的内容。

下图是一个典型的HTTP请求分层传递原理:
图片描述

4、二层负载均衡

二层负债均衡是基于数据链路层的负债均衡,即让负债均衡服务器和业务服务器绑定同一个虚拟IP(即VIP),客户端直接通过这个VIP进行请求。

那么如何区分相同IP下的不同机器呢?没错,通过MAC物理地址,每台机器的MAC物理地址都不一样,当负载均衡服务器接收到请求之后,通过改写HTTP报文中以太网首部的MAC地址,按照某种算法将请求转发到目标机器上,实现负载均衡。

这种方式负载方式虽然控制粒度比较粗,但是优点是负载均衡服务器的压力会比较小,负载均衡服务器只负责请求的进入,不负责请求的响应(响应是有后端业务服务器直接响应给客户端),吞吐量会比较高。

图片描述

5、三层负载均衡

三层负载均衡是基于网络层的负载均衡,通俗的说就是按照不同机器不同IP地址进行转发请求到不同的机器上。

这种方式虽然比二层负载多了一层,但从控制的颗粒度上看,并没有比二层负载均衡更有优势,并且,由于请求的进出都要经过负载均衡服务器,会对其造成比较大的压力,性能也比二层负载均衡要差。

图片描述

6、四层负载均衡

四层的负载均衡就是基于IP+端口的负载均衡:在三层负载均衡的基础上,通过发布三层的IP地址(VIP),然后加四层的端口号,来决定哪些流量需要做负载均衡,对需要处理的流量进行NAT处理,转发至后台服务器,并记录下这个TCP或者UDP的流量是由哪台服务器处理的,后续这个连接的所有流量都同样转发到同一台服务器处理。

对应的负载均衡器称为四层交换机(L4 switch),主要分析IP层及TCP/UDP层,实现四层负载均衡。

此种负载均衡器不理解应用协议(如HTTP/FTP/MySQL等等),常见例子有:LVS,F5。

7、七层负载均衡

七层的负载均衡就是基于虚拟的URL或主机IP的负载均衡:在四层负载均衡的基础上(没有四层是绝对不可能有七层的),再考虑应用层的特征,比如同一个Web服务器的负载均衡,除了根据VIP加80端口辨别是否需要处理的流量,还可根据七层的URL、浏览器类别、语言来决定是否要进行负载均衡。

举个例子,如果你的Web服务器分成两组,一组是中文语言的,一组是英文语言的,那么七层负载均衡就可以当用户来访问你的域名时,自动辨别用户语言,然后选择对应的语言服务器组进行负载均衡处理。

对应的负载均衡器称为七层交换机(L7 switch),除了支持四层负载均衡以外,还有分析应用层的信息,如HTTP协议URI或Cookie信息,实现七层负载均衡。此种负载均衡器能理解应用协议,常见例子有: haproxy,MySQL Proxy。

8、四层负载均衡和七层负载均衡的区别

8.1 技术原理上的区别

所谓四层负载均衡,也就是主要通过报文中的目标地址和端口,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。

以常见的TCP为例,负载均衡设备在接收到第一个来自客户端的SYN 请求时,即通过上述方式选择一个最佳的服务器,并对报文中目标IP地址进行修改(改为后端服务器IP),直接转发给该服务器。TCP的连接建立,即三次握手是客户端和服务器直接建立的,负载均衡设备只是起到一个类似路由器的转发动作。在某些部署情况下,为保证服务器回包可以正确返回给负载均衡设备,在转发报文的同时可能还会对报文原来的源地址进行修改。

所谓七层负载均衡,也称为“内容交换”,也就是主要通过报文中的真正有意义的应用层内容,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。

以常见的TCP为例,负载均衡设备如果要根据真正的应用层内容再选择服务器,只能先代理最终的服务器和客户端建立连接(三次握手)后,才可能接受到客户端发送的真正应用层内容的报文,然后再根据该报文中的特定字段,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。负载均衡设备在这种情况下,更类似于一个代理服务器。负载均衡和前端的客户端以及后端的服务器会分别建立TCP连接。所以从这个技术原理上来看,七层负载均衡明显的对负载均衡设备的要求更高,处理七层的能力也必然会低于四层模式的部署方式。

8.2 应用场景的需求

七层应用负载的好处,是使得整个网络更"智能化"。参考这篇《利用负载均衡优化和加速HTTP应用》,就可以基本上了解这种方式的优势所在。

例如访问一个网站的用户流量,可以通过七层的方式,将对图片类的请求转发到特定的图片服务器并可以使用缓存技术;将对文字类的请求可以转发到特定的文字服务器并可以使用压缩技术。

当然这只是七层应用的一个小案例,从技术原理上,这种方式可以对客户端的请求和服务器的响应进行任意意义上的修改,极大的提升了应用系统在网络层的灵活性。很多在后台,例如Nginx或者Apache上部署的功能可以前移到负载均衡设备上,例如客户请求中的Header重写,服务器响应中的关键字过滤或者内容插入等功能。

另外一个常常被提到功能就是安全性。网络中最常见的SYN Flood攻击,即黑客控制众多源客户端,使用虚假IP地址对同一目标发送SYN攻击,通常这种攻击会大量发送SYN报文,耗尽服务器上的相关资源,以达到Denial of Service(DoS)的目的。

从技术原理上也可以看出,四层模式下这些SYN攻击都会被转发到后端的服务器上;而七层模式下这些SYN攻击自然在负载均衡设备上就截止,不会影响后台服务器的正常运营。另外负载均衡设备可以在七层层面设定多种策略,过滤特定报文,例如SQL Injection等应用层面的特定攻击手段,从应用层面进一步提高系统整体安全。

现在的7层负载均衡,主要还是着重于应用HTTP协议,所以其应用范围主要是众多的网站或者内部信息平台等基于B/S开发的系统。 4层负载均衡则对应其他TCP应用,例如IM即时通讯、实时消息推送等socket长连接系统。

8.3 七层应用需要考虑的问题

七层应用需要考虑的问题:

1)是否真的必要:七层应用的确可以提高流量智能化,同时必不可免的带来设备配置复杂,负载均衡压力增高以及故障排查上的复杂性等问题。在设计系统时需要考虑四层七层同时应用的混杂情况。

2)是否真的可以提高安全性:例如SYN Flood攻击,七层模式的确将这些流量从服务器屏蔽,但负载均衡设备本身要有强大的抗DDoS能力,否则即使服务器正常而作为中枢调度的负载均衡设备故障也会导致整个应用的崩溃。

3)是否有足够的灵活度:七层应用的优势是可以让整个应用的流量智能化,但是负载均衡设备需要提供完善的七层功能,满足客户根据不同情况的基于应用的调度。最简单的一个考核就是能否取代后台Nginx或者Apache等服务器上的调度功能。能够提供一个七层应用开发接口的负载均衡设备,可以让客户根据需求任意设定功能,才真正有可能提供强大的灵活性和智能性。

8.4 总体对比

四层负载均衡和七层负载均衡技术的总体对比:

1)智能性:七层负载均衡由于具备OIS七层的所有功能,所以在处理用户需求上能更加灵活,从理论上讲,七层模型能对用户的所有跟服务端的请求进行修改。例如对文件header添加信息,根据不同的文件类型进行分类转发。四层模型仅支持基于网络层的需求转发,不能修改用户请求的内容。

2)安全性:七层负载均衡由于具有OSI模型的全部功能,能更容易抵御来自网络的攻击;四层模型从原理上讲,会直接将用户的请求转发给后端节点,无法直接抵御网络攻击。

3)复杂度:四层模型一般比较简单的架构,容易管理,容易定位问题;七层模型架构比较复杂,通常也需要考虑结合四层模型的混用情况,出现问题定位比较复杂。

4)效率比:四层模型基于更底层的设置,通常效率更高,但应用范围有限;七层模型需要更多的资源损耗,在理论上讲比四层模型有更强的功能,现在的实现更多是基于http应用。

9、负载均衡技术的常见具体应用方案

目前有许多不同的负载均衡技术实现用以满足不同的应用需求,下面从负载均衡所采用的设备对象(软/硬件负载均衡),应用的OSI网络层次(网络层次上的负载均衡),及应用的地理结构(本地/全局负载均衡)等来分类。

9.1 软/硬件负载均衡

软件负载均衡解决方案:是指在一台或多台服务器相应的操作系统上安装一个或多个附加软件来实现负载均衡,如DNS Load Balance,CheckPoint Firewall-1 ConnectControl,Keepalive+ipvs等,它的优点是基于特定环境,配置简单,使用灵活,成本低廉,可以满足一般的负载均衡需求。软件解决方案缺点也较多,因为每台服务器上安装额外的软件运行会消耗系统不定量的资源,越是功能强大的模块,消耗得越多,所以当连接请求特别大的时候,软件本身会成为服务器工作成败的一个关键;软件可扩展性并不是很好,受到操作系统的限制;由于操作系统本身的Bug,往往会引起安全问题。

硬件负载均衡解决方案:是直接在服务器和外部网络间安装负载均衡设备,这种设备通常是一个独立于系统的硬件,我们称之为负载均衡器。由于专门的设备完成专门的任务,独立于操作系统,整体性能得到大量提高,加上多样化的负载均衡策略,智能化的流量管理,可达到最佳的负载均衡需求。负载均衡器有多种多样的形式,除了作为独立意义上的负载均衡器外,有些负载均衡器集成在交换设备中,置于服务器与Internet链接之间,有些则以两块网络适配器将这一功能集成到PC中,一块连接到Internet上,一块连接到后端服务器群的内部网络上。

软件负载均衡与硬件负载均衡的对比如下。

1)软件负载均衡:

优点:是需求环境明确,配置简单,操作灵活,成本低廉,效率不高,能满足普通的企业需求;

缺点:依赖于系统,增加资源开销;软件的优劣决定环境的性能;系统的安全,软件的稳定性均会影响到整个环境的安全。

2)硬件负载均衡:

优点:是独立于系统,整体性能大量提升,在功能、性能上优于软件方式;智能的流量管理,多种策略可选,能达到最佳的负载均衡效果;

缺点:是价格昂贵。

9.2 本地/全局负载均衡

负载均衡从其应用的地理结构上分为本地负载均衡(Local Load Balance)和全局负载均衡(Global Load Balance,也叫地域负载均衡),本地负载均衡是指对本地的服务器群做负载均衡,全局负载均衡是指对分别放置在不同的地理位置、有不同网络结构的服务器群间作负载均衡。

本地负载均衡能有效地解决数据流量过大、网络负荷过重的问题,并且不需花费昂贵开支购置性能卓越的服务器,充分利用现有设备,避免服务器单点故障造成数据流量的损失。其有灵活多样的均衡策略把数据流量合理地分配给服务器群内的服务器共同负担。即使是再给现有服务器扩充升级,也只是简单地增加一个新的服务器到服务群中,而不需改变现有网络结构、停止现有的服务。

全局负载均衡主要用于在一个多区域拥有自己服务器的站点,为了使全球用户只以一个IP地址或域名就能访问到离自己最近的服务器,从而获得最快的访问速度,也可用于子公司分散站点分布广的大公司通过Intranet(企业内部互联网)来达到资源统一合理分配的目的。

9.3 网络层次上的负载均衡

针对网络上负载过重的不同瓶颈所在,从网络的不同层次入手,我们可以采用相应的负载均衡技术来解决现有问题。

随着带宽增加,数据流量不断增大,网络核心部分的数据接口将面临瓶颈问题,原有的单一线路将很难满足需求,而且线路的升级又过于昂贵甚至难以实现,这时就可以考虑采用链路聚合(Trunking)技术。

链路聚合技术(第二层负载均衡)将多条物理链路当作一条单一的聚合逻辑链路使用,网络数据流量由聚合逻辑链路中所有物理链路共同承担,由此在逻辑上增大了链路的容量,使其能满足带宽增加的需求。

现代负载均衡技术通常操作于网络的第四层或第七层。第四层负载均衡将一个Internet上合法注册的IP地址映射为多个内部服务器的IP地址,对每次 TCP连接请求动态使用其中一个内部IP地址,达到负载均衡的目的。在第四层交换机中,此种均衡技术得到广泛的应用,一个目标地址是服务器群VIP(虚拟 IP,Virtual IP address)连接请求的数据包流经交换机,交换机根据源端和目的IP地址、TCP或UDP端口号和一定的负载均衡策略,在服务器IP和VIP间进行映射,选取服务器群中最好的服务器来处理连接请求。

第七层负载均衡控制应用层服务的内容,提供了一种对访问流量的高层控制方式,适合对HTTP服务器群的应用。第七层负载均衡技术通过检查流经的HTTP报头,根据报头内的信息来执行负载均衡任务。

第七层负载均衡优点表现在如下几个方面:

1)通过对HTTP报头的检查,可以检测出HTTP400、500和600系列的错误信息,因而能透明地将连接请求重新定向到另一台服务器,避免应用层故障;

2)可根据流经的数据类型(如判断数据包是图像文件、压缩文件或多媒体文件格式等),把数据流量引向相应内容的服务器来处理,增加系统性能;

3)能根据连接请求的类型,如是普通文本、图象等静态文档请求,还是asp、cgi等的动态文档请求,把相应的请求引向相应的服务器来处理,提高系统的性能及安全性。

第七层负载均衡缺点表现在如下几个方面:

1)第七层负载均衡受到其所支持的协议限制(一般只有HTTP),这样就限制了它应用的广泛性;

2)第七层负载均衡检查HTTP报头会占用大量的系统资源,势必会影响到系统的性能,在大量连接请求的情况下,负载均衡设备自身容易成为网络整体性能的瓶颈。

10、常用的负载均衡算法

常用的负载均衡算法分为两类:

1)一种是静态负载均衡;

2)一种是动态负载均衡。

10.1 静态均衡算法

【10.1.1】轮询法:

将请求按顺序轮流地分配到每个节点上,不关心每个节点实际的连接数和当前的系统负载。

优点:简单高效,易于水平扩展,每个节点满足字面意义上的均衡;

缺点:没有考虑机器的性能问题,根据木桶最短木板理论,集群性能瓶颈更多的会受性能差的服务器影响。

图片描述

【10.1.2】随机法:

将请求随机分配到各个节点。由概率统计理论得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配,也就是轮询的结果。

优缺点和轮询相似。

图片描述

【10.1.3】源地址哈希法:

源地址哈希的思想是根据客户端的IP地址,通过哈希函数计算得到一个数值,用该数值对服务器节点数进行取模,得到的结果便是要访问节点序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会落到到同一台服务器进行访问。

优点:相同的IP每次落在同一个节点,可以人为干预客户端请求方向,例如灰度发布;

缺点:如果某个节点出现故障,会导致这个节点上的客户端无法使用,无法保证高可用。当某一用户成为热点用户,那么会有巨大的流量涌向这个节点,导致冷热分布不均衡,无法有效利用起集群的性能。所以当热点事件出现时,一般会将源地址哈希法切换成轮询法。

图片描述

【10.1.4】加权轮询法:

不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。

加权轮询算法要生成一个服务器序列,该序列中包含n个服务器。n是所有服务器的权重之和。在该序列中,每个服务器的出现的次数,等于其权重值。并且,生成的序列中,服务器的分布应该尽可能的均匀。比如序列{a, a, a, a, a, b, c}中,前五个请求都会分配给服务器a,这就是一种不均匀的分配方法,更好的序列应该是:{a, a, b, a, c, a, a}。

优点:可以将不同机器的性能问题纳入到考量范围,集群性能最优最大化;

缺点:生产环境复杂多变,服务器抗压能力也无法精确估算,静态算法导致无法实时动态调整节点权重,只能粗糙优化。

图片描述

【10.1.5】加权随机法:

与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。

【10.1.6】键值范围法:

根据键的范围进行负债,比如0到10万的用户请求走第一个节点服务器,10万到20万的用户请求走第二个节点服务器……以此类推。

优点:容易水平扩展,随着用户量增加,可以增加节点而不影响旧数据;

缺点:容易负债不均衡,比如新注册的用户活跃度高,旧用户活跃度低,那么压力就全在新增的服务节点上,旧服务节点性能浪费。而且也容易单点故障,无法满足高可用。

图片描述

(注:以上所提到的单点故障,都可以用主从方式来解决,从节点监听主节点心跳,当发现主节点死亡,从节点切换成主节点顶替上去。这里可以思考一个问题,怎么设计集群主从可以最大程度上降低成本)

10.2 动态负债均衡算法
【10.2.1】最小连接数法:

根据每个节点当前的连接情况,动态地选取其中当前积压连接数最少的一个节点处理当前请求,尽可能地提高后端服务的利用效率,将请求合理地分流到每一台服务器。俗称闲的人不能闲着,大家一起动起来。

优点:动态,根据节点状况实时变化;

缺点:提高了复杂度,每次连接断开需要进行计数;

实现:将连接数的倒数当权重值。

【10.2.2】最快响应速度法:

根据请求的响应时间,来动态调整每个节点的权重,将响应速度快的服务节点分配更多的请求,响应速度慢的服务节点分配更少的请求,俗称能者多劳,扶贫救弱。

优点:动态,实时变化,控制的粒度更细,跟灵敏;

缺点:复杂度更高,每次需要计算请求的响应速度;

实现:可以根据响应时间进行打分,计算权重。

【10.2.3】观察模式法:

观察者模式是综合了最小连接数和最快响应度,同时考量这两个指标数,进行一个权重的分配。

附录:更多架构设计方案的文章精选

[1] 有关IM架构设计的文章:

《浅谈IM系统的架构设计》

《简述移动端IM开发的那些坑:架构设计、通信协议和客户端》

《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》

《一套原创分布式即时通讯(IM)系统理论架构方案》

《从零到卓越:京东客服即时通讯系统的技术架构演进历程》

《蘑菇街即时通讯/IM服务器开发之架构选择》

《腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT》

《微信后台基于时间序的海量数据冷热分级架构设计实践》

《微信技术总监谈架构:微信之道——大道至简(演讲全文)》

《如何解读《微信技术总监谈架构:微信之道——大道至简》》

《快速裂变:见证微信强大后台架构从0到1的演进历程(一)》

《17年的实践:腾讯海量产品的技术方法论》

《移动端IM中大规模群消息的推送如何保证效率、实时性?》

《现代IM系统中聊天消息的同步和存储方案探讨》

《IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》

《IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议》

《IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token》

《WhatsApp技术实践分享:32人工程团队创造的技术神话》

《微信朋友圈千亿访问量背后的技术挑战和实践总结》

《王者荣耀2亿用户量的背后:产品定位、技术架构、网络方案等》

《IM系统的MQ消息中间件选型:Kafka还是RabbitMQ?》

《腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面》

《以微博类应用场景为例,总结海量社交系统的架构设计步骤》

《快速理解高性能HTTP服务端的负载均衡技术原理》

《子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践》

《知乎技术分享:从单机到2000万QPS并发的Redis高性能缓存实践之路》

《IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列》

《微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)》

《微信技术分享:微信的海量IM聊天消息序列号生成实践(容灾方案篇)》

《新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践》

《一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践》

《阿里技术分享:深度揭秘阿里数据库技术方案的10年变迁史》

《阿里技术分享:阿里自研金融级数据库OceanBase的艰辛成长之路》

更多同类文章 ……

[2] 更多其它架构设计相关文章:

《腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面》

《快速理解高性能HTTP服务端的负载均衡技术原理》

《子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践》

《知乎技术分享:从单机到2000万QPS并发的Redis高性能缓存实践之路》

《新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践》

《阿里技术分享:深度揭秘阿里数据库技术方案的10年变迁史》

《阿里技术分享:阿里自研金融级数据库OceanBase的艰辛成长之路》

《达达O2O后台架构演进实践:从0到4000高并发请求背后的努力》

《优秀后端架构师必会知识:史上最全MySQL大表优化方案总结》

《小米技术分享:解密小米抢购系统千万高并发架构的演进和实践》

《一篇读懂分布式架构下的负载均衡技术:分类、原理、算法、常见方案等》

(本文同步发布于:http://www.52im.net/thread-24...

查看原文

赞 6 收藏 5 评论 1

石的三次方 发布了文章 · 2020-11-09

刨析SpringIOC及其启动原理

IOC总结

1. IOC概述

三个问题:

  1. IOC是什么

    1. 为什么用它
    2. 怎么用

1.1 是什么?

两个概念:控制反转,依赖注入

来看一下传统的干活方式:在对象单一职责原则的基础上,一个对象很少有不依赖其他对象而完成自己的工作,所以这个时候就会出现对象之间的依赖。而体现在我们的开发中,就是需要什么对象的时候,就创建什么对象,此时对象创建的控制权在我们自己手里。当对象创建的太多的时候,就会出现一个对象更改,就得更改所有依赖它的对象,耦合性大。自主性体现的同时也出现了对象耦合严重的情况

这个时候,我们就会思考,能不能我们在用的时候直接拿到这个对象去用,而将创建对象的能力交给第三方,这样我们就不需要关心对象是怎么创建的了。即将自己的控制权交出去。这就是控制反转

这个时候,就会有另一个问题产生了,对象怎么才能直接被我们拿来用呢。对象创建的时候,我们把这个对象注入到这个对象中,然后就可以使用了。这就是依赖注入

另一个问题,耦合性怎么被解决掉的?通过控制反转我们仅仅使用了这个对象,如果对象发生了修改,我们仅仅需要修改第三方创建对象的方式即可,这个时候难道还会出现所谓的对象耦合吗?

完成这些工作的就是IOC容器,它帮助我们创建对象,然后在对象被使用的时候,将对象注入到这个对象中。而由于IOC创建对象是通过反射来创建的,所以其速度不如直接new对象


还不理解???放心,听笔者讲一个故事,笔者最喜欢讲故事了

前段时间,天气逐渐回暖,鉴于家里没有短袖的情况,笔者只能选择购买了。这个时候笔者有两种选择,第一、去生产衣服的厂家直接去买(便宜);第二、去实体店或者网店购买(较昂贵)。之后,由于笔者属于宅男大军的一员,直接网上购物。

这个场景就是一个典型的控制反转的过程。笔者不需要关注衣服怎么生产的,而是仅仅去淘宝(IOC容器)上,寻找自己想要的衣服(对象),然后直接拿过来用即可。但是由于存在中间商赚差价,所以价格更贵(时间更长)

最后两句话:

控制反转:将自己的控制权交给自己信任的第三方,甲乙之间不存在依赖关系

依赖注入:开放一个端口留给A,然后在需要的时候,将B注入到A中。

1.2 为什么用

在上面,笔者已经很清晰的描述了为什么要使用IOC,主要原因就是由于对象之间的耦合。

1.3 怎么用

1.3.1 XML

通过书写XML配置文件,向容器中添加需要注入的Bean

1.3.2 Annotation

通过@Configuration注解指定配置类。

2. IOC架构

一个图搞定,这个就是IOC的架构思路,这不是其执行流程图

我们接下来一步一步来解读。

2.1 白话版

在第一章中我们了解了IOC是来帮助我们管理和创建对象的。

这个时候我们需要一个承载我们需要创建信息的容器,即图中的XML或者注解,那么有了我们自己的BeanDefiniton信息以后,我们需要一个接口用来读取这些信息,于是出现了BeanDefinitionReader用来读取我们自己的Bean信息。

那么我们需要考虑一个问题了,那么多的对象怎么生产呢?

答案就是工厂模式。Spring默认的工厂是DefaultListableBeanFactory,没错,Spring中的所有对象(容器对象和我们自己创建的对象)都是由他创建的。大批量生产对象

这个时候又有了一个问题,我们不想通过BeanFactory直接生产了,需要对这个工厂进行一些特定处理,于是出现了BeanFactoryPostProcessor,用来对工厂做一些特定的处理。我们自己可以通过实现这个接口,进行自定义BeanFactory。又有兄弟说了:我想单独创建一些我喜欢的对象,安排FactoryBean诞生了,它可以帮助我们创建一个我们需要的对象(第四部分详细解释他们之间的区别)。

那又有兄弟说了:我想让统一的对象创建之前按照我的方式进行一些特殊的行为,简单,安排

BeanPostProcessor出现了,他提供了两个方法:一个在对象实例化之后初始化之前,执行内部的Before方法,在初始化之后,执行After方法。(Bean生命周期,第四部分详解

这个时候有兄弟有疑问了,不是说BeanPostProcessor在创建对象之前执行吗?怎么是创建完毕以后才执行的Before方法。

如果各位兄弟了解过指令重排序这个概念,那么一定会听过一个案例,创建一个对象需要三步

  • 创建空间(实例化)
  • 初始化
  • 赋值

其中在初始化和赋值会出现指令重排序

根据这个点,应该可以get到一个点,实例化和初始化不一样。

所以又引出了一个点,我们对Bean进行一些操作,怎么操作,肯定是修改属性,或者添加一些属性等等,需要等待其在堆中开辟空间即实例化完成以后执行吧。

所以BeanPostProcessorbefore方法在实例化之后执行,初始化之前执行。

经历过前面一大堆的操作以后,终于我们的对象进入我们兜里了(容器里)。

关于销毁,一般情况下我们通过ApplicationContext拿不到其销毁方法,只能通过其子类实现获取,关于销毁同样的流程,先执行一个销毁之前的操作,然后再销毁。

2.2 实际工作流程

看过Spring源码或者听过的都知道里面有一个方法叫做refresh,他完成了好多事情。当然他的行为也代表了整个IOC容器加载和实例化对象的过程。第三章的代码解读中我们仔细看

执行过程:

  • 加载配置文件,初始化系统环境Environment接口
  • 准备上下文环境,初始化一些配置资源
  • 创建一个工厂
  • 为工厂添加各种环境
  • 获取子类自己重写的BeanFactoryPostProcessor
  • 执行容器和我们自己的BeanFactoryPostProcessor
  • 注册BeanPostProcessor
  • 国际化处理
  • 转播器
  • 子类初始化Bean
  • 注册监听器,观察者模式
  • 完成Bean创建
  • 发布相应的事件,监听器

3. IOC源码解读

写在之前:IOC的源码比较复杂,所以个人建议视频方式学习,大家可以B站搜索阁主梧桐(笔者认为讲的不错的一个解读),如果大家不喜欢视频的方式,又想深度学习IOC源码那么推荐程序员囧辉它的博客对于IOC的讲解非常深入。另外本文接下来的Spring源码,主要是通过图示的方法梳理其流程,作者水平有限。如有错误请留言。

3.1 上下文配置启动

在创建ClassPathXmlApplicationContext的时候,构造方法中执行了这些方法。

说白了,加载了一个解析配置文件路径的加载器;然后又通过系统环境变量拿到这个配置文件,进行一些配置文件的去空格,转换表达式等等操作(没有进行解析);最后就是那个被我标成红色东东,refresh方法中它完成了几乎所有的工作。下面细聊

3.2 refresh

这个方法几乎完成了所有的操作,创建工厂,执行Processor等等,实例化对象,开启事件监听等等。

接下来细聊

3.3.1 prepareRefresh()

这个方法的主要作用是为应用上下文的刷新做一些准备性的工作。校验资源文件,设置启动时间和活跃状态等。

3.3.2 obtainFreshBeanFactory()

可以get到,它主要就是创建了一个工厂BeanFactory,并且解析了配置文件,加载了Bean定义信息(面试的时候直接答这个点就够了,如果想说的可以将下面的bean信息加载聊聊)

没错,标红的就是咱接下来细聊的点

这个就是加载配置文件的过程,注意:此时仍然没有解析,解析在标红的下面

这个就是读取的过程,具体解析流程来自parse中,这个直接调用了Java中的解析XML的类库,有兴趣自行翻阅,最后返回了一个Document对象。

通过Document对象,读取内部的标签,执行不同的方法,逻辑和MyBatis中解析配置文件的思想相同,大家自行翻阅。

此时所有的Bean定义信息都被保存到了BeanDefinitionRegistry接口,然后走子类DefaultListableBeanFactory工厂的注册方法

3.3.3 prepareBeanFactory(beanFactory)

BeanFactory准备一些环境,方便在实例化的时候使用,同时添加容器自己的BeanPostProcessor

3.3.4 postProcessBeanFactory

留给子类扩展的BeanFactoryPostProcessor

3.3.5 invokeBeanFactoryPostProcessors(beanFactory)

这个类,涉及到了两个接口。

  • BeanFactoryPostProcessor
  • BeanDefinitionRegistryPostProcessor接口,这个接口是BeanFactoryPostProcessor的子接口,它的优先级比BeanFactoryPostProcessor更高

它的总体执行流程是:先执行BeanDefinitionRegistryPostProcessorBeanFactoryPostProcessor,然后再执行BeanFactoryPostProcessor

下图是BeanDefinitionRegistryPostProcessor接口的处理过程

BeanFactoryPostProcessor的处理逻辑

总逻辑就是先分类,已经处理过的直接跳过,没有处理过的,分类处理,逻辑和上面的相同。

3.3.6 registerBeanPostProcessors

这个方法的逻辑和上面的一样,只不过上面是直接执行了BeanFactoryPostProcessor,而这个仅仅注册没执行。

首先拿到工厂中所有的BeanPostProcessor类型的Bean,然后分类处理,排序注册。

3.3.7 initMessageSource()

执行国际化内容

3.3.8 initApplicationEventMulticaster

创建了一个多播器,为添加Listener提供支持。

主要逻辑:

  • 容器中是否存在applicationEventMulticaster,如果存在直接注册
  • 如果不存在,创建一个SimpleApplicationEventMulticaster,注册到容器中。
3.3.9 onRefresh()

子类扩展

3.3.10 registerListeners()

观察者模式的实现

protected void registerListeners() {
        // 拿到当前容器中的监听器,注册到多播器中
        for (ApplicationListener<?> listener : getApplicationListeners()) {
            getApplicationEventMulticaster().addApplicationListener(listener);
        }

        //拿到容器中为监听器的Bean,注册
        String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
        for (String listenerBeanName : listenerBeanNames) {
            getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
        }

        // 清空开始的事件,到广播器中
        Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
        this.earlyApplicationEvents = null;
        if (earlyEventsToProcess != null) {
            for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
                getApplicationEventMulticaster().multicastEvent(earlyEvent);
            }
        }
    }
3.3.11 finishBeanFactoryInitialization
这一部分的内容太多了,所以采用代码和图解的方式来讲解。
    /**
     * Finish the initialization of this context's bean factory,
     * initializing all remaining singleton beans.
       在上下文工厂中完成所有Bean 的初始化
     */
    protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
        // 初始化上下文转换服务Bean
        if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
                beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
            beanFactory.setConversionService(
                    beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
        }

        
        //如果不存在前入值解析器,则注册一个默认的嵌入值解析器,主要是注解属性解析
        if (!beanFactory.hasEmbeddedValueResolver()) {
            beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
        }

        // 初始化LoadTimeWeaverAware
        String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
        for (String weaverAwareName : weaverAwareNames) {
            getBean(weaverAwareName);
        }

        // Stop using the temporary ClassLoader for type matching.
        beanFactory.setTempClassLoader(null);

        // Allow for caching all bean definition metadata, not expecting further changes.
        beanFactory.freezeConfiguration();

        // Instantiate all remaining (non-lazy-init) singletons.
        //实例化,重点
        beanFactory.preInstantiateSingletons();
    }

下图是创建Bean的主要流程

按照途中的序号一个一个说:

  1. BeanDefinition是否需要合并。BeanDefinition根据不同类型的配置文件信息,会将Bean封装到不同的Bean信息定义类中。比如我们常用的配置文件版的GenericBeanDefinition;注解扫描版的ScannedGenericBeanDefinition等等。

而在这个过程中就出现了,父定义和子定义,我们需要在实际处理定义信息的时候进行合并处理,主要有一下三个方面

  • 存在父定义信息,使用父定义信息创建一个RootBeanDefinition,然后将自定义信息作为参数传入。
  • 不存在父定义信息,并且当前BeanDefinitionRootBeanDefintion类型的,直接返回一份RootBeanDefintion的克隆
  • 不存在父定义信息,并且当前BeanDefintion不是RootBeanDefintiton类型的,直接通过该BeanDefintion构建一个RootBeanDefintion返回

上面的流程也是源码中的执行流程

  1. isFactoryBean。判断是否为FactoryBean

简单介绍一下:FactoryBean是让开发者创建自己需要Bean接口。内部提供了三个方法

T getObject() throws Exception;//返回的Bean信息
Class<?> getObjectType();//返回的Bean类型
default boolean isSingleton() {return true;}//是否单例

当我们通过GetBean直接该Bean的时候,获取到的是该工厂指定返回的Bean类型。如果想要获取该Bean本身,需要通过一个前缀获得&

@Override
public boolean isFactoryBean(String name) throws NoSuchBeanDefinitionException {
    String beanName = transformedBeanName(name); //解析真正的BeanName
    Object beanInstance = getSingleton(beanName, false);//获取容器中的bean
    if (beanInstance != null) {//如果容器中存在,直接返回该Bean是否为FactoryBea类型
        return (beanInstance instanceof FactoryBean);
    }
    //没有Bean信息,检查这个Bean信息
    if (!containsBeanDefinition(beanName) && getParentBeanFactory() instanceof ConfigurableBeanFactory) {
            // 从父工厂中获取
        return 
            ((ConfigurableBeanFactory) getParentBeanFactory()).isFactoryBean(name);
        }
    //MergedBeanDefinition来检查beanName对应的Bean是否为FactoryBean
        return isFactoryBean(beanName, getMergedLocalBeanDefinition(beanName));
    }

再来看一个点,这个就是从容器中获取Bean的主要方法,也是解决循环依赖的逻辑

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    //查看当前容器中是否存在该Bean
        Object singletonObject = this.singletonObjects.get(beanName);
    //如果不存在,且当前Bean正在被创建
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            synchronized (this.singletonObjects) {
                //从早期的容器中获取Bean
                singletonObject = this.earlySingletonObjects.get(beanName);
                //如果早期容器也没有且允许创建早期引用
                if (singletonObject == null && allowEarlyReference) {
                    //获取该Bean的ObjectFactory工厂
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    //如果当前工厂不为空
                    if (singletonFactory != null) {
                        //创建一个对象实例,此时处于半初始化状态
                        singletonObject = singletonFactory.getObject();
                        //添加到早期引用中
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        //移除创建早期引用的工厂,因为该Bean已经创建且添加到了早期容器中,不需要再次进行创建了。
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return singletonObject;
}

来聊一下它是怎么解决循环引用的?

它引入了一个三级缓存的概念

/**存放了所有的单例Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** 存放了Bean创建需要的ObejctFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/** 存放了早期创建的Bean,此时的Bean没有进行属性赋值,仅仅通过构造方法创建了一个实例 */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

//正在创建的Bean
private final Set<String> singletonsCurrentlyInCreation =
            Collections.newSetFromMap(new ConcurrentHashMap<>(16));

在发生循环引用的时候,它首先通过ObejctFactory工厂将Bean创建出来,此时的对象并没有进行属性赋值,仅仅在堆中开辟了空间。然后将此时的Bean添加到earlySingletonObjects容器里,也就是说这个容器中保存的Bean都是半成品。而在之后的属性赋值中,由于对象为单例的,所以其引用地址不会发生变化,即对象最终是完整的。

  1. getBean通过这个方法直接创建了所有的对象,这也是Spring最核心的方法了

先来看一下它整体的一个流程

它的主要逻辑是:先拿到当前要实例化的Bean的真实名字,主要是为了处理FactoryBean,拿到以后,从当前容器中看是否已经创建过该Bean,如果存在直接返回。

如果不存在,获取其父工厂,如果父工厂不为空,而且当前容器中不存在当前Bean的信息,则尝试从父工厂中获取Bean定义信息,进行Bean实例化

如果父工厂为空,将当前Bean信息存放到alreadyCreated缓存中。

获取当前Bean的合并信息(getMergedLocalBeanDefinition),查看当前Bean是否存在依赖,如果存在则判断当前Bean和依赖Bean是否为循环依赖,如果不是循环依赖则先创建依赖Bean

判断当前Bean的作用域。

如果当前Bean是单例对象,直接创建Bean实例

如果当前Bean是多例对象,将当前Bean信息添加到正在创建多例缓存中,创建完毕以后移除

如果当前Bean是其他类型,如Requtst,Session等类型,则自定义一个ObejctFacotry工厂,重写getObject方法,创建对象

对象创建以后,判断当前对象是否为自己需要的对象,如果是直接返回;如果不是进行类型转换,如果类型转换失败,直接抛异常

接下来看一眼CreateBean的执行

这个方法主要完成的事情是:通过Bean的名字拿到对应的Class对象;如果当前Bean获取到的Class对象不为空且该RootDefintiton可以直接获取到该Bean,克隆一份Bean定义信息,方便之后使用。

验证当前Bean上的@Override信息。执行BeanPostProcessor,返回一个代理对象(如果存在代理的话)

如果不存在代理,则直接创建Bean

接下来我们来聊一下这个玩意——resolveBeforeInstantiation

protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
        Object bean = null;
        if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
            // Make sure bean class is actually resolved at this point.
            //当前定义信息不是合并,且存在Bean增强器
            if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
                //获取Bean的Class类型
                Class<?> targetType = determineTargetType(beanName, mbd);
                if (targetType != null) {
                    //如果不为null,则执行前置处理器
                    bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
                    if (bean != null) {
                        //如果前置处理器不为null,则后置处理器执行,跳过spring默认初始化
                        bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
                    }
                }
            }
            //代表已经再实例化之前进行了解析
            mbd.beforeInstantiationResolved = (bean != null);
        }
        return bean;
    }

来吧,继续,看一下那个前置处理器逻辑

protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            //拿到工厂中的所有的BeanPostProcessor
            if (bp instanceof InstantiationAwareBeanPostProcessor) {
                //找到所有我们需要的增强器
                InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
                // 返回一个代理实例
                Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName);
                if (result != null) {
                    return result;
                }
            }
        }
        return null;
    }

后置处理器就不看了,就调用了所有的后置处理器,然后执行了一遍,没有其他逻辑。

接下来继续我们的正题:doCreateBean

其大致流程如上图:

先判断以后是否单例,然后从FactoryBean缓存中看一下是否存在正在创建的Bean,如果存在拿出,如果不存在则创建一个当前Bean的包装类实例。然后拿到这个类的实例和实例类型,执行以后后置处理器。

当前Bean是否为单例,是否允许循环依赖,时候正在进行创建,如果是,创建一个当前Bean的ObejctFactory以解决循环依赖的问题

填充Bean的属性,进行Bean的实例化。

查看早期容器缓存中(缓存中的二级缓存中是否有该Bean)。如果有,则说明存在循环依赖,则进行处理

先看循环依赖吧

if (earlySingletonExposure) {
    //从早期的Bean容器中拿到实例对象,此时的Bean必然存在循环依赖
    Object earlySingletonReference = getSingleton(beanName, false);
    if (earlySingletonReference != null) {
        
        if (exposedObject == bean) {
            exposedObject = earlySingletonReference;
        } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
            
            //获取依赖的全部Bean信息
            String[] dependentBeans = getDependentBeans(beanName);
            Set < String > actualDependentBeans = new LinkedHashSet < > (dependentBeans.length);
            for (String dependentBean: dependentBeans) {
                //清除这些Bean信息,此时的Bean已经是脏数据了
                if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                    //无法清理存入actualDependentBeans中
                    actualDependentBeans.add(dependentBean);
                }
            }
            if (!actualDependentBeans.isEmpty()) {
                throw new BeanCurrentlyInCreationException
            }
        }
    }
}

// Register bean as disposable.
try {
    registerDisposableBeanIfNecessary(beanName, bean, mbd);
} catch (BeanDefinitionValidationException ex) {
    throw new BeanCreationException(
        mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
}

接着来,createBeanInstance

Spring提供了三种方式创建对象的包装:

  • 通过供给者对象对象直接创建。obtainFromSupplier
  • 通过工厂方法直接创建。
  • 默认创建。

    • 构造方法是否需要自动注入
    • 构造方法不需要自动注入,调用默认的构造方法

这个方法执行完毕以后,你应该知晓的一个点是:此时对象实例已经创建了,剩下的就是执行一系列增强器和初始化方法,属性填充等等。


我们按照代码执行顺序来,属性填充即populateBean

这个方法执行逻辑:

首先判断传入的Bean是否为null,如果为null则判断Bean定义信息中是否存在属性值,如果存在,异常;如果不存在跳过

当前Bean定义信息是否为合并以后的,如果是且此时的工厂中存在InstantiationAwareBeanPostProcessors,那么在属性填充之前进行修改Bean的信息

拿到所有的属性值,解析属性值的自动注入方式,Type或者Name,进行自动注入

判断是否存在InstantiationAwareBeanPostProcessors,修改之前设置的属性

判断是否存在依赖检查,检查依赖

属性赋值


接下来看执行初始化方法,就是调用BeanPostprocessor,init等方法

这个就是这个方法的执行流程图,相信到这个地方,大家应该对于为什么BeanPostProcessor的before方法会在init方法执行了解了。这个方法的作用仅仅是用来进行一个生命周期的打印,对象在之前已经创建了。


接下来看一下销毁的方法。registerDisposableBeanIfNecessary

对于单例Bean来说,Spring将需要销毁的Bean存放到了disposableBeans缓存中,通过DisposableBeanAdapter封装了销毁Bean

对于其他作用域来说,自定义了销毁回调函数,不过最后还是封装为DisposableBeanAdapter

在封装为DisposableBeanAdapter的过程中,会首先判断该Bean中是否存在destroy方法,然后给赋值给destroyMethodName变量。再次判断这个方法的参数,如果参数的个数大于1,则抛出异常

3.3.12 finishRefresh

这个方法进行了一系列的资源清理和

protected void finishRefresh() {
        // 清空上下文资源缓存
        clearResourceCaches();

        // 初始化生命周期处理器
        initLifecycleProcessor();

        // 将已经刷新完毕的处理器传播(扔到)生命周期处理器中
        getLifecycleProcessor().onRefresh();

        // 推送上下文刷新完毕的时间到相应的监听器
        publishEvent(new ContextRefreshedEvent(this));

        // Participate in LiveBeansView MBean, if active.
        LiveBeansView.registerApplicationContext(this);
    }

initLifecycleProcessor,这个方法极具简单,就看一下当前Bean中是否存在生命周期处理器,如果存在直接使用这个,如果不存在则创建一个默认的,并且注册为一个单例的扔到容器中。

4. 常见题目

4.1 Bean的生命周期?

Spring官方解释在BeanDefinition接口的注释里

答:Bean完整的生命周期是:

  • 设置一系列Aware接口的功能
  • 实例化Bean
  • 调用BeanPostProcessorbefore方法
  • 执行InitializingBean接口方法afterPropertiesSet
  • 执行init方法
  • 调用BeanPostProcessorpostProcessAfterInitialization方法
  • 调用DestructionAwareBeanPostProcessors接口的postProcessBeforeDestruction方法
  • 调用destory方法

4.2 FactoryBean和BeanFactory的区别

答:BeanFactorySpring默认生产对象的工厂。

FactoryBeanSpring提供的一个生产特定类型和特定对象的工厂。例如Mybatis-spring中的SqlSessionFactoryBean就是通过这种方法创建的。

4.3 什么是循环依赖?Spring如何处理循环依赖的?

答:循环依赖是指:在创建A对象的时候需要注入B对象;在创建B对象的时候需要注入A对象,两者互相依赖。

出现循环依赖有两种情况:

  • 构造器依赖(无法解决)
  • 属性注入(可以解决

解决循环依赖,Spring引入了三级缓存的概念。上面的源码讲解中介绍过

  • singletonObjects存放了所有的单例Bean,此时所有的Bean信息都是完整的
  • earlySingletonObjects存放了早期的Bean,此时仅仅创建了一个Bean实例,未进行属性填充
  • singletonFactories存放了Bean的工厂

Spring通过将创建Bean的工厂暴露出来,然后在出现循环依赖的时候通过这个工厂常见一个bean,然后将这个Bean注入,由于对象是单例的,所以在接下来的属性填充中,可以保证为同一个对象,至此,循环依赖解除。

使用三太子敖丙的一句话:解决循环依赖的过程就是力扣中的第一题两数之和的过程

4.4 什么是IOC

答:IOC存在两个点:

  • 控制反转。将常见对象的控制权交给第三方,这里的第三方就是Spring
  • 依赖注入。在类中需要使用到的对象,全部通过反射从第三方容器注入而不是自己创建。这里的第三方容器即Spring

4.5 ApplicationContext和BeanFactory的区别

答:

  • ApplicationContext采用了立即加载,即加载配置文件的时候就创建了对象。BeanFactory采用了延时加载的方式,使用的时候才创建。
  • 对于BeanPostProcessorBeanFactoryProcessor而言,BeanFactory是手动注册,ApplicationContext采用了自动注册。

4.6 Spring 框架中都用到了哪些设计模式?

答:

  • 单例模式。这个不需要多说
  • 代理模式。AOP使用到的
  • 装饰着模式。BeanWrapper
  • 工厂模式。BeanFactory,创建对象的时候
  • 模板方法模式。JDBCTemplate
  • 观察者模式。各种事件监听
  • ……

更多原创文章和Java系列教程请关注公众号@MakerStack

查看原文

赞 3 收藏 3 评论 0

石的三次方 发布了文章 · 2020-11-07

抽丝剥茧——调停者和门面设计模式

调停者和门面设计模式

今天我们来聊两个设计模式:调停者设计模式和门面设计模式,为什么要将他们放在一起讲解,因为他们两个东东太像了,仅仅是由于作用的地方不同而产生的不同的叫法。

我们用一个对于我们90后最难的一个问题来入手吧。假设我们厌倦了城市生活,想要找一个安静的地方安家,养猪,顺便写一个猪脸识别来分类管理这些猪(梦想中的生活)。而在做这些事情的前提,我们必须建造一个房子和一个猪圈。

我们来看一下我们以前会怎么做。

盖房子需要工人,砖头,水泥等等,我们需要一个一个联系所需要的人。但我这么聪明当然不会这么干了,所以我找了一个人来帮我完成这些事情,于是就成为了这样的流程。

我找了一个包工头,代理商帮我去完成这些事情。这个流程就是一个完整的门面模式。是不是感觉和代理模式有点像,帮我做事情。其实吧设计模式到最后就殊途同归了,正所谓,太极剑法,学多少忘多少,最后记得的只是太极剑。

我们回到编程领域,我们来看一下门面模式具体的类图实现

那接下来我们来看一下调停者设计模式。

它和门面模式最大的区别就是门面模式是挡在外层的,而它是在所有服务中间的。我们来看一下它的原理图。

我们再来看一下它的类图实现

发现了没,两个模式的类图实现几乎相同,所以他们的代码实现也几乎相同。

了解了他们的原理以后,我们来聊聊他们在实际代码中的应用。

门面模式:服务器部署时的网关,将所有的请求拦截,具体的方法转发由网关决定

调停者模式:协调中间件,微服务中将所有的服务注册到类似于zookeeper的协调中间件中,通过中间件访问其他服务;消息中间件,需要什么消息通过消息中间件进行获取。

对于一些比较老的项目,门面模式和调停者模式的调度中心很有可能是一个,如通过Nginx管理服务。

我们来看一下具体的代码实现吧。

门面模式代码实现(角色组成)

  • 子系统
 ​
 class Cement{
  void cement(){
  System.out.println("水泥");
  }
 }
 ​
 class Worker{
  void worker(){
  System.out.println("工人");
  }
 }
 ​
 class Brick{
  void brick(){
  System.out.println("砖头");
  }
 }
  • 门面
 class Contractor{
  private Cement cement = new Cement();
  private Worker worker = new Worker();
  private Brick brick = new Brick();
 ​
  void cement(){
  cement.cement();
  }
 ​
  void worker(){
  worker.worker();
  }
 ​
  void brick(){
  brick.brick();
  }
 }

调停者模式的代码实现和门面模式几乎相同。两者只是因为应对与不同的位置而诞生,本质相同。

更多原创文章请关注公众号@MakerStack

查看原文

赞 0 收藏 0 评论 0

石的三次方 发布了文章 · 2020-11-06

基于MVC的RESTFul风格API实战

基于MVCRESTful风格的实现

1.RESTful风格阐述

REST服务是一种ROA(Resource-Oriented Architecture,面向资源的架构)应用。主要特点是方法信息存在于HTTP协议的方法中(GET,POST,PUT,DELETE),作用域存在于URL中。例如,在一个获取设备资源列表的GET请求中,方法信息是GET,作用域信息是URI种包含的对设备资源的过滤、分页和排序等条件

==良好的REST API不需要任何文档==

1.1REST风格资源路径

REST风格的资源路径设计是面向资源的,==资源的名称==应该是准确描述该资源的==名词==。

资源路径概览:sheme://host:port/path?queryString

例:http://localhost:8080/bywlstudio/users/user?username=xiuer

1.2HTTP方法
GET用于==读取==、==检索==、==查询==、==过滤==资源

PSOT用于==创建==一个资源

PUT用于==修改==、==更新==资源、==创建客户端维护主键信息的资源==

DELETE用于==删除==资源

资源地址和HTTP方法结合在一起就可以实现对资源的完整定位

1.3RESTful风格API设计

上文讲述了通过HTTP方法和资源路径对服务器的一个资源进行定位的过程

接下来看一个REST风格API的设计

功能描述
添加/创建POST/users
PUT/users{id}1
删除DELETE/users/{id}
修改/更新PUT/users/{id}
查询全部GET/users
主键查询GET/users/{id}
GET/users?id=26
分页作用域查询GET/users?start=0&size=10
GET/users?07,2019-07,2020

可以看到通过这个RESTAPI都是通过对==同一个资源==的操作,所不同的就是通过不同的==HTTP方法==来实现对资源不同的处理。

2.MVCREST的支持

1.1主要通过注解来实现
  • @Controller声名一个处理请求的控制器
  • @RequestMapping请求映射地址,它存在几个子注解对于实现REST风格来说更加具有==语义性==

    • @GETMapping ==GET请求==
    • @PUTMapping ==PUT请求==
    • @POSTMapping ==POST请求==
    • @DELETEMapping ==DELETE请求==
  • @ResponseBody 将响应内容转换为JSON格式
  • @RequestBody 请求内容转换为JSON格式
  • @PathVariable("id")用于绑定一个参数
  • @RESTController 等同于@Controller+@ResponseBody在类上写了这个注解,标识这个类的所有方法只==返回数据==,而不进行==视图跳转==
1.2返回HTTP状态码

REST风格API一个最鲜明的特点通过返回对应的HTTPStatus来判断客户端的操作是否完成

==下面是spring中关于Http状态码描述的枚举类,本文列举了常见的状态码==(读者若对此感兴趣可以查看HttpStatus源码)

public enum HttpStatus{
    OK(200, "OK"),//用于服务器有实体响应
    CREATED(201, "Created"),//创建了新实体,响应该实体
    NO_CONTENT(204, "No Content"),//服务器正常响应,但无实体响应
    BAD_REQUEST(400, "Bad Request"),//客户端请求语法错误
    NOT_FOUND(404, "Not Found"),//目标资源不存在
    INTERNAL_SERVER_ERROR(500, "Internal Server Error"),//服务器内部错误
    NOT_IMPLEMENTED(501, "Not Implemented"),//服务器不支持当前请求
}

Spring返回状态码是通过@ResponseStatus注解或者ResponseEntity<?>类实现的。

==@ResponseStatus方式==

@GetMapping(path = "/user/{id}" , produces = "application/json;charset=utf-8")
@ResponseStatus(HttpStatus.OK)
public User findUserById(@PathVariable("id")Integer id){
    User user = userService.findUserById(id);
    return user ;
}

==ResponseEntity<?>==方式

@GetMapping(produces = "application/json;charset=utf-8")
public ResponseEntity<List<User>> findAll(){
    List<User> users = userService.findAll();
    return new ResponseEntity<List<User>>(users , HttpStatus.OK);
}
1.3由于MVC默认不支持PUTDELETE方法,所以需要手动开启

tomcat服务器的web.xml文件中开启一下配置

<servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <init-param>
        <param-name>readonly</param-name>
        <param-value>true</param-value><!--开启这个-->
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

在项目的web.xml中配置

<filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <servlet-name>dispathcherServlet</servlet-name>
  </filter-mapping>

3.MVC实现REST代码实现

3.1实例环境
  • JDK1.8
  • maven3.60
  • tomcat9
3.2API设计
URIDescriptionResponseHTTPStatus
==GET==/users获取全部用户JSON200
==GET==/users/{id}获取指定主键的用户JSON200
==PUT==/users/{id}修改指定的主键的用户信息JSON200/201
==POST==/users增加一个用户JSON201
==DELETE==/users/{id}删除一个用户void204
3.3控制层代码
@RestController
@RequestMapping("/users")
public class UserControler {

    @Autowired
    private IUserService userService ;

    //REST风格实现方法

    /**
     * 查询所有
     * @return
     */
    @GetMapping(produces = "application/json;charset=utf-8")
    public ResponseEntity<List<User>> findAll(){
        List<User> users = userService.findAll();
        return new ResponseEntity<List<User>>(users , HttpStatus.OK);
    }

    /**、
     * 根据ID查询
     * @param id
     * @return
     */

    @GetMapping(path = "/{id}" , produces = "application/json;charset=utf-8")
    @ResponseStatus(HttpStatus.OK)
    public User findUserById(@PathVariable("id")Integer id){
        User user = userService.findUserById(id);
        return user ;
    }
    /**
     * 增加一个用户
     * 返回该用户
     */
    @PostMapping(produces = "application/json;charset=utf-8")
    @ResponseStatus(HttpStatus.CREATED)
    public User addUser(@RequestBody User user){
        User newUser = userService.addUser(user);
        return newUser ;
    }

    /**
     * 更新
     * @param user
     */
    @PutMapping(path = "/{id}" ,produces = "application/json;charset=utf-8")
    public ResponseEntity<User> updateUser(@PathVariable("id") Integer id , @RequestBody User user){
        user.setUid(id);
        //资源是否修改
        boolean flag = userService.updateUser(user);
        User deUser = userService.findUserById(id);
        if(flag)
            return new ResponseEntity<User>(deUser,HttpStatus.CREATED);
        return new ResponseEntity<User>(deUser,HttpStatus.OK);
    }

    @DeleteMapping(path = "/{id}"  , produces = "application/json;charset=utf-8")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delUser(@PathVariable("id") Integer id){
        User user = userService.findUserById(id);
        userService.delUser(id);
    }
}

更多原创文章和Java学习资料@公众号MakerStack


  1. 创建客户端维护主键信息的资源
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 3 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-07-28
个人主页被 655 人浏览