开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

类加载流程,类加载机制及自定义类加载器详解(面试再也不怕了)

发表于 2019-06-12

一、引言二、类的加载、链接、初始化1、加载1.1、加载的class来源2、类的链接2.1、验证2.2、准备2.3、解析3、类的初始化3.1、< clinit>方法相关3.2、类初始化时机3.3、final定义的初始化3.4、ClassLoader只会对类进行加载,不会进行初始化三、类加载器1、JVM类加载器分类1.1、Bootstrap ClassLoader1.2 、Extension ClassLoader1.3、 System ClassLoader四、类加载机制1.1、JVM主要的类加载机制。1.2、类加载流程图五、创建并使用自定义类加载器1、自定义类加载分析2、实现自定义类加载器六、总结

一、引言

当程序使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、链接、初始化三个步骤对该类进行类加载。

二、类的加载、链接、初始化

1、加载

类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象。类的加载过程是由类加载器来完成,类加载器由JVM提供。我们开发人员也可以通过继承ClassLoader来实现自己的类加载器。

1.1、加载的class来源
  • 从本地文件系统内加载class文件
  • 从JAR包加载class文件
  • 通过网络加载class文件
  • 把一个java源文件动态编译,并执行加载。

2、类的链接

通过类的加载,内存中已经创建了一个Class对象。链接负责将二进制数据合并到 JRE中。链接需要通过验证、准备、解析三个阶段。

2.1、验证

验证阶段用于检查被加载的类是否有正确的内部结构,并和其他类协调一致。即是否满足java虚拟机的约束。

2.2、准备

类准备阶段负责为类的类变量分配内存,并设置默认初始值。

2.3、解析

我们知道,引用其实对应于内存地址。思考这样一个问题,在编写代码时,使用引用,方法时,类知道这些引用方法的内存地址吗?显然是不知道的,因为类还未被加载到虚拟机中,你无法获得这些地址。举例来说,对于一个方法的调用,编译器会生成一个包含目标方法所在的类、目标方法名、接收参数类型以及返回值类型的符号引用,来指代要调用的方法。

解析阶段的目的,就是将这些符号引用解析为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必会触发解析与初始化)。

3、类的初始化

类的初始化阶段,虚拟机主要对类变量进行初始化。虚拟机调用< clinit>方法,进行类变量的初始化。

java类中对类变量进行初始化的两种方式:

  1. 在定义时初始化
  2. 在静态初始化块内初始化
3.1、< clinit>方法相关
  • 虚拟机会收集类及父类中的类变量及类方法组合为< clinit>方法,根据定义的顺序进行初始化。虚拟机会保证子类的< clinit>执行之前,父类的< clinit>方法先执行完毕。因此,虚拟机中第一个被执行完毕的< clinit>方法肯定是java.lang.Object方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class Test {  
    static int A = 10;
    static {
        A = 20;
    }
}

class Test1 extends Test {
    private static int B = A;
    public static void main(String[] args) {
        System.out.println(Test1.B);
    }
}
//输出结果
//20

从输出中看出,父类的静态初始化块在子类静态变量初始化之前初始化完毕,所以输出结果是20,不是10。

  • 如果类或者父类中都没有静态变量及方法,虚拟机不会为其生成< clinit>方法。
  • 接口与类不同的是,执行接口的<clinit>方法不需要先执行父接口的<clinit>方法。 只有当父接口中定义的变量使用时,父接口才会初始化。 另外,接口的实现类在初始化时也一样不会执行接口的<clinit>方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码public interface InterfaceInitTest {  
    long A = CurrentTime.getTime();

}

interface InterfaceInitTest1 extends InterfaceInitTest {
    int B = 100;
}

class InterfaceInitTestImpl implements InterfaceInitTest1 {
    public static void main(String[] args) {
        System.out.println(InterfaceInitTestImpl.B);
        System.out.println("---------------------------");
        System.out.println("当前时间:"+InterfaceInitTestImpl.A);
    }
}

class CurrentTime {
    static long getTime() {
        System.out.println("加载了InterfaceInitTest接口");
        return System.currentTimeMillis();
    }
}
//输出结果
//100
//---------------------------
//加载了InterfaceInitTest接口
//当前时间:1560158880660

从输出验证了:对于接口,只有真正使用父接口的类变量才会真正的加载父接口。这跟普通类加载不一样。

  • 虚拟机会保证一个类的< clinit>方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的< clinit>方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>方法完毕。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码public class MultiThreadInitTest {  
    static int A = 10;
    static {
           System.out.println(Thread.currentThread()+"init MultiThreadInitTest");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread() + "start");
            System.out.println(MultiThreadInitTest.A);
            System.out.println(Thread.currentThread() + "run over");
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}
//输出结果
//Thread[main,5,main]init MultiThreadInitTest
//Thread[Thread-0,5,main]start
//10
//Thread[Thread-0,5,main]run over
//Thread[Thread-1,5,main]start
//10
//Thread[Thread-1,5,main]run over

从输出中看出验证了:只有第一个线程对MultiThreadInitTest进行了一次初始化,第二个线程一直阻塞等待等第一个线程初始化完毕。

3.2、类初始化时机
  1. 当虚拟机启动时,初始化用户指定的主类;
  2. 当遇到用以新建目标类实例的new指令时,初始化new指令的目标类;
  3. 当遇到调用静态方法或者使用静态变量,初始化静态变量或方法所在的类;
  4. 子类初始化过程会触发父类初始化;
  5. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口初始化;
  6. 使用反射API对某个类进行反射调用时,初始化这个类;
  7. Class.forName()会触发类的初始化
3.3、final定义的初始化

注意:对于一个使用final定义的常量,如果在编译时就已经确定了值,在引用时不会触发初始化,因为在编译的时候就已经确定下来,就是“宏变量”。如果在编译时无法确定,在初次使用才会导致初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class StaticInnerSingleton {  
    /**
     * 使用静态内部类实现单例:
     * 1:线程安全
     * 2:懒加载
     * 3:非反序列化安全,即反序列化得到的对象与序列化时的单例对象不是同一个,违反单例原则
     */
    private static class LazyHolder {
        private static final StaticInnerSingleton INNER_SINGLETON = new StaticInnerSingleton();
    }

    private StaticInnerSingleton() {
    }

    public static StaticInnerSingleton getInstance() {
        return LazyHolder.INNER_SINGLETON;
    }
}

看这个例子,单例模式静态内部类实现方式。我们可以看到单例实例使用final定义,但在编译时无法确定下来,所以在第一次使用StaticInnerSingleton.getInstance()方法时,才会触发静态内部类的加载,也就是延迟加载。这里想指出,如果final定义的变量在编译时无法确定,则在使用时还是会进行类的初始化。

3.4、ClassLoader只会对类进行加载,不会进行初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class Tester {  
    static {
        System.out.println("Tester类的静态初始化块");
    }
}

class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        //下面语句仅仅是加载Tester类
        classLoader.loadClass("loader.Tester");
        System.out.println("系统加载Tester类");
        //下面语句才会初始化Tester类
        Class.forName("loader.Tester");
    }
}
//输出结果
//系统加载Tester类
//Tester类的静态初始化块

从输出证明:ClassLoader只会对类进行加载,不会进行初始化;使用Class.forName()会强制导致类的初始化。

三、类加载器

类加载器负责将.class文件(不管是jar,还是本地磁盘,还是网络获取等等)加载到内存中,并为之生成对应的java.lang.Class对象。一个类被加载到JVM中,就不会第二次加载了。

那怎么判断是同一个类呢?

每个类在JVM中使用全限定类名(包名+类名)与类加载器联合为唯一的ID,所以如果同一个类使用不同的类加载器,可以被加载到虚拟机,但彼此不兼容。

1、JVM类加载器分类

1.1、Bootstrap ClassLoader

Bootstrap ClassLoader为根类加载器,负责加载java的核心类库。根加载器不是ClassLoader的子类,是有C++实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class BootstrapTest {  
    public static void main(String[] args) {
        //获取根类加载器所加载的全部URL数组
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        Arrays.stream(urLs).forEach(System.out::println);
    }
}
//输出结果
//file:/C:/SorftwareInstall/java/jdk/jre/lib/resources.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/rt.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/sunrsasign.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jsse.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jce.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/charsets.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jfr.jar
//file:/C:/SorftwareInstall/java/jdk/jre/classes

根类加载器负责加载%JAVA_HOME%/jre/lib下的jar包(以及由虚拟机参数 -Xbootclasspath 指定的类)。

我们将rt.jar解压,可以看到我们经常使用的类库就在这个jar包中。

1.2 、Extension ClassLoader

Extension ClassLoader为扩展类加载器,负责加载%JAVA_HOME%/jre/ext或者java.ext.dirs系统熟悉指定的目录的jar包。大家可以将自己写的工具包放到这个目录下,可以方便自己使用。

1.3、 System ClassLoader

System ClassLoader为系统(应用)类加载器,负责加载加载来自java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader.getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器默认都以系统类加载器作为父加载器。

四、类加载机制

1.1、JVM主要的类加载机制。

  1. 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也由该类加载器负责载入,除非显示使用另一个类加载器来载入。
  2. 父类委托(双亲委派):先让父加载器试图加载该Class,只有在父加载器无法加载时该类加载器才会尝试从自己的类路径中加载该类。
  3. 缓存机制:缓存机制会将已经加载的class缓存起来,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存中不存在该Class时,系统才会读取该类的二进制数据,并将其转换为Class对象,存入缓存中。这就是为什么更改了class后,需要重启JVM才生效的原因。

注意:类加载器之间的父子关系并不是类继承上的父子关系,而是实例之间的父子关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码public class ClassloaderPropTest {  
    public static void main(String[] args) throws IOException {
        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("系统类加载器:" + systemClassLoader);
        /*
        获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定,如果操作系统没有指定
        CLASSPATH环境变量,则默认以当前路径作为系统类加载器的加载路径
         */
        Enumeration<URL> eml = systemClassLoader.getResources("");
        while (eml.hasMoreElements()) {
            System.out.println(eml.nextElement());
        }
        //获取系统类加载器的父类加载器,得到扩展类加载器
        ClassLoader extensionLoader = systemClassLoader.getParent();
        System.out.println("系统类的父加载器是扩展类加载器:" + extensionLoader);
        System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
        System.out.println("扩展类加载器的parant:" + extensionLoader.getParent());
    }
}
//输出结果
//系统类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
//file:/C:/ProjectTest/FengKuang/out/production/FengKuang/
//系统类的父加载器是扩展类加载器:sun.misc.Launcher$ExtClassLoader@1540e19d
//扩展类加载器的加载路径:C:\SorftwareInstall\java\jdk\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
//扩展类加载器的parant:null

从输出中验证了:系统类加载器的父加载器是扩展类加载器。但输出中扩展类加载器的父加载器是null,这是因为父加载器不是java实现的,是C++实现的,所以获取不到。但扩展类加载器的父加载器是根加载器。

1.2、类加载流程图

图中红色部分,可以是我们自定义实现的类加载器来进行加载。

五、创建并使用自定义类加载器

1、自定义类加载分析

除了根类加载器,所有类加载器都是ClassLoader的子类。所以我们可以通过继承ClassLoader来实现自己的类加载器。

ClassLoader类有两个关键的方法:

  1. protected Class loadClass(String name, boolean resolve):name为类名,resove如果为true,在加载时解析该类。
  2. protected Class findClass(String name) :根据指定类名来查找类。

所以,如果要实现自定义类,可以重写这两个方法来实现。但推荐重写findClass方法,而不是重写loadClass方法,因为loadClass方法内部回调用findClass方法。

我们来看一下loadClass的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码protected Class<?> loadClass(String name, boolean resolve)  
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //第一步,先从缓存里查看是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                //第二步,判断父加载器是否为null
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                   //第三步,如果前面都没有找到,就会调用findClass方法
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                   sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

loadClass加载方法流程:

  1. 判断此类是否已经加载;
  2. 如果父加载器不为null,则使用父加载器进行加载;反之,使用根加载器进行加载;
  3. 如果前面都没加载成功,则使用findClass方法进行加载。

所以,为了不影响类的加载过程,我们重写findClass方法即可简单方便的实现自定义类加载。

2、实现自定义类加载器

基于以上分析,我们简单重写findClass方法进行自定义类加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
java复制代码public class Hello {  
   public void test(String str){
       System.out.println(str);
   }
}

public class MyClassloader extends ClassLoader {

    /**
     * 读取文件内容
     *
     * @param fileName 文件名
     * @return
     */
    private byte[] getBytes(String fileName) throws IOException {
        File file = new File(fileName);
        long len = file.length();
        byte[] raw = new byte[(int) len];
        try (FileInputStream fin = new FileInputStream(file)) {
            //一次性读取Class文件的全部二进制数据
            int read = fin.read(raw);
            if (read != len) {
                throw new IOException("无法读取全部文件");
            }
            return raw;
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        //将包路径的(.)替换为斜线(/)
        String fileStub = name.replace(".", "/");
        String classFileName = fileStub + ".class";
        File classFile = new File(classFileName);

        //如果Class文件存在,系统负责将该文件转换为Class对象
        if (classFile.exists()) {
            try {
                //将Class文件的二进制数据读入数组
                byte[] raw = getBytes(classFileName);
                //调用ClassLoader的defineClass方法将二进制数据转换为Class对象
                clazz = defineClass(name, raw, 0, raw.length);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //如果clazz为null,表明加载失败,抛出异常
        if (null == clazz) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

    public static void main(String[] args) throws Exception {
        String classPath = "loader.Hello";
        MyClassloader myClassloader = new MyClassloader();
        Class<?> aClass = myClassloader.loadClass(classPath);
        Method main = aClass.getMethod("test", String.class);
        System.out.println(main);
        main.invoke(aClass.newInstance(), "Hello World");
    }
}
//输出结果
//Hello World

ClassLoader还有一个重要的方法defineClass(String name, byte[] b, int off, int len)。此方法的作用是将class的二进制数组转换为Calss对象。

此例子很简单,我写了一个Hello测试类,并且编译过后放在了当前路径下(大家可以在findClass中加入判断,如果没有此文件,可以尝试查找.java文件,并进行编译得到.class文件;或者判断.java文件的最后更新时间大于.class文件最后更新时间,再进行重新编译等逻辑)。

六、总结

本篇从类加载的三大阶段:加载、链接、初始化开始细说每个阶段的过程;详细讲解了JVM常用的类加载器的区别与联系,以及类加载机制流程,最后通过自定义的类加载器例子结束本篇。小弟能力有限,大家看出有问题请指出,让博主学习改正。欢迎讨论啊。

注意:本篇博客总结主要来源。如有转载,请注明出处

  1. 《疯狂java讲义(第3版)》
  2. 《深入理解java虚拟机++JVM高级特性与最佳实践》

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Spring】HttpMessageConverter的作

发表于 2019-06-12

相信使用过Spring的开发人员都用过@RequestBody、@ResponseBody注解,可以直接将输入解析成Json、将输出解析成Json,但HTTP 请求和响应是基于文本的,意味着浏览器和服务器通过交换原始文本进行通信,而这里其实就是HttpMessageConverter发挥着作用。

HttpMessageConverter

Http请求响应报文其实都是字符串,当请求报文到java程序会被封装为一个ServletInputStream流,开发人员再读取报文,响应报文则通过ServletOutputStream流,来输出响应报文。

从流中只能读取到原始的字符串报文,同样输出流也是。那么在报文到达SpringMVC / SpringBoot和从SpringMVC / SpringBoot出去,都存在一个字符串到java对象的转化问题。这一过程,在SpringMVC / SpringBoot中,是通过HttpMessageConverter来解决的。HttpMessageConverter接口源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public interface HttpMessageConverter<T> {

boolean canRead(Class<?> clazz, MediaType mediaType);

boolean canWrite(Class<?> clazz, MediaType mediaType);

List<MediaType> getSupportedMediaTypes();

T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;

void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}

下面以一例子来说明

1
2
3
4
5
复制代码@RequestMapping("/test")
@ResponseBody
public String test(@RequestBody String param) {
return "param '" + param + "'";
}

在请求进入test方法前,会根据@RequestBody注解选择对应的HttpMessageConverter实现类来将请求参数解析到param变量中,因为这里的参数是String类型的,所以这里是使用了StringHttpMessageConverter类,它的canRead()方法返回true,然后read()方法会从请求中读出请求参数,绑定到test()方法的param变量中。

同理当执行test方法后,由于返回值标识了@ResponseBody,SpringMVC / SpringBoot将使用StringHttpMessageConverter的write()方法,将结果作为String值写入响应报文,当然,此时canWrite()方法返回true。

借用下图简单描述整个过程:

在Spring的处理过程中,一次请求报文和一次响应报文,分别被抽象为一个请求消息HttpInputMessage和一个响应消息HttpOutputMessage。

处理请求时,由合适的消息转换器将请求报文绑定为方法中的形参对象,在这里同一个对象就有可能出现多种不同的消息形式,如json、xml。同样响应请求也是同样道理。

在Spring中,针对不同的消息形式,有不同的HttpMessageConverter实现类来处理各种消息形式,至于各种消息解析实现的不同,则在不同的HttpMessageConverter实现类中。

替换@ResponseBody默认的HttpMessageConverter

这里使用SpringBoot演示例子,在SpringMVC / SpringBoot中@RequestBody这类注解默认使用的是jackson来解析json,看下面例子:

1
2
3
4
5
6
7
8
9
10
11
复制代码@Controller
@RequestMapping("/user")
public class UserController {

@RequestMapping("/testt")
@ResponseBody
public User testt() {
User user = new User("name", 18);
return user;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class User {

private String username;

private Integer age;

private Integer phone;

private String email;

public User(String username, Integer age) {
super();
this.username = username;
this.age = age;
}
}

浏览器访问/user/testt返回如下:

这就是使用jackson解析的结果,现在来改成使用fastjson解析对象,这里就是替换默认的HttpMessageConverter,就是将其改成使用FastJsonHttpMessageConverter来处理Java对象与HttpInputMessage/HttpOutputMessage间的转化。

首先新建一配置类来添加配置FastJsonHttpMessageConverter,Spring4.x开始推荐使用Java配置加注解的方式,也就是无xml文件,SpringBoot就更是了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
复制代码import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;

import java.nio.charset.Charset;

@Configuration
public class HttpMessageConverterConfig {

//引入Fastjson解析json,不使用默认的jackson
//必须在pom.xml引入fastjson的jar包,并且版必须大于1.2.10
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
//1、定义一个convert转换消息的对象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();

//2、添加fastjson的配置信息
FastJsonConfig fastJsonConfig = new FastJsonConfig();

SerializerFeature[] serializerFeatures = new SerializerFeature[]{
// 输出key是包含双引号
// SerializerFeature.QuoteFieldNames,
// 是否输出为null的字段,若为null 则显示该字段
// SerializerFeature.WriteMapNullValue,
// 数值字段如果为null,则输出为0
SerializerFeature.WriteNullNumberAsZero,
// List字段如果为null,输出为[],而非null
SerializerFeature.WriteNullListAsEmpty,
// 字符类型字段如果为null,输出为"",而非null
SerializerFeature.WriteNullStringAsEmpty,
// Boolean字段如果为null,输出为false,而非null
SerializerFeature.WriteNullBooleanAsFalse,
// Date的日期转换器
SerializerFeature.WriteDateUseDateFormat,
// 循环引用
SerializerFeature.DisableCircularReferenceDetect,
};

fastJsonConfig.setSerializerFeatures(serializerFeatures);
fastJsonConfig.setCharset(Charset.forName("UTF-8"));

//3、在convert中添加配置信息
fastConverter.setFastJsonConfig(fastJsonConfig);

//4、将convert添加到converters中
HttpMessageConverter<?> converter = fastConverter;

return new HttpMessageConverters(converter);
}
}

这里将字符串类型的值如果是null就返回“”,数值类型的如果是null就返回0,重启应用,再次访问/user/testt接口,返回如下:

可以看到此时null都转化成“”或0了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【进阶 7-2 期】深入浅出防抖函数 debounce

发表于 2019-06-11

更新:谢谢大家的支持,最近折腾了一个博客官网出来,方便大家系统阅读,后续会有更多内容和更多优化,猛戳这里查看

—— 以下是正文 ——

引言

上一节我们认识了节流函数 throttle,了解了它的定义、实现原理以及在 underscore 中的实现。这一小节会继续之前的篇幅聊聊防抖函数 debounce,结构是一样的,将分别介绍定义、实现原理并给出了 2 种实现代码并在最后介绍在 underscore 中的实现,欢迎大家拍砖。

有什么想法或者意见都可以在评论区留言,下图是本文的思维导图,高清思维导图和更多文章请看我的 Github。

111

定义及解读

防抖函数 debounce 指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次。假如我们设置了一个等待时间 3 秒的函数,在这 3 秒内如果遇到函数调用请求就重新计时 3 秒,直至新的 3 秒内没有函数调用请求,此时执行函数,不然就以此类推重新计时。

img

举一个小例子:假定在做公交车时,司机需等待最后一个人进入后再关门,每次新进一个人,司机就会把计时器清零并重新开始计时,重新等待 1 分钟再关门,如果后续 1 分钟内都没有乘客上车,司机会认为乘客都上来了,将关门发车。

此时「上车的乘客」就是我们频繁操作事件而不断涌入的回调任务;「1 分钟」就是计时器,它是司机决定「关门」的依据,如果有新的「乘客」上车,将清零并重新计时;「关门」就是最后需要执行的函数。

如果你还无法理解,看下面这张图就清晰多了,另外点击 这个页面 查看节流和防抖的可视化比较。其中 Regular 是不做任何处理的情况,throttle 是函数节流之后的结果(上一小节已介绍),debounce 是函数防抖之后的结果。

image-20190525193539745

原理及实现

实现原理就是利用定时器,函数第一次执行时设定一个定时器,之后调用时发现已经设定过定时器就清空之前的定时器,并重新设定一个新的定时器,如果存在没有被清空的定时器,当定时器计时结束后触发函数执行。

实现 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码// 实现 1
// fn 是需要防抖处理的函数
// wait 是时间间隔
function debounce(fn, wait = 50) {
// 通过闭包缓存一个定时器 id
let timer = null
// 将 debounce 处理结果当作函数返回
// 触发事件回调时执行这个返回函数
return function(...args) {
// 如果已经设定过定时器就清空上一次的定时器
if (timer) clearTimeout(timer)

// 开始设定一个新的定时器,定时器结束后执行传入的函数 fn
timer = setTimeout(() => {
fn.apply(this, args)
}, wait)
}
}

// DEMO
// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000)
// 停止滑动 1 秒后执行函数 () => console.log('fn 防抖执行了')
document.addEventListener('scroll', betterFn)

实现 2

上述实现方案已经可以解决大部分使用场景了,不过想要实现第一次触发回调事件就执行 fn 有点力不从心了,这时候我们来改写下 debounce 函数,加上第一次触发立即执行的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
js复制代码// 实现 2
// immediate 表示第一次是否立即执行
function debounce(fn, wait = 50, immediate) {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)

// ------ 新增部分 start ------
// immediate 为 true 表示第一次触发后执行
// timer 为空表示首次触发
if (immediate && !timer) {
fn.apply(this, args)
}
// ------ 新增部分 end ------

timer = setTimeout(() => {
fn.apply(this, args)
}, wait)
}
}

// DEMO
// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000, true)
// 第一次触发 scroll 执行一次 fn,后续只有在停止滑动 1 秒后才执行函数 fn
document.addEventListener('scroll', betterFn)

实现原理比较简单,判断传入的 immediate 是否为 true,另外需要额外判断是否是第一次执行防抖函数,判断依旧就是 timer 是否为空,所以只要 immediate && !timer 返回 true 就执行 fn 函数,即 fn.apply(this, args)。

加强版 throttle

现在考虑一种情况,如果用户的操作非常频繁,不等设置的延迟时间结束就进行下次操作,会频繁的清除计时器并重新生成,所以函数 fn 一直都没办法执行,导致用户操作迟迟得不到响应。

有一种思想是将「节流」和「防抖」合二为一,变成加强版的节流函数,关键点在于「 wait 时间内,可以重新生成定时器,但只要 wait 的时间到了,必须给用户一个响应」。这种合体思路恰好可以解决上面提出的问题。

给出合二为一的代码之前先来回顾下 throttle 函数,上一小节中有详细的介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码// fn 是需要执行的函数
// wait 是时间间隔
const throttle = (fn, wait = 50) => {
// 上一次执行 fn 的时间
let previous = 0
// 将 throttle 处理结果当作函数返回
return function(...args) {
// 获取当前时间,转换成时间戳,单位毫秒
let now = +new Date()
// 将当前时间和上一次执行函数的时间进行对比
// 大于等待时间就把 previous 设置为当前时间并执行函数 fn
if (now - previous > wait) {
previous = now
fn.apply(this, args)
}
}
}

结合 throttle 和 debounce 代码,加强版节流函数 throttle 如下,新增逻辑在于当前触发时间和上次触发的时间差小于时间间隔时,设立一个新的定时器,相当于把 debounce 代码放在了小于时间间隔部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
js复制代码// fn 是需要节流处理的函数
// wait 是时间间隔
function throttle(fn, wait) {

// previous 是上一次执行 fn 的时间
// timer 是定时器
let previous = 0, timer = null

// 将 throttle 处理结果当作函数返回
return function (...args) {

// 获取当前时间,转换成时间戳,单位毫秒
let now = +new Date()

// ------ 新增部分 start ------
// 判断上次触发的时间和本次触发的时间差是否小于时间间隔
if (now - previous < wait) {
// 如果小于,则为本次触发操作设立一个新的定时器
// 定时器时间结束后执行函数 fn
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
previous = now
fn.apply(this, args)
}, wait)
// ------ 新增部分 end ------

} else {
// 第一次执行
// 或者时间间隔超出了设定的时间间隔,执行函数 fn
previous = now
fn.apply(this, args)
}
}
}

// DEMO
// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log('fn 节流执行了'), 1000)
// 第一次触发 scroll 执行一次 fn,每隔 1 秒后执行一次函数 fn,停止滑动 1 秒后再执行函数 fn
document.addEventListener('scroll', betterFn)

看完整段代码会发现这个思想和上篇文章介绍的 underscore 中 throttle 的实现思想非常相似。

underscore 源码解析

看完了上文的基本版代码,感觉还是比较轻松的,现在来学习下 underscore 是如何实现 debounce 函数的,学习一下优秀的思想,直接上代码和注释,本源码解析依赖于 underscore 1.9.1 版本实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
js复制代码// 此处的三个参数上文都有解释
_.debounce = function(func, wait, immediate) {
// timeout 表示定时器
// result 表示 func 执行返回值
var timeout, result;

// 定时器计时结束后
// 1、清空计时器,使之不影响下次连续事件的触发
// 2、触发执行 func
var later = function(context, args) {
timeout = null;
// if (args) 判断是为了过滤立即触发的
// 关联在于 _.delay 和 restArguments
if (args) result = func.apply(context, args);
};

// 将 debounce 处理结果当作函数返回
var debounced = restArguments(function(args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
// 第一次触发后会设置 timeout,
// 根据 timeout 是否为空可以判断是否是首次触发
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
// 设置定时器
timeout = _.delay(later, wait, this, args);
}

return result;
});

// 新增 手动取消
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};

return debounced;
};

// 根据给定的毫秒 wait 延迟执行函数 func
_.delay = restArguments(function(func, wait, args) {
return setTimeout(function() {
return func.apply(null, args);
}, wait);
});

相比上文的基本版实现,underscore 多了以下几点功能。

  • 1、函数 func 的执行结束后返回结果值 result
  • 2、定时器计时结束后清除 timeout,使之不影响下次连续事件的触发
  • 3、新增了手动取消功能 cancel
  • 4、immediate 为 true 后只会在第一次触发时执行,频繁触发回调结束后不会再执行

小结

  • 函数节流和防抖都是「闭包」、「高阶函数」的应用
  • 函数节流 throttle 指的是某个函数在一定时间间隔内(例如 3 秒)执行一次,在这 3 秒内 无视后来产生的函数调用请求
+ 节流可以理解为养金鱼时拧紧水龙头放水,3 秒一滴
    - 「管道中的水」就是我们频繁操作事件而不断涌入的回调任务,它需要接受「水龙头」安排
    - 「水龙头」就是节流阀,控制水的流速,过滤无效的回调任务
    - 「滴水」就是每隔一段时间执行一次函数
    - 「3 秒」就是间隔时间,它是「水龙头」决定「滴水」的依据
+ 应用:监听滚动事件添加节流函数后,每隔固定的一段时间执行一次
+ 实现方案 1:用时间戳来判断是否已到执行时间,记录上次执行的时间戳,然后每次触发后执行回调,判断当前时间距离上次执行时间的间隔是否已经达到时间差(Xms) ,如果是则执行,并更新上次执行的时间戳,如此循环
+ 实现方案 2:使用定时器,比如当 scroll 事件刚触发时,打印一个 *hello world*,然后设置个 1000ms 的定时器,此后每次触发 scroll 事件触发回调,如果已经存在定时器,则回调不执行方法,直到定时器触发,handler 被清除,然后重新设置定时器
  • 函数防抖 debounce 指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次
+ 防抖可以理解为司机等待最后一个人进入后再关门,每次新进一个人,司机就会把计时器清零并重新开始计时


    - 「上车的乘客」就是我们频繁操作事件而不断涌入的回调任务
    - 「1 分钟」就是计时器,它是司机决定「关门」的依据,如果有新的「乘客」上车,将清零并重新计时
    - 「关门」就是最后需要执行的函数
+ 应用:input 输入回调事件添加防抖函数后,只会在停止输入后触发一次
+ 实现方案:使用定时器,函数第一次执行时设定一个定时器,之后调用时发现已经设定过定时器就清空之前的定时器,并重新设定一个新的定时器,如果存在没有被清空的定时器,当定时器计时结束后触发函数执行

参考

underscore.js

前端性能优化原理与实践

文章穿梭机

  • 【进阶 6-3 期】深入浅出节流函数 throttle
  • 【进阶 6-2 期】深入高阶函数应用之柯里化
  • 【进阶 6-1 期】JavaScript 高阶函数浅析
  • 【进阶 5-3 期】深入探究 Function & Object 鸡蛋问题
  • 【进阶 5-2 期】图解原型链及其继承优缺点
  • 【进阶 5-1 期】重新认识构造函数、原型和原型链

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java自定义异常处理——最佳实践【译】

发表于 2019-06-10

我们几乎已经在我们的每个行业标准应用的代码中处理java自定义异常了。常见的手段是创建一个语义性的继承基础exception类的自定义异常类。

1)Java自定义异常处理 – 新的方法

1.1 传统异常处理

我们的新方法使用静态内部类来处理每个新的异常场景。

传统上我们通过继承Exception类来创建一个DBException。然后每次遇到需要抛出一个与数据库相关异常的时候,我们创建一个DBException的实例,添加一些信息之后抛出它。

现在让我们考虑以下我们需要抛出DBException的场景:

  1. SQL执行错误
  2. 找不到任何一行数据
  3. 当我们只需要一行数据却返回了多行数据
  4. 无效的参数错误
  5. 其它错误

上述方法的问题在于当这些异常在catch块或者应用代码中被处理时,DBException无法提供足够的信息来分别处理上面列出来的异常用例。

1.2 使用内部类的新异常处理

让我们为每一个用例创建一个内部类然后把它们组合到DBException内部来解决上述的问题吧。

首先创建一个抽象的BaseException来作为所有异常类的父类。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码// BaseException.java
public abstract class BaseException extends Exception{
private String message;

public BaseException(String msg)
{
this.message = msg;
}
public String getMessage() {
return message;
}
}

现在创建我们的Exception内部类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
复制代码// DBExeption.java
public class DBExeption
{

public static class BadExecution extends BaseException
{
private static final long serialVersionUID = 3555714415375055302L;
public BadExecution(String msg) {
super(msg);
}
}

public static class NoData extends BaseException
{
private static final long serialVersionUID = 8777415230393628334L;
public NoData(String msg) {
super(msg);
}
}

public static class MoreData extends BaseException
{
private static final long serialVersionUID = -3987707665150073980L;
public MoreData(String msg) {
super(msg);
}
}

public static class InvalidParam extends BaseException
{
private static final long serialVersionUID = 4235225697094262603L;
public InvalidParam(String msg) {
super(msg);
}
}
}

这里我们创建了许多内部类来处理每一种异常情况。你可以根据实际情况随意扩展新的异常内部类。

1.3如何使用自定义异常

为了理解它的作用,现在让我们来让我们创建一个异常然后抛出它。然后我们将会在日志中看见错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码// TestExceptions.java
public class TestExceptions {
public static void main(String[] args)
{
try
{
throw new DBExeption.NoData("No row found for id : x");
}
catch(Exception e)
{
e.printStackTrace();
}
}
}

程序输出:

1
2
3
复制代码Console
com.exception.DBExeption$NoData: No row found for id : x
at com.test.TestExceptions.main(TestExceptions.java:7)

正如你在异常栈中所见的日志消息,它所携带的信息更多更具体了。它清楚展示了错误是什么。在应用代码之中,你也可以通过检查自定义异常实例来做对应的处理。

  1. 使用内部类作为自定义异常类的优点

  1. 最显著的优点在于即使其它开发者写了一些难以读懂的错误信息,你也可以很清楚地弄懂具体错误是什么。
  2. 你可以使用不同的异常实例来处理不同的异常场景
  3. 你不需要使用单个异常来覆盖许多的异常情况
  4. 编写否定的单元测试用例会更加容易
  5. 日志会更加有意义以及高可读性

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

难得的一次技术面——终得小米offer

发表于 2019-06-08

前言

从面试到现在已有一个多月了,面试的问题还记得八九成。端午节前刚从上家离职趁着假期有空把面试问题总结一下。主要是记录一下问题,答案三言两语写不完,网上有蛮多文章讲的挺好所以本文不再展开。

技术栈

java, golang, js, python 主要是前面两个。

面试总结

  1. 其他Java团队leader面
  2. 其他Golang团队leader面
  3. 本团队leader面
  4. 总监面

之所以把面试总结放前面是因为头一次写文章没经验下面排版太朴素了,怕大家看不下去所以先简单几句话概况。面试总共4小时,4轮。回忆了一下大概回答了90~95%左右,总体感觉还可以。给我的印象这几个面试官都很专业,问题质量也挺高,人也很nice。

这次面试像是照镜子知道了自身的不足,接下来还需扎实沉淀技术。加强算法能力。理解源码。努力不负每次机会。

2年前从sz回到wh 然后才感受到sz的好。属于后知后觉类型。看着以前用友的同事们一个个跳的都挺棒(阿里、腾讯、阿里、百度、京东、美团、网易等等)自己再看下自己感觉在荒野求生。

没啥爱好这几年除了玩玩游戏就是看看技术,也没考虑过人生追求,但是最近半年思考了一下人生突然知道了自己想要什么(都二十八九岁马上奔三了这也太后知后觉了)

统计下数据算个帐

19年wh 投了50+ 面了4个 拿到2个。。大部分是不回复。

17年wh 投了10多个 面了7个 拿到4个。(后来某种原因拒绝了讯飞的offer、选择了某公司)

12~16年sz 数据不记得了。虽然自己是个弱鸡但是感觉工作挺好找。(16年用友)

所以19年wh真是神奇,我一度怀疑是简历太水了,发给sz、hz朋友看他们说还好啊没问题。难道是特朗普贸易战搞的招聘困难?【微笑】

本文标题怎讲

讲个真实的笑话,前一阵子在wh面一家小公司架构师职位
场景:4个人同时面我,一个大桌子,总经理办公室

总经理:你要不要做销售啊 以前做过销售吗?

我:尴尬的说做销售太难了我没这个能力。(【黑人问号】为啥一开始就跟我聊这个,难道我看起来这么没有技术含量?我投了几十个简历了兴奋的来这里你跟我说这?这对的起我认真的态度吗?)

总经理:哦!你公司做硬件产品的啊!(我简历里有写NLP机器人项目)

我:“这是个软件产品”(what the f**k,大哥哥你对我有严重的误解。)

总经理:那就让技术总监来面一下吧(估计是让他没面子了)

开始技术面

他: 介绍一下你做的项目

我: 介绍某个项目的背景,说了下项目是干啥用的,简单提了一下技术栈 大概5~6分钟 因为要互动所以不能一个人说太久

他:GRPC是干什么用的,你们为什么不用http restful来做呢

我:远程调用 基于http2 有4中调用方式 序列化协议采用protobuf(心里想这么多内容了你一个个的问吧)

他:好的,我技术面完了

总经理补了几句总结性的客套话 面试结束

【WTF 这就结束了?】这才15分钟不到?我就这样被淘汰了?嗯是的【微笑】

ok 综上所述 抱着终于得到了一次技术面的心情写下这个题目。

一面 Java

主要是Java基础 框架原理

  1. 详述线程池构造方法有哪些各有什么用、ctl、allowCoreThreadTimeOut变量的作用,初始化阶段、大量提交任务阶段、执行完所有任务阶段这写过程。(addWorker过程和其它部分回答得不错 runWorker getTask的一些细节回答的不好。线程池是Java躲不开的问题 网上有很多答案不再细说。)
  2. HashMap数据结构,resize过程,如果多线程去操作会出现哪些问题,1.7和1.8有什么变化,既然提到了红黑树那么来聊聊它和BST、AVL各自有啥特点有啥区别,说一下平衡过程(ok。这个是基础内容网上一大把答案不再细说)
  3. 接下来聊聊concurrenthashmap怎样保证线程安全的1.7和1.8区别(ok)
  4. 线程有几种状态,sleep wait 区别(ok)
  5. synchronized Lock区别,synchronized工作原理对象头、JVM中锁的优化,再聊聊并发包的AQS、公平锁非公平锁 读写锁、CAS和底层的unsafe(ok)
  6. JVM内存结构,堆的内存结构哪些是线程共享的呢,使用过javap命令吗结合这个命令你个谈谈对JVM内存各个区的理解。调优相关。(ok)
  7. 聊一下GC,可达性分析算法、哪些对象可以作为GC ROOT,根据新生代老年代特点的不同来说一下他们适合使用哪些垃圾回收算法。对比一下标记清除和标记整理。(ok)
  8. 类加载器,双亲委派,安全沙箱机制(ok)
  9. 聊聊IO吧,BIO、NIO,IO模型,jvm怎样实现NIO的呢(ok。还好之前略看了一下JVM这一块的c c++代码。多路复用 非阻塞之类的就不细说了。说几个关键点,IO模型参照《unix网络编程》。select、poll、epoll。fcntl)
  10. 巴拉巴拉聊项目牵扯出一堆问题 一致性hash算法、分布式事物、Service Mesh实践、rabbitmq(基本ok)
  11. TCP滑动窗口 ACK机制。(ok)
  12. zuul、hystrix、feign工作原理,springmvc工作原理(ok)
  13. 举例说明spring中使用到的设计模式(ok,掘金有这个文章)
  14. git使用规范、gitflow(ok)
  15. dubbo 相关问题(ok)

二面 golang

  1. Golang 的并发模型(回答的一般 M、P、G)
  2. 聊聊gin源码 路由、group、middleware &设计模式(路由的实现用到了前缀树这个回答得不好 ,其它ok)
  3. grpc4种调用方式,你看过grpc golang版源码 client –> server这个过程你讲一下。protobuf协议 GRPC 性能优化,http2 (ok,这个印象深刻在上家公司时候还做过分享)
  4. cap原理,注册中心选型AP or CP。
  5. etcd是CAP的哪种?etcd数据一致性算法是?详述raft协议,发生分区隔离之后会怎样,隔离恢复之后怎样保证数据一致性?(ok,说raft之前先说了一下Paxos 后来引出了Raft 对比一下,然后开始讲raft各种情况 极力推荐去这里看看 raft.github.io/)
  6. 你说看过源码那聊一下gomicro吧(说了一下各种组件每种适合做什么事,讲了一下我们的项目中微服务用到了哪些,上家公司gomicro技术栈是我推动的所以这里回答得还可以。)
  7. 聊一下你们没有采用gomicro的时候你们的微服务是怎样实践的(注册中心发现用的etcd,lease续期、服务降级、限流、熔断、缓存、一致性hash等负载均衡策略实现,GRPC,golang版rabbitmq客户端&断网重连。不像spring cloud或gomicro很多现成的可以用这些都是手撸 当然这是我的团队共同完成的,这过程中收获蛮多所以不要排斥学习另一门语言或者技术栈 往往会对自己打开一扇窗)
  8. golang压测pprof,火焰图,结合项目讲一下架构推演 性能提升点(OK)
  9. 你们项目结构和依赖管理(OK,这里不得不吐槽一下 godep、glide真的不如maven好用)
  10. 遇到过哪些坑(使用etcd过程中遇到的坑,使用不当造成的协程泄露以及如何避免的,watch、lease、空间压缩、等)
  11. 使用golang你印象深刻的是啥(协程,chan、select这是绝壁是巨好用的,defer,panic&reverse)
  12. golang编码规范、日志规范(这里要提一句规范很重要,架构演进的时候做重构深有体会,然而并没有很统一的规范像Java阿里规范那种,对于分包官方并没有给出一个推荐的目录划分方式 所以google然后根据自身体会制定了一个团队内部规范 后来发现掘金有一篇文章有所共鸣并严重赞同 draveness.me/golang-101 日志框架并没有Java直接采用slf4j 下面用 log4j2或logback那么果断,选之又选决定用logrus,团队的QL同学根据需求定制了)

三面 综合

  1. kafka工作原理、零拷贝、分组协调器工作原理、offset相关问题
  2. 链路追踪 SkyWalking、zipkin 各自特点和实现原理,Java探针
  3. mysql 锁、索引、事务,大数据量优化 分表分库方案
  4. redis线程模型 skiplist ziplist数据结构,持久化方案 rdb快照备份的过程(copy-on-write这个回答的不好 我只知道Java的copyonwrite ,Linux fork进程具体操作不清楚)。淘汰策略,缓存穿透 大规模失效解决方案
  5. 手写代码算法题 假设有两种操作符*和- *代表×2 -代表减一,给你两个数 a,b 要求计算出从a经过这两种运算得到b最少多少步 (ok,算法不是我强项,这题刷算法的时候也没做过还好给的比较简单,大概10多秒有思路 几分钟写了一下不太完整 让我讲了讲思路 使用二叉树去做)
  6. 聊一聊感兴趣的技术 未来发展方向(技术方面)

四面 总监

不详述了 就是以下这些问题

职业规划、为什么跳槽、兴趣爱好、了解一下性格、自我评价上一份工作经历等,我也问了一些问题互动了一下。

面试结束,回去等HR通知。

最终拿到offer

这两年的经历一句话概括。

对就是这样,不要看轻一个人。

最后祝团队剩下的小伙伴猥琐发育。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

利用FastDFS搭建文件服务Docker一键启动集成版 -

发表于 2019-06-06

一、什么是FastDFS

最近公司业务需求,需要搭建文件服务器,经过各种咨询和搜索,决定使用FastDFS。那FastDFS有什么优点呢?

FastDFS是用c语言编写的一款开源的分布式文件系统。FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。

更详细的大家可以参考:来自ityouknow的文章: 点击查看

二、如何使用docker镜像

为了更方便地集成到现有服务里,我们需要一个可以一键运行的docker镜像,但是网络上找了很多,有的把libfastcommon分离了,有的配置繁琐,都没有很好整合或者不满足业务需求。因此,接下来我们一步一步来写这个dockerfile来实现一键运行。

如果你比较懒,直接run下面的镜像就可以:

1
2
3
4
5
6
复制代码docker run -d annoak/fastdfs:latest \
-p 8888:8888 \
-p 22122:22122 \
-e TZ=Asia/Shanghai \
-v /data/fdfs:/var/local/fdfs \
--restart=always

如果需要自定义端口

1
2
3
4
5
6
7
8
复制代码docker run -d annoak/fastdfs:latest \
-p 8888:8888 \
-p 22122:22122 \
-e TZ=Asia/Shanghai \
-e NGINX_PORT=8888 \
-e FDFS_PORT=22122 \
-v /data/fdfs:/var/local/fdfs \
--restart=always

如果需要自定义HOST

  • 第一种:[传入网络名,自动获取IP]
1
2
3
4
5
6
7
复制代码docker run -d annoak/fastdfs:latest \
-p 8888:8888 \
-p 22122:22122 \
-e TZ=Asia/Shanghai \
-e NET_VAR=eth0 \
-v /data/fdfs:/var/local/fdfs \
--restart=always
  • 第二种:[直接传入IP]
1
2
3
4
5
6
7
复制代码docker run -d annoak/fastdfs:latest \
-p 8888:8888 \
-p 22122:22122 \
-e TZ=Asia/Shanghai \
-e HOST_IP=xxx.xxx.xxx.xxx \
-v /data/fdfs:/var/local/fdfs \
--restart=always

如果不想映射端口直接使用宿主机网络:

1
2
3
4
5
复制代码docker run -d annoak/fastdfs:latest \
--net=host \
-e TZ=Asia/Shanghai \
-v /data/fdfs:/var/local/fdfs \
--restart=always

如果需要自定义nginx版本

1
2
3
4
5
6
复制代码docker run -d annoak/fastdfs:latest \
--net=host \
-e TZ=Asia/Shanghai \
-e NGINX_VERSION=1.17.0 \
-v /data/fdfs:/var/local/fdfs \
--restart=always

如果你使用的是docker-compose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码services:
fastdfs:
image: annoak/fastdfs:latest
container_name: my-fastdfs
environment:
- TZ=Asia/Shanghai
- NGINX_PORT=8888
- FDFS_PORT=22122
- HOST_IP=192.168.198.107
- NET_VAR=eth0
volumes:
- /data/fdfs:/var/local/fdfs
#ports:# 如果用host网络,无需映射
# - "8888:8888"
# - "22122:22122"
# 把root权限带进去
privileged: true
network_mode: "host"
restart: always

如果你想知道它是怎么来的,请继续往下看:

三、如何编写dockerfile

  • 第1步,当然是创建一个dockerfile文件,然后去看看我们需要用的软件版本。

依赖版本:libfastcommon和fastdfs的版本为master分支,nginx默认1.17.0,你懂的。

具体版本查看:

libfastcommon: 点击跳转

fastdfs: 点击跳转

nginx: 点击跳转

  • 第2步,我们需要选择一个环境,嗯,我们选择最小的Linux -> alpine,现在dockerfile是这样的:
1
复制代码FROM alpine:3.7
  • 第3步,定义几个全局变量吧
1
2
3
4
5
6
7
8
9
10
复制代码# 工作目录
ENV HOME=/root/fastdfs \
# nginx版本
NGINX_VERSION=1.17.0 \
# nginx端口默认值
NGINX_PORT=8888 \
# IP所在网络默认值
NET_VAR=eth0 \
# fastdfs端口默认值
FDFS_PORT=22122
  • 第4步,我们需要安装一些编译期间的依赖软件,顺便更新下系统软件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码# 创建目录
RUN mkdir -p ${HOME}

# 升级软件包
RUN apk update

# 安装必要的软件,加上--virtual .mybuilds,
RUN apk add --no-cache --virtual .mybuilds \
bash \
gcc \
make \
linux-headers \
curl \
gnupg \
gd-dev \
pcre-dev \
zlib-dev \
libc-dev \
libxslt-dev \
openssl-dev \
geoip-dev

为什么要加–virtual .mybuilds,它是什么?
当您安装软件包时,这些软件包不会添加到全局软件包中。而且可以很容易地恢复。比如我需要gcc来编译程序,但是一旦程序被编译,我就不再需要gcc了。

  • 第5步,我们需要检查一下目录,然后下载fastdfs的依赖libfastcommon并且编译安装
1
2
3
4
5
6
7
复制代码# 下载、安装libfastcommon
RUN cd ${HOME}/ \
&& curl -fSL https://github.com/happyfish100/libfastcommon/archive/master.tar.gz -o fastcommon.tar.gz \
&& tar zxf fastcommon.tar.gz \
&& cd ${HOME}/libfastcommon-master/ \
&& ./make.sh \
&& ./make.sh install
  • 第6步,下载、编译、安装fastdfs
1
2
3
4
5
6
7
复制代码# 下载、安装fastdfs
RUN cd ${HOME}/ \
&& curl -fSL https://github.com/happyfish100/fastdfs/archive/master.tar.gz -o fastfs.tar.gz \
&& tar zxf fastfs.tar.gz \
&& cd ${HOME}/fastdfs-master/ \
&& ./make.sh \
&& ./make.sh install
  • 第7步,配置一下fastdfs, 替换一下里面的路径/home/yuqing/fastdfs
1
2
3
4
5
6
7
8
复制代码# 配置fastdfs
RUN cd /etc/fdfs/ \
&& cp storage.conf.sample storage.conf \
&& sed -i "s|/home/yuqing/fastdfs|/var/local/fdfs/storage|g" /etc/fdfs/storage.conf \
&& cp tracker.conf.sample tracker.conf \
&& sed -i "s|/home/yuqing/fastdfs|/var/local/fdfs/tracker|g" /etc/fdfs/tracker.conf \
&& cp client.conf.sample client.conf \
&& sed -i "s|/home/yuqing/fastdfs|/var/local/fdfs/storage|g" /etc/fdfs/client.conf
  • 第8步,好了,接下来我们安装一下nginx和nginx插件
1
2
3
4
5
6
7
8
9
复制代码# 下载nginx插件
RUN cd ${HOME}/ \
&& curl -fSL https://github.com/happyfish100/fastdfs-nginx-module/archive/master.tar.gz -o nginx-module.tar.gz \
&& tar zxf nginx-module.tar.gz

# 下载nginx
RUN cd ${HOME}/ \
&& curl -fSL http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz -o nginx-${NGINX_VERSION}.tar.gz \
&& tar zxf nginx-${NGINX_VERSION}.tar.gz
  • 第9步,我们把nginx两兄弟一起编译安装下
1
2
3
4
5
6
复制代码# 将nginx和fastdfs的nginx插件编译
RUN cd ${HOME} \
&& chmod u+x ${HOME}/fastdfs-nginx-module-master/src/config \
&& cd nginx-${NGINX_VERSION} \
&& ./configure --add-module=${HOME}/fastdfs-nginx-module-master/src \
&& make && make install

这时候,你大概会报错:No such file or directory #include “common_define.h” 这很致命,怎么解决呢?

我们需要改一下源码里的fastdfs-nginx-module-master/src/config文件
在dockerfile同目录下,新建一个文件:config,写入如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码ngx_addon_name=ngx_http_fastdfs_module
if test -n "${ngx_module_link}"; then
ngx_module_type=HTTP
ngx_module_name=$ngx_addon_name
ngx_module_incs="/usr/local/include"
ngx_module_libs="-lfastcommon -lfdfsclient"
ngx_module_srcs="$ngx_addon_dir/ngx_http_fastdfs_module.c"
ngx_module_deps=
CFLAGS="$CFLAGS -D_FILE_OFFSET_BITS=64 -DFDFS_OUTPUT_CHUNK_SIZE='256*1024' -DFDFS_MOD_CONF_FILENAME='\"/etc/fdfs/mod_fastdfs.conf\"'"
. auto/module
else
HTTP_MODULES="$HTTP_MODULES ngx_http_fastdfs_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_fastdfs_module.c"
CORE_INCS="$CORE_INCS /usr/local/include"
CORE_LIBS="$CORE_LIBS -lfastcommon -lfdfsclient"
CFLAGS="$CFLAGS -D_FILE_OFFSET_BITS=64 -DFDFS_OUTPUT_CHUNK_SIZE='256*1024' -DFDFS_MOD_CONF_FILENAME='\"/etc/fdfs/mod_fastdfs.conf\"'"
fi

然后在dockerfile中编译那段之前,添加

1
2
复制代码# 修复编译找不到 #include "common_define.h"的致命问题
ADD ./config ${HOME}/fastdfs-nginx-module-master/src
  • 第10步,配置nginx和fastdfs环境,配置一下nginx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码# 配置nginx和fastdfs环境,配置nginx
RUN cp ${HOME}/fastdfs-nginx-module-master/src/mod_fastdfs.conf /etc/fdfs/ \
&& sed -i "s|^store_path0.*$|store_path0=/var/local/fdfs/storage|g" /etc/fdfs/mod_fastdfs.conf \
&& sed -i "s|^url_have_group_name=.*$|url_have_group_name=true|g" /etc/fdfs/mod_fastdfs.conf \
&& cd ${HOME}/fastdfs-master/conf/ \
&& cp http.conf mime.types anti-steal.jpg /etc/fdfs/ \
&& echo -e "events {\n\
worker_connections 1024;\n\
}\n\
http {\n\
include mime.types;\n\
default_type application/octet-stream;\n\
server {\n\
listen \$NGINX_PORT;\n\
server_name localhost;\n\
location ~ /group[0-9]/M00 {\n\
ngx_fastdfs_module;\n\
}\n\
}\n\
}">/usr/local/nginx/conf/nginx.conf

第11步,我们很接近成功了,先清理下我们不会再用到的文件和软件

1
2
3
4
复制代码# 清理临时软件和文件
RUN rm -rf ${HOME}/*
RUN apk del .mybuilds
RUN apk add bash pcre-dev zlib-dev

第12步,我们好像就差一个启动脚本了,接下来,写个启动脚本;这里需要注意NET_VAR变量,我在这里踩了个坑,默认是eth0,适合Linux,如果是mac,需要给它赋值en0,或者直接自定义:HOST_IP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码# 创建启动脚本
RUN echo -e "mkdir -p /var/local/fdfs/storage/data /var/local/fdfs/tracker; \n\
ln -s /var/local/fdfs/storage/data/ /var/local/fdfs/storage/data/M00; \n\n\
sed -i \"s/listen\ .*$/listen\ \$NGINX_PORT;/g\" /usr/local/nginx/conf/nginx.conf; \n\
sed -i \"s/http.server_port=.*$/http.server_port=\$NGINX_PORT/g\" /etc/fdfs/storage.conf; \n\
if [ \"\$HOST_IP\" = \"\" ]; then \n\
HOST_IP=\$(ifconfig \$NET_VAR | grep \"inet\" | grep -v \"inet6\" | awk '{print \$2}' | awk -F: '{print \$2}')\n\
fi \n\
sed -i \"s/^tracker_server=.*$/tracker_server=\$HOST_IP:\$FDFS_PORT/g\" /etc/fdfs/storage.conf; \n\
sed -i \"s/^tracker_server=.*$/tracker_server=\$HOST_IP:\$FDFS_PORT/g\" /etc/fdfs/client.conf; \n\
sed -i \"s/^tracker_server=.*$/tracker_server=\$HOST_IP:\$FDFS_PORT/g\" /etc/fdfs/mod_fastdfs.conf; \n\
/etc/init.d/fdfs_trackerd start; \n\
/etc/init.d/fdfs_storaged start; \n\
/usr/local/nginx/sbin/nginx; \n\
tail -f /usr/local/nginx/logs/access.log \
">/start.sh \
&& chmod u+x /start.sh

第13步,好了,暴露端口,配置启动脚本,收工

1
2
3
复制代码# 暴露端口
EXPOSE ${NGINX_PORT} ${FDFS_PORT}
ENTRYPOINT ["/bin/bash","/start.sh"]
  • 总结:完整的dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
复制代码FROM alpine:3.7

MAINTAINER oak <ritj@163.com>

# 定义变量
ENV HOME=/root/fastdfs \
# nginx版本
NGINX_VERSION=1.17.0 \
# nginx端口默认值
NGINX_PORT=8888 \
# IP所在网络
NET_VAR=eth0 \
# fastdfs端口默认值
FDFS_PORT=22122

# 创建目录
RUN mkdir -p ${HOME}

# 升级软件包
RUN apk update

# 安装必要的软件
RUN apk add --no-cache --virtual .mybuilds \
bash \
gcc \
make \
linux-headers \
curl \
gnupg \
gd-dev \
pcre-dev \
zlib-dev \
libc-dev \
libxslt-dev \
openssl-dev \
geoip-dev

# 下载、安装libfastcommon
RUN cd ${HOME}/ \
&& curl -fSL https://github.com/happyfish100/libfastcommon/archive/master.tar.gz -o fastcommon.tar.gz \
&& tar zxf fastcommon.tar.gz \
&& cd ${HOME}/libfastcommon-master/ \
&& ./make.sh \
&& ./make.sh install

# 下载、安装fastdfs
RUN cd ${HOME}/ \
&& curl -fSL https://github.com/happyfish100/fastdfs/archive/master.tar.gz -o fastfs.tar.gz \
&& tar zxf fastfs.tar.gz \
&& cd ${HOME}/fastdfs-master/ \
&& ./make.sh \
&& ./make.sh install

# 配置fastdfs
RUN cd /etc/fdfs/ \
&& cp storage.conf.sample storage.conf \
&& sed -i "s|/home/yuqing/fastdfs|/var/local/fdfs/storage|g" /etc/fdfs/storage.conf \
&& cp tracker.conf.sample tracker.conf \
&& sed -i "s|/home/yuqing/fastdfs|/var/local/fdfs/tracker|g" /etc/fdfs/tracker.conf \
&& cp client.conf.sample client.conf \
&& sed -i "s|/home/yuqing/fastdfs|/var/local/fdfs/storage|g" /etc/fdfs/client.conf

# 下载nginx插件
RUN cd ${HOME}/ \
&& curl -fSL https://github.com/happyfish100/fastdfs-nginx-module/archive/master.tar.gz -o nginx-module.tar.gz \
&& tar zxf nginx-module.tar.gz

# 下载nginx
RUN cd ${HOME}/ \
&& curl -fSL http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz -o nginx-${NGINX_VERSION}.tar.gz \
&& tar zxf nginx-${NGINX_VERSION}.tar.gz

# 修复编译找不到 #include "common_define.h"的致命问题
ADD ./config ${HOME}/fastdfs-nginx-module-master/src

# 将nginx和fastdfs的nginx插件编译
RUN cd ${HOME} \
&& chmod u+x ${HOME}/fastdfs-nginx-module-master/src/config \
&& cd nginx-${NGINX_VERSION} \
&& ./configure --add-module=${HOME}/fastdfs-nginx-module-master/src \
&& make && make install

# 配置nginx和fastdfs环境,配置nginx
RUN cp ${HOME}/fastdfs-nginx-module-master/src/mod_fastdfs.conf /etc/fdfs/ \
&& sed -i "s|^store_path0.*$|store_path0=/var/local/fdfs/storage|g" /etc/fdfs/mod_fastdfs.conf \
&& sed -i "s|^url_have_group_name=.*$|url_have_group_name=true|g" /etc/fdfs/mod_fastdfs.conf \
&& cd ${HOME}/fastdfs-master/conf/ \
&& cp http.conf mime.types anti-steal.jpg /etc/fdfs/ \
&& echo -e "events {\n\
worker_connections 1024;\n\
}\n\
http {\n\
include mime.types;\n\
default_type application/octet-stream;\n\
server {\n\
listen \$NGINX_PORT;\n\
server_name localhost;\n\
location ~ /group[0-9]/M00 {\n\
ngx_fastdfs_module;\n\
}\n\
}\n\
}">/usr/local/nginx/conf/nginx.conf

# 清理临时软件和文件
RUN rm -rf ${HOME}/*
RUN apk del .mybuilds
RUN apk add bash pcre-dev zlib-dev

# 创建启动脚本
RUN echo -e "mkdir -p /var/local/fdfs/storage/data /var/local/fdfs/tracker; \n\
ln -s /var/local/fdfs/storage/data/ /var/local/fdfs/storage/data/M00; \n\n\
sed -i \"s/listen\ .*$/listen\ \$NGINX_PORT;/g\" /usr/local/nginx/conf/nginx.conf; \n\
sed -i \"s/http.server_port=.*$/http.server_port=\$NGINX_PORT/g\" /etc/fdfs/storage.conf; \n\
if [ \"\$HOST_IP\" = \"\" ]; then \n\
HOST_IP=\$(ifconfig \$NET_VAR | grep \"inet\" | grep -v \"inet6\" | awk '{print \$2}' | awk -F: '{print \$2}')\n\
fi \n\
sed -i \"s/^tracker_server=.*$/tracker_server=\$HOST_IP:\$FDFS_PORT/g\" /etc/fdfs/storage.conf; \n\
sed -i \"s/^tracker_server=.*$/tracker_server=\$HOST_IP:\$FDFS_PORT/g\" /etc/fdfs/client.conf; \n\
sed -i \"s/^tracker_server=.*$/tracker_server=\$HOST_IP:\$FDFS_PORT/g\" /etc/fdfs/mod_fastdfs.conf; \n\
/etc/init.d/fdfs_trackerd start; \n\
/etc/init.d/fdfs_storaged start; \n\
/usr/local/nginx/sbin/nginx; \n\
tail -f /usr/local/nginx/logs/access.log \
">/start.sh \
&& chmod u+x /start.sh

# 暴露端口
EXPOSE ${NGINX_PORT} ${FDFS_PORT}

ENTRYPOINT ["/bin/bash","/start.sh"]

文件结构:

四、如何查看日志

脚本一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码#!/bin/bash
STORAGE=/var/local/fdfs/storage/logs/storaged.log
TRACKER=/var/local/fdfs/tracker/logs/trackerd.log
NGINX=/usr/local/nginx/logs/access.log

ID=$(docker ps|grep fastdfs|awk '{print $1}')
USER_PRINT=$1
LOG=""
if [[ "${USER_PRINT}" = "tracker" ]];then
LOG=${TRACKER}
elif [[ "${USER_PRINT}" = "storage" ]]; then
LOG=${STORAGE}
else
LOG=${NGINX}
fi

docker exec -it $ID /usr/bin/tail -f ${LOG}

使用方法(记得给脚本赋执行权):
./log.sh tracker 或 storage 或 nginx

完

没有公众号,也没有二维码,也没有广告,仅此。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

15个经典的Spring面试常见问题

发表于 2019-06-05

我自己总结的Java学习的系统知识点以及面试问题,已经开源,目前已经 41k+ Star。会一直完善下去,欢迎建议和指导,同时也欢迎Star: github.com/Snailclimb/…

这篇文章主要是想通过一些问题,加深大家对于 Spring 的理解,所以不会涉及太多的代码!这篇文章整理了挺长时间,下面的很多问题我自己在使用 Spring 的过程中也并没有注意,自己也是临时查阅了很多资料和书籍补上的。网上也有一些很多关于 Spring 常见问题/面试题整理的文章,我感觉大部分都是互相 copy,而且很多问题也不是很汗,有些回答也存在问题。所以,自己花了一周的业余时间整理了一下,希望对大家有帮助。

什么是 Spring 框架?

Spring 是一种轻量级开发框架,旨在提高开发人员的开发效率以及系统的可维护性。Spring 官网:spring.io/。

我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是:核心容器、数据访问/集成,、Web、AOP(面向切面编程)、工具、消息和测试模块。比如:Core Container 中的 Core 组件是Spring 所有组件的核心,Beans 组件和 Context 组件是实现IOC和依赖注入的基础,AOP组件用来实现面向切面编程。

Spring 官网列出的 Spring 的 6 个特征:

  • 核心技术 :依赖注入(DI),AOP,事件(events),资源,i18n,验证,数据绑定,类型转换,SpEL。
  • 测试 :模拟对象,TestContext框架,Spring MVC 测试,WebTestClient。
  • 数据访问 :事务,DAO支持,JDBC,ORM,编组XML。
  • Web支持 : Spring MVC和Spring WebFlux Web框架。
  • 集成 :远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。
  • 语言 :Kotlin,Groovy,动态语言。

列举一些重要的Spring模块?

下图对应的是 Spring4.x 版本。目前最新的5.x版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。

Spring主要模块

  • Spring Core: 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。主要提供 IOC 依赖注入功能。
  • **Spring Aspects ** : 该模块为与AspectJ的集成提供支持。
  • Spring AOP :提供了面向方面的编程实现。
  • Spring JDBC : Java数据库连接。
  • Spring JMS :Java消息服务。
  • Spring ORM : 用于支持Hibernate等ORM工具。
  • Spring Web : 为创建Web应用程序提供支持。
  • Spring Test : 提供了对 JUnit 和 TestNG 测试的支持。

谈谈自己对于 Spring IoC 和 AOP 的理解

IoC

IoC(Inverse of Control:控制反转)是一种设计思想,就是 将原本在程序中手动创建对象的控制权,交由Spring框架来管理。 IoC 在其他语言中也有应用,并非 Spirng 特有。 IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。

将对象之间的相互依赖关系交给 IOC 容器来管理,并由 IOC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IOC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IOC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。

Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。

推荐阅读:www.zhihu.com/question/23…

Spring IOC的初始化过程:

Spring IOC的初始化过程

IOC源码阅读

  • javadoop.com/post/spring…

AOP

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:

SpringAOPProcess

当然你也可以使用 AspectJ ,Spring AOP 已经集成了AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。

使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。

Spring AOP 和 AspectJ AOP 有什么区别?

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。

Spring 中的 bean 的作用域有哪些?

  • singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。
  • prototype : 每次请求都会创建一个新的 bean 实例。
  • request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
  • session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。
  • global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话

Spring 中的单例 bean 的线程安全问题了解吗?

大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。

常见的有两种解决办法:

  1. 在Bean对象中尽量避免定义可变的成员变量(不太现实)。
  2. 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。

Spring 中的 bean 生命周期?

这部分网上有很多文章都讲到了,下面的内容整理自:yemengying.com/2016/07/14/… ,除了这篇文章,再推荐一篇很不错的文章 :www.cnblogs.com/zrtqsk/p/37… 。

  • Bean 容器找到配置文件中 Spring Bean 的定义。
  • Bean 容器利用 Java Reflection API 创建一个Bean的实例。
  • 如果涉及到一些属性值 利用 set()方法设置一些属性值。
  • 如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入Bean的名字。
  • 如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。
  • 如果Bean实现了 BeanFactoryAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoade r对象的实例。
  • 与上面的类似,如果实现了其他 *.Aware接口,就调用相应的方法。
  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法
  • 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。
  • 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
  • 如果有和加载这个 Bean的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法
  • 当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。
  • 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。

图示:

Spring Bean 生命周期

与之比较类似的中文版本:

Spring Bean 生命周期

说说自己对于 Spring MVC 了解?

谈到这个问题,我们不得不提提之前 Model1 和 Model2 这两个没有 Spring MVC 的时代。

  • Model1 时代 : 很多学 Java 后端比较晚的朋友可能并没有接触过 Model1 模式下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。这个模式下 JSP 即是控制层又是表现层。显而易见,这种模式存在很多问题。比如①将控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;②前端和后端相互依赖,难以进行测试并且开发效率极低;
  • Model2 时代 :学过 Servlet 并做过相关 Demo 的朋友应该了解“Java Bean(Model)+ JSP(View,)+Servlet(Controller) ”这种开发模式,这就是早期的 JavaWeb MVC 开发模式。Model:系统涉及的数据,也就是 dao 和 bean。View:展示模型中的数据,只是用来展示。Controller:处理用户请求都发送给 ,返回数据给 JSP 并展示给用户。

Model2 模式下还存在很多问题,Model2的抽象和封装程度还远远不够,使用Model2进行开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。于是很多JavaWeb开发相关的 MVC 框架营运而生比如Struts2,但是 Struts2 比较笨重。随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。相比于 Struts2 , Spring MVC 使用更加简单和方便,开发效率更高,并且 Spring MVC 运行速度更快。

MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的Web层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为 Service层(处理业务)、Dao层(数据库操作)、Entity层(实体类)、Controller层(控制层,返回数据给前台页面)。

Spring MVC 的简单原理图如下:

SpringMVC 工作原理了解吗?

原理如下图所示:

SpringMVC运行原理

上图的一个笔误的小问题:Spring MVC 的入口函数也就是前端控制器 DispatcherServlet 的作用是接收请求,响应结果。

流程说明(重要):

  1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet。
  2. DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。
  3. 解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。
  4. HandlerAdapter 会根据 Handler来调用真正的处理器开处理请求,并处理相应的业务逻辑。
  5. 处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View。
  6. ViewResolver 会根据逻辑 View 查找实际的 View。
  7. DispaterServlet 把返回的 Model 传给 View(视图渲染)。
  8. 把 View 返回给请求者(浏览器)

Spring 框架中用到了哪些设计模式?

关于下面一些设计模式的详细介绍,可以看笔主前段时间的原创文章《面试官:“谈谈Spring中都用到了那些设计模式?”。》 。

  • 工厂设计模式 : Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
  • 代理设计模式 : Spring AOP 功能的实现。
  • 单例设计模式 : Spring 中的 Bean 默认都是单例的。
  • 模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
  • 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
  • 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
  • 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。
  • ……

@Component 和 @Bean 的区别是什么?

  1. 作用对象不同: @Component 注解作用于类,而@Bean注解作用于方法。
  2. @Component通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了Spring这是某个类的示例,当我需要用它的时候还给我。
  3. @Bean 注解比 Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册bean。比如当我们引用第三方库中的类需要装配到 Spring容器时,则只能通过 @Bean来实现。

@Bean注解使用示例:

1
2
3
4
5
6
7
8
复制代码@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}

}

上面的代码相当于下面的 xml 配置

1
2
3
复制代码<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

下面这个例子是通过 @Component 无法实现的。

1
2
3
4
5
6
7
8
9
10
11
复制代码@Bean
public OneService getService(status) {
case (status) {
when 1:
return new serviceImpl1();
when 2:
return new serviceImpl2();
when 3:
return new serviceImpl3();
}
}

将一个类声明为Spring的 bean 的注解有哪些?

我们一般使用 @Autowired 注解自动装配 bean,要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类,采用以下注解可实现:

  • @Component :通用的注解,可标注任意类为 Spring 组件。如果一个Bean不知道属于拿个层,可以使用@Component 注解标注。
  • @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。
  • @Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。

Spring 管理事务的方式有几种?

  1. 编程式事务,在代码中硬编码。(不推荐使用)
  2. 声明式事务,在配置文件中配置(推荐使用)

声明式事务又分为两种:

  1. 基于XML的声明式事务
  2. 基于注解的声明式事务

Spring 事务中的隔离级别有哪几种?

TransactionDefinition 接口中定义了五个表示隔离级别的常量:

  • TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
  • TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
  • TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

Spring 事务中哪几种事务传播行为?

支持当前事务的情况:

  • TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  • TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)

不支持当前事务的情况:

  • TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。

其他情况:

  • TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

参考

  • 《Spring 技术内幕》
  • www.cnblogs.com/wmyskxz/p/8…
  • www.journaldev.com/2696/spring…
  • www.edureka.co/blog/interv…
  • howtodoinjava.com/interview-q…
  • www.tomaszezula.com/2014/02/09/…
  • stackoverflow.com/questions/3…

公众号

如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。

《Java面试突击》: 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本公众号后台回复 “Java面试突击” 即可免费领取!

Java工程师必备学习资源: 一些Java工程师常用学习资源公众号后台回复关键字 “1” 即可免费无套路获取。

我的公众号

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我对支付平台架构设计的一些思考

发表于 2019-06-05

微信公众号「后端进阶」,专注后端技术分享:Java、Golang、WEB框架、分布式中间件、服务治理等等。

老司机倾囊相授,带你一路进阶,来不及解释了快上车!

我在前一家公司的第一个任务是开发统一支付平台,由于公司的业务需求,需要接入多个第三方支付,之前公司的支付都是散落在各个项目中,及其不利于支付的管理,于是聚合三方支付,统一支付平台的任务就落在我手上,可以说是完全从 0 开始设计,经过一翻实战总结,我得出了一些架构设计上的思考,之前就一直很想把自己的架构设计思路写出来,但一直没动手,前几天在技术群里有人问到相关问题,我觉得有必要把它写出来,以帮助到更多需要开发支付平台的开发人员。

组件模式

由于公司业务在很多地区都有,需要提供多种支付途径,以满足业务的发展,所以设计的支付平台需要接入多种第三方支付渠道,如:微信支付、支付宝支付、PayPal、IPayLinks 等等,我们都知道,每个第三方支付,都有自己一套对外 API,官方都有一套 SDK 来实现这些 API,我们应该如何组织这些 API 呢?

由于第三方支付渠道会随着业务的发展变动,所以组织这些 SDK 就需要在不影响支付平台整体架构的前提下可灵活插拔,这里我使用了组件的思想,将支付 API 拆分成各种组件支付组件、退款组件、订单组件、账单组件等等,那么这样就可以当引入一个第三方支付 SDK 时,可灵活在组件上面添加需要的 API,架构设计如下:

通过 Builder 模式根据请求参数构建对应的组件对象,将组件与外部分离,隐藏组件构建的实现。组件模式 + Builder 模式使得支付平台具备了高扩展性。

多账户体系

在接入各种第三方支付平台,我们当时又遇到一个账户的问题,原因是公司当时的小程序与 APP 使用的是不同的微信账号,因此会出现微信支付会对应到多个账户的问题,而我当时设计支付平台时,没有考虑到这个问题,当时第三方支付只对应了一个账户,而且不同的第三方支付的账户之间相互独立且不统一。

于是我引入了多账户体系,多账户体系最重要的一个核心概念是以账户为粒度,接入多个第三方支付,统一账户的参数,构建了统一的支付账户体系,支付平台无需关心不同支付之间的账户差异以及第三方支付是否有多少个账户。

此时我在支付平台架构图加上账户层:

前端只需要传递 accountId,支付平台就可以根据 accountId 查询出对应的支付账户,然后通过 Builder 模式构建支付账户对应的组件对象,完全屏蔽不同支付之间的差异,在多账户体系里面,可以支持无限多个支付账户,完全满足了公司业务的发展需求。

统一回调与异步分发处理

做过支付开发的同学都知道,目前的第三方支付都有一个特点,就是支付/退款成功后,会有一个支付/退款回调的功能,目的是为了让商户平台自行校验该笔订单是否合法,比如:防止在支付时,客户端恶意篡改金额等参数,那么此时支付成功后,订单会处于支付中状态,需要等待第三方支付的回调,如果此时收到了回调,在校验时发现订单的金额与支付的金额不对,然后将订单改成支付失败,以防止资金损失。回调的思想是只要保证最终的一致性,所以我们调起支付时,并不需要在此时校验参数的正确性,只需要在回调时校验即可。

讲完了回调的目的,那么我们如何来设计支付平台的回调呢?

由于支付平台接入了多个第三方支付,如果此时每个第三方支付设置一个回调地址,那么将会出现多个回调地址,由于回调的 API 必须是暴露出去才能接受第三方的回调请求,所以就会有安全问题,我们必须在 API 外层设置安全过滤,不然很容易出现一些非法暴力访问,所以我们需要统一回调 API,统一做安全校验,之后再进行一层分发。

分发的机制我这里建议用 RocketMQ 来处理,可能有人会问,如果用 RocketMQ 来做分发处理,此时怎么实时返回校验结果到第三方支付呢?这个问题也是我当时一直头疼的问题,以下是我对回调设计的一些思考:

  1. 公司的系统是基于 SpringCloud 微服务架构,微服务之间通过 HTTP 通信,当时有很多个微服务接入了我的支付平台,如果用 HTTP 作分发,可以保证消息返回的实时性,但也会出现一个问题,由于网络不稳定,就会出现请求失败或超时的问题,接口的稳定性得不到保障。
  2. 由于第三方支付如果收到 false 响应,就在接下来一段时间内再次发起回调请求,这么做的目的是为了保证回调的成功率,对于第三方支付来说,这没毛病,但对于商户支付平台来说,也许就是一个比较坑爹的设计,你想一下,假设有一笔订单在支付时恶意篡改了金额,回调校验失败,返回 false 到第三方支付,此时第三方支付会再重复发送回调,无论发送多少次回调,都会校验失败,这就额外增加了不必要的交互,当然这里也可以用幂等作处理,以下是微信支付回调的应用场景说明:

基于以上两点思考,我认为返回 false 到第三方支付是没必要的,为了系统的健壮性,我采用了消息队列来做异步分发,支付平台收到回调请求后直接返回 true,这时你可能会提出一个疑问,如果此时校验失败了,但此时返回 true,会不会出现问题?首先,校验失败情况,订单必定是处于支付失败的状态,此时返回 true 目的是为了减少与第三方支付不必要的远程交互。

因为 RocketMQ 的消息是持久化到磁盘的,所以用消息队列来做异步分发最大的好处,就是可以复查消息队列里面的消息来排查问题,而且消息队列可以在业务的高峰期进行流量削峰。

以下是统一回调与分发处理的架构设计图:

聚合支付

支付平台聚合了多种第三方支付,因此在请求层需要做很多的适配工作,以满足多种支付的需求,可能你会想,直接在适配那里加几行 if else 不就得了吗,这么做也没问题,也可以满足多种支付的需求,但你有没有想过,假设此时再加一个第三方支付,你会怎么做?你只能原有方法上加多个 else 条件,这样就会导致请求层代码不断地随着业务发展改变,使得代码及其不优雅,而且也不好维护,这时我们就得用上策略模式,将这些 if else 代码消除,当我们增加一个第三方支付时,我们只需要新建一个 Strategy 类就可以了,策略模式究竟怎么使用可以看看大话设计模式。

因此我在 Builder 模式前加多了一层支付策略层:

请求处理

由于支付平台涉及到资金,支付的各种请求与返回,以及异常记录在一个支付平台中异常重要,因此我们需要记录每一次的支付请求记录,以便后续排查问题。

基于这点需求,我在开始请求第三方支付之前,设计了一层 Handler 层,所有的请求都必须经过 Handler 层进行处理,Handler 核心方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public K handle(T t) {
K k;
try {
before(t);
k = execute(t);
after(k);
} catch (Exception e) {
exception(t, e);
}
return k;
}
protected abstract void before(T t);
protected abstract void after(K k);
protected abstract void exception(T t, Exception exception);

原则上来说,我设计的 Handler 层,利用了模版模式,不仅仅可以实现日志的记录,还可以实现多种处理方式,比如请求监控,消息推送等等,实现了 Handler 层的高扩展性。

以下是 Handler 层的架构设计图:

写在最后

以上就是我的支付平台架构设计思路,总结来说,支付平台需要具备可扩展性、稳定性、高可用性,因此我在设计支付平台时使用了很多设计模式以及引入消息队列处理回调分发的问题,使得支付平台具备这几点特性,希望能够给你一些启发与帮助,最后我把支付平台整体的架构设计图贴出来:

公众号「后端进阶」,专注后端技术分享!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

简单总结下线程和进程的区别

发表于 2019-06-04

进程和线程是什么?

首先你要理解cpu的概念,计算机上的所有操作都是由cpu来执行的,cpu将要执行的操作分为一个个的任务,这些任务我们就可以理解为进程,而这些任务又分为一些细粒度更小的子任务,这些子任务就称作线程

cpu轮流执行任务的,每一个任务需要经过以下三个阶段:

  • 加载上下文
  • 执行
  • 保存上下文

也就是说,每一个进程从加载、执行,到切换下一个进程执行,都会经历同样的一个过程,我们的cpu就是在无时无刻地进行这样的进程切换操作

而我们又可以把进程分为一些细粒度更小的线程,这些线程之间也可以来回的切换,就像进程一样,但是却不需要加载和保存上下文的操作,因为这些线程都是共享上下文的

进程与线程的区别

这里为大家简单总结了一些进程和线程的区别,如有错误欢迎指正:

  • 性质不同:进程是资源分配的基本单位,线程是cpu执行运算和调度的基本单位
  • 归属不同:一个操作系统中可以有很多进程,一个进程可以有很多线程
  • 开销不同:进程创建、销毁和切换的开销都要远大于线程
  • 拥有资源不同:每个进程都拥有自己的内存和资源,一个进程中的线程会共享这些内存和资源
  • 通信方式不同:进程之间可以通过管道、消息队列、共享内存、信号量,以及Socket等机制实现通信,线程之间主要通过共享变量及其变种形式实现通信
  • 控制和影响能力不同:子进程无法控制父进程,一个进程发生异常时一般不会影响其他进程;子线程可以控制父线程,如果主线程发生异常,会影响其所在进程和其余线程
  • 扩展能力不同:多进程可以方便地扩展到多机分布式系统上,多线程想要扩展到多台机器上就很困难
  • cpu利用率不同:进程的cpu利用率低,因为需要额外的上下文切换开销;线程的cpu利用率高,因为切换简单
  • 可靠性不同:进程的可靠性要高于线程

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

蚂蚁金服终端实验室演进之路

发表于 2019-06-04

作者:周力(问瑾),蚂蚁金服技术专家。本文将从支付宝业务特性出发,深度解析无线实验集群在支付宝的演进与发展,并探讨 IoT 与人机如何交互并提供真正落地的时间方案。

现场视频(复制地址到浏览器中打开):
t.cn/AiKDZg5G

0. 背景

作为国民级 App,支付宝客户端需要为亿级用户提供多元化的服务,因此应用的稳定性与可靠性面临巨大的挑战,需要不断地完善和优化。

今天,让我们站在服务质量的全方位监控与优化的角度,从蚂蚁终端实验室的演进之路展开探讨,从借助使用开源的自动化方案,到自研并逐步完善无线实验集群技术体系,支付宝内部经历了怎样的业务场景演练,以及相应的技术架构如何借助移动开发平台 mPaaS 对外输出。

1. 发展历程

总的来说, 蚂蚁终端实验室从诞生到现在,一共经历过三个阶段(工具化、服务化以及中台化),其每个阶段都有特点和意义:

  • 工具化阶段:

该阶段主要以使用市面上主流开源软件为主,如客户端开源软件 Appium, 其覆盖的端为 Android 和 iOS;通过这种开源工具和 App 测试流程结合的方式,快速满足业务方的提测需求,从而帮助业务方完成一般意义上的自动化测试工作(如基本的功能测试、兼容性测试等)。

  • 服务化阶段:

服务化阶段存在一个重要的背景:支付宝着手前后端研发流程分离,并逐步沉淀出独立的 App 端研发流程系统(研发协作流程与 App 构建流程)。在独立的 App 研发流程和系统的基础上,终端实验室以一种服务化的形式支撑 App 的研发和协作, 处理满足日常用户自动化工作外,同时还担当着持续集成、日常发布前自动验包工作等; 另外在日常发布发布提供质量数据支持,如客户端代码覆盖率统计等。

  • 中台化阶段:

伴随着终端实验室的能力不断提升优化以及测试规模的逐步扩大,服务上不仅需要满足蚂蚁金服体系 App(支付宝、口碑、网商银行等)日常测试需求,而且还需要将能力扩散覆盖到整个阿里巴巴集团的业务。

随之而来的是实验室需要面临多样化的业务方需求和定制化功能,如何在多元复杂的业务环境中,与业务方或者说上游系统完成能力共建?带着这个问题,终端实验室逐步沉淀并着手建设中台化平台:一方面让通用服务不断下沉,另一方面抽象出标准 SDK 的方式,让业务方根据自身业务特点建设特定的能力。

此外,在建设平台化的同时,终端实验室贴合支付宝业务场景的发展,构建如网络实验室、扫码实验室等一系列真实实验室的能力。

经历了几年的不断发展,终端实验室逐步完成了中台化的转变,其端上覆盖了 Android、iOS 以及 IoT 设备,服务上覆盖了通用能力、小程序准入、研发流程建设、真机租用以及用例管控等。

2. 技术生态

在了解完终端实验室的历程之后,我们能够对其提供的服务有一个全面的认识。当我们去总结和分析这些服务时,可以把这些具体能力分为三大块:平台服务能力、客户端SDK 以及 实验室能力。

  • 平台服务能力

平台服务能力的目标是聚焦“如何把蚂蚁实验室构建成一个更为开放的平台”,因此我们需要考虑到如何让更多的业务方和上游系统一起参与能力共建,从而将平台的建设思路分为 2 大部分:设备实验集群和开放SDK。

1. 设备集群

蚂蚁实验室不仅包含数以千计的公用终端设备,覆盖市面绝大多数手机终端,帮助业务同学完成日常自动化测试工作,而且提供了用户自建实验室的方式:用户只需要根据自身业务场景特性进行设备采购、实验室部署,便具备在自有平台上运行自有设备的能力。

从平台的开放性与部署动态化角度看,目前设备集群能保证设备归属和业务场景做到充分隔离,保证各业务在平台使用上能相互独立。另外,面对阿里巴巴集团众多研发中心,设备集群在部署上也支持多地部署、相互隔离。

2. 开放SDK

为了给上游系统和用户提供更为开放的能力,帮助业务方根据自身需求完成能力建设。终端实验室提供开放的 SDK 能力:上游系统只需在自己服务上接入 SDK,就能够完成任务构建链路,从用例管理、设备选择、任务执行,到执行结果回调,在此基础上用户就能够根据自身业务特点将业务数据进行多维度组合,形成自己的能力输出。

  • 客户端 SDK

终端实验室经过几个阶段的发展,不仅提供 UI 自动化框架能力,而且在一些复杂场景做了深入研究和落地的工作。在这里我们以令大家头痛的“App 兼容性验证”作为切入点,结合目前常用的几种机器学习方案,分析方案的优缺点,最终形成了终端实验室的解决方案。

一方面伴随着移动互联网的快速发展,目前市面上手机的品牌和型号层出不穷,如何快速准确的验证 App 的功能在不同类型手机上运行有效性与稳定性,的确是件困难的事情;另一方面,目前针对图片的机器学习技术日益成熟,其图识别的准确性也完全能够满足日常兼容性的要求。

通常来说兼容性测试会采用两种方式:1.图像相似度计算;2. 无监督的异常点聚类。 这两种方式在使用方式和结果输出都有其优缺点:

  • 对于“图像相似度计算”来说,其异常图片的识别成功率非常高,但其前提条件比较苛刻:用户需要对每一版 App 以及每一个业务点进行图片搜集和上传,而往往每条用例可能会包含少则几张图片多则十几张图片,对于几百、甚至几千条测试用例来说,就算是一版 App 的期望图片搜集工作都是巨大的,何况目前移动互联网普遍都是快速迭代发布,所以导致了这种预先处理图片的方式是不太可行的,下图是一般意义以图搜图的数据流:

  • 另一种常用的方案是直接将同一业务场景下不同手机的一组截图交给无监督的异常点聚类算法处理,这种方案的优点比较明显:对于用户和平台来说,没有增加的额外的工作量,操作简单,但带来的问题是,计算出来的结果并不完全可信,特别是在一些极端情况下(如某一类异常图片总数较多的情况),少数正常的图片反而会被识别成异常图片,告知给业务方。

对比以上两种技术方案,终端实验室在兼容性异常图片发现上采用了更加灵活的方案,通过手机端“异常目标检测”和服务端“异常点聚类”相结合的方式完成目标。

首先,平台搜集常见异常图片,并训练成模型,植入手机端。

其次,当用户执行兼容性测试的时候,在手机端完成一部分“常见异常图片”的发现工作。

再次,当任务执行完后,服务端将剩下一部分图片交给““异常点聚类”处理,并进一步是被不同的图片。

最后,在整个执行任务结束后,平台就能有效识别异常图片,另外当异常图片未被有效识别的情况下,又可以在平台上快速提交异常图片,并交给算法逻辑继续学习,形成新的模型,从而在下一次任务执行过程中,就能把这种新发现的异常捕获住。

通过这种灵活的方案,一方面大大提升了异常图片检测结果的准确度,另一方面在整个异常图片的发现上形成了闭环,大大提升的兼容性测试的效能。

  • 实验室能力

为了应对日益复杂的用户使用环境和不稳定的运行环境,终端实验室不断去构建各种专项实验室,尽可能在实验室环境里就把问题发现并推动研发流程去解决。同时伴随着 IoT 时代的到来,面对种类繁多的终端设备,如何能够通过实验技术的手段帮助研发同学提升效能,是一个新问题也是一个比较有挑战的问题:终端实验室通过托管 IoT 设备的方式,让用户快速方便寻找设备,并进行功能验证。具体技术方案是在原有的 Android/iOS 真机租用方案的基础上做了能力升级。

第一, 将终端实验室上某一款手机和 IoT 设备做关联,保证当浏览器通过 WS 远程操作手机打开摄像头就能够看到对应的 IoT 设备;

第二,通过 WS 读取 IoT 串口的 trace 信息,并将数据以 WS 的形式推送到用户浏览器端;

第三,在宿主机上集成 IoT 设备操作的 SDK,保证宿主机能够通过命令行或者 HTTP 方式操控 IoT 设备;

第四,宿主机集成语音转文字 SDK,这样当 IoT 设备发出声音时,就能够在页面上以文字的方式告诉用例。

通过这种远程 IoT 租用的方式,用户就能够快速做作一台远程设备,另外在给 IoT 设备发送指令的同时,可以看到设备的相应信息(视觉展示、声音展示以及实时日志信息),从而达到快速验证的目的。

  • 机械臂扫码测试:
  • 智能机柜支持真机云测

3. 借助 mPaaS 对外输出

以上介绍的蚂蚁金服终端实验室相应能力的构建与实践,目前已经通过移动开发平台 mPaaS 对外输出一部分能力。

在 mPaaS 平台上,我们将自动化测试框架,真机调度管理,场景化测试方案以及详尽的测试报告方案整合外部客户的现有业务场景和系统,从而覆盖 App 开发期的各个阶段,确保应用上线前获取充分测试,发现 bug,减少线上问题,提高整体用户体验。

目前,终端实验室不仅对内服务了包括蚂蚁金服体系下的支付宝 App、网商银行、口碑商家等,同时借助 mPaaS 与大量生态合作伙伴一同共建能力,包括常熟农商行、西安银行、泰隆银行等。由于篇幅限制,很多技术要点我们无法一一展开,欢迎大家通过技术文档或点击“阅读原文”进一步了解 mPaaS :tech.antfin.com/docs/2/4954…

活动推荐:MTSC 2019 测试开发大会

MTSC2019 第五届中国移动互联网测试开发会将于 6 月 28-29 日在北京国际会议中心举行,50+ 来自 Google,BAT,TMD 等一线互联网企业的测试大咖分享精彩议题,涵盖移动自动化测试、服务端测试、质量保障 QA、高新测试技术(AI+、大数据测试、IoT 测试)等专题。

蚂蚁金服多位技术专家将在大会上分享精彩议题,解密蚂蚁金服内部移动测试 2.0+ 演进之路、代码实时染色系统如何完成代码覆盖率检测等,期待与你交流。

往期阅读

《开篇 | 蚂蚁金服 mPaaS 服务端核心组件体系概述》

《蚂蚁金服 mPaaS 服务端核心组件:亿级并发下的移动端到端网络接入架构解析》

《mPaaS 核心组件:支付宝如何为移动端产品构建舆情分析体系?》

《mPaaS 服务端核心组件:移动分析服务 MAS 架构解析》

《蚂蚁金服面对亿级并发场景的组件体系设计》

《自动化日志收集及分析在支付宝 App 内的演进》

关注我们公众号,获得第一手 mPaaS 技术实践干货

QRCode

钉钉群:通过钉钉搜索群号“23124039”

期待你的加入~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…869870871…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%