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

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


  • 首页

  • 归档

  • 搜索

用 Javassist 进行类转换 用 Javassist

发表于 2017-09-23

Java 编程的动态性, 第四部分

用 Javassist 进行类转换

用 Javassist 转换字节码中的方法

系列内容:

此内容是该系列的一部分:Java 编程的动态性, 第四部分

讲过了 Java 类格式和利用反射进行的运行时访问后,本系列到了进入更高级主题的时候了。本月我将开始本系列的第二部分,在这里 Java 类信息只不过是由应用程序操纵的另一种形式的数据结构而已。我将这个主题的整个内容称为 classworking。

我将以 Javassist 字节码操作库作为对 classworking 的讨论的开始。Javassist 不仅是一个处理字节码的库,而且更因为它的另一项功能使得它成为试验 classworking 的很好的起点。这一项功能就是:可以用 Javassist 改变 Java 类的字节码,而无需真正了解关于字节码或者 Java 虚拟机(Java virtual machine JVM)结构的任何内容。从某方面将这一功能有好处也有坏处 – 我一般不提倡随便使用不了解的技术 – 但是比起在单条指令水平上工作的框架,它确实使字节码操作更可具有可行性了。

Javassist 基础

Javassist 使您可以检查、编辑以及创建 Java 二进制类。检查方面基本上与通过 Reflection API 直接在 Java 中进行的一样,但是当想要修改类而不只是执行它们时,则另一种访问这些信息的方法就很有用了。这是因为 JVM 设计上并没有提供在类装载到 JVM 中后访问原始类数据的任何方法,这项工作需要在 JVM 之外完成。

Javassist 使用 javassist.ClassPool 类跟踪和控制所操作的类。这个类的工作方式与 JVM 类装载器非常相似,但是有一个重要的区别是它不是将装载的、要执行的类作为应用程序的一部分链接,类池使所装载的类可以通过 Javassist API 作为数据使用。可以使用默认的类池,它是从 JVM 搜索路径中装载的,也可以定义一个搜索您自己的路径列表的类池。甚至可以直接从字节数组或者流中装载二进制类,以及从头开始创建新类。

装载到类池中的类由 javassist.CtClass 实例表示。与标准的 Java java.lang.Class 类一样, CtClass 提供了检查类数据(如字段和方法)的方法。不过,这只是 CtClass 的部分内容,它还定义了在类中添加新字段、方法和构造函数、以及改变类、父类和接口的方法。奇怪的是,Javassist 没有提供删除一个类中字段、方法或者构造函数的任何方法。

字段、方法和构造函数分别由 javassist.CtField、``javassist.CtMethod 和 javassist.CtConstructor 的实例表示。这些类定义了修改由它们所表示的对象的所有方法的方法,包括方法或者构造函数中的实际字节码内容。

所有字节码的源代码
Javassist 让您可以完全替换一个方法或者构造函数的字节码正文,或者在现有正文的开始或者结束位置选择性地添加字节码(以及在构造函数中添加其他一些变量)。不管是哪种情况,新的字节码都作为类 Java 的源代码声明或者 String 中的块传递。Javassist 方法将您提供的源代码高效地编译为 Java 字节码,然后将它们插入到目标方法或者构造函数的正文中。

Javassist 接受的源代码与 Java 语言的并不完全一致,不过主要的区别只是增加了一些特殊的标识符,用于表示方法或者构造函数参数、方法返回值和其他在插入的代码中可能用到的内容。这些特殊标识符以符号 $ 开头,所以它们不会干扰代码中的其他内容。

对于在传递给 Javassist 的源代码中可以做的事情有一些限制。第一项限制是使用的格式,它必须是单条语句或者块。在大多数情况下这算不上是限制,因为可以将所需要的任何语句序列放到块中。下面是一个使用特殊 Javassist 标识符表示方法中前两个参数的例子,这个例子用来展示其使用方法:

1
2
3
4
复制代码{
  System.out.println("Argument 1: " + $1);
  System.out.println("Argument 2: " + $2);
}

对于源代码的一项更实质性的限制是不能引用在所添加的声明或者块外声明的局部变量。这意味着如果在方法开始和结尾处都添加了代码,那么一般不能将在开始处添加的代码中的信息传递给在结尾处添加的代码。有可能绕过这项限制,但是绕过是很复杂的 – 通常需要设法将分别插入的代码合并为一个块。

用 Javassist 进行 Classworking

作为使用 Javassist 的一个例子,我将使用一个通常直接在源代码中处理的任务:测量执行一个方法所花费的时间。这在源代码中可以容易地完成,只要在方法开始时记录当前时间、之后在方法结束时再次检查当前时间并计算两个值的差。如果没有源代码,那么得到这种计时信息就要困难得多。这就是 classworking 方便的地方 – 它让您对任何方法都可以作这种改变,并且不需要有源代码。

清单 1 显示了一个(不好的)示例方法,我用它作为我的计时试验的实验品: StringBuilder 类的 buildString 方法。这个方法使用一种所有 Java 性能优化的高手都会叫您 不要使用的方法构造一个具有任意长度的 String – 它通过反复向字符串的结尾附加单个字符来产生更长的字符串。因为字符串是不可变的,所以这种方法意味着每次新的字符串都要通过一个循环来构造:使用从老的字符串中拷贝的数据并在结尾添加新的字符。最终的效果是用这个方法产生更长的字符串时,它的开销越来越大。

清单 1. 需要计时的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码public class StringBuilder
{
    private String buildString(int length) {
        String result = "";
        for (int i = 0; i < length; i++) {
            result += (char)(i%26 + 'a');
        }
        return result;
    }
     
    public static void main(String[] argv) {
        StringBuilder inst = new StringBuilder();
        for (int i = 0; i < argv.length; i++) {
            String result = inst.buildString(Integer.parseInt(argv[i]));
            System.out.println("Constructed string of length " +
                result.length());
        }
    }
}

添加方法计时

因为有这个方法的源代码,所以我将为您展示如何直接添加计时信息。它也作为使用 Javassist 时的一个模型。清单 2 只展示了 buildString() 方法,其中添加了计时功能。这里没有多少变化。添加的代码只是将开始时间保存为局部变量,然后在方法结束时计算持续时间并打印到控制台。

清单 2. 带有计时的方法
1
2
3
4
5
6
7
8
9
10
复制代码private String buildString(int length) {
    long start = System.currentTimeMillis();
    String result = "";
    for (int i = 0; i < length; i++) {
        result += (char)(i%26 + 'a');
    }
    System.out.println("Call to buildString took " +
        (System.currentTimeMillis()-start) + " ms.");
    return result;
}

用 Javassist 来做

来做 使用 Javassist 操作类字节码以得到同样的效果看起来应该不难。Javassist 提供了在方法的开始和结束位置添加代码的方法,别忘了,我在为该方法中加入计时信息就是这么做的。

不过,还是有障碍。在描述 Javassist 是如何让您添加代码时,我提到添加的代码不能引用在方法中其他地方定义的局部变量。这种限制使我不能在 Javassist 中使用在源代码中使用的同样方法实现计时代码,在这种情况下,我在开始时添加的代码中定义了一个新的局部变量,并在结束处添加的代码中引用这个变量。

那么还有其他方法可以得到同样的效果吗?是的,我 可以在类中添加一个新的成员字段,并使用这个字段而不是局部变量。不过,这是一种糟糕的解决方案,在一般性的使用中有一些限制。例如,考虑在一个递归方法中会发生的事情。每次方法调用自身时,上次保存的开始时间值就会被覆盖并且丢失。

幸运的是有一种更简洁的解决方案。我可以保持原来方法的代码不变,只改变方法名,然后用原来的方法名增加一个新方法。这个 拦截器(interceptor)方法可以使用与原来方法同样的签名,包括返回同样的值。清单 3 展示了通过这种方法改编后源代码看上去的样子:

清单 3. 在源代码中添加一个拦截器方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码private String buildString$impl(int length) {
    String result = "";
    for (int i = 0; i < length; i++) {
        result += (char)(i%26 + 'a');
    }
    return result;
}
private String buildString(int length) {
    long start = System.currentTimeMillis();
    String result = buildString$impl(length);
    System.out.println("Call to buildString took " +
        (System.currentTimeMillis()-start) + " ms.");
    return result;
}

通过 Javassist 可以很好地利用这种使用拦截器方法的方法。因为整个方法是一个块,所以我可以毫无问题地在正文中定义并且使用局部变量。为拦截器方法生成源代码也很容易 – 对于任何可能的方法,只需要几个替换。

运行拦截

实现添加方法计时的代码要用到在 Javassist 基础中描述的一些 Javassist API。清单 4 展示了该代码,它是一个带有两个命令行参数的应用程序,这两个参数分别给出类名和要计时的方法名。 main() 方法的正文只给出类信息,然后将它传递给 addTiming() 方法以处理实际的修改。 addTiming() 方法首先通过在名字后面附加“ $impl” 重命名现有的方法,接着用原来的方法名创建该方法的一个拷贝。然后它用含有对经过重命名的原方法的调用的计时代码替换拷贝方法的正文。

清单4. 用 Javassist 添加拦截器方法
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
复制代码public class JassistTiming 
{
    public static void main(String[] argv) {
        if (argv.length == 2) {
            try {
                 
                // start by getting the class file and method
                CtClass clas = ClassPool.getDefault().get(argv[0]);
                if (clas == null) {
                    System.err.println("Class " + argv[0] + " not found");
                } else {
                     
                    // add timing interceptor to the class
                    addTiming(clas, argv[1]);
                    clas.writeFile();
                    System.out.println("Added timing to method " +
                        argv[0] + "." + argv[1]);
                     
                }
                 
            } catch (CannotCompileException ex) {
                ex.printStackTrace();
            } catch (NotFoundException ex) {
                ex.printStackTrace();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
             
        } else {
            System.out.println("Usage: JassistTiming class method-name");
        }
    }
     
    private static void addTiming(CtClass clas, String mname)
        throws NotFoundException, CannotCompileException {
         
        //  get the method information (throws exception if method with
        //  given name is not declared directly by this class, returns
        //  arbitrary choice if more than one with the given name)
        CtMethod mold = clas.getDeclaredMethod(mname);
         
        //  rename old method to synthetic name, then duplicate the
        //  method with original name for use as interceptor
        String nname = mname+"$impl";
        mold.setName(nname);
        CtMethod mnew = CtNewMethod.copy(mold, mname, clas, null);
         
        //  start the body text generation by saving the start time
        //  to a local variable, then call the timed method; the
        //  actual code generated needs to depend on whether the
        //  timed method returns a value
        String type = mold.getReturnType().getName();
        StringBuffer body = new StringBuffer();
        body.append("{\nlong start = System.currentTimeMillis();\n");
        if (!"void".equals(type)) {
            body.append(type + " result = ");
        }
        body.append(nname + "(?);\n");
         
        //  finish body text generation with call to print the timing
        //  information, and return saved value (if not void)
        body.append("System.out.println(\"Call to method " + mname +
            " took \" +\n (System.currentTimeMillis()-start) + " +
            "\" ms.\");\n");
        if (!"void".equals(type)) {
            body.append("return result;\n");
        }
        body.append("}");
         
        //  replace the body of the interceptor method with generated
        //  code block and add it to class
        mnew.setBody(body.toString());
        clas.addMethod(mnew);
         
        //  print the generated code block just to show what was done
        System.out.println("Interceptor method body:");
        System.out.println(body.toString());
    }
}

构造拦截器方法的正文时使用一个 java.lang.StringBuffer 来累积正文文本(这显示了处理 String 的构造的正确方法,与在 StringBuilder 的构造中使用的方法是相对的)。这种变化取决于原来的方法是否有返回值。如果它 有返回值,那么构造的代码就将这个值保存在局部变量中,这样在拦截器方法结束时就可以返回它。如果原来的方法类型为 void ,那么就什么也不需要保存,也不用在拦截器方法中返回任何内容。

除了对(重命名的)原来方法的调用,实际的正文内容看起来就像标准的 Java 代码。它是代码中的 `body.append(nname

  • “(?);\n”)这一行,其中nname是原来方法修改后的名字。在调用中使用的?` 标识符是 Javassist 表示正在构造的方法的一系列参数的方式。通过在对原来方法的调用中使用这个标识符,在调用拦截器方法时提供的参数就可以传递给原来的方法。

清单 5 展示了首先运行未修改过的 StringBuilder 程序、然后运行 JassistTiming 程序以添加计时信息、最后运行修改后的 StringBuilder 程序的结果。可以看到修改后的 StringBuilder 运行时会报告执行的时间,还可以看到因为字符串构造代码效率低下而导致的时间增加远远快于因为构造的字符串长度的增加而导致的时间增加。

清单 5. 运行这个程序
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
复制代码[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Constructed string of length 1000
Constructed string of length 2000
Constructed string of length 4000
Constructed string of length 8000
Constructed string of length 16000
[dennis]$ java -cp javassist.jar:. JassistTiming StringBuilder buildString
Interceptor method body:
{
long start = System.currentTimeMillis();
java.lang.String result = buildString$impl(?);
System.out.println("Call to method buildString took " +
 (System.currentTimeMillis()-start) + " ms.");
return result;
}
Added timing to method StringBuilder.buildString
[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Call to method buildString took 37 ms.
Constructed string of length 1000
Call to method buildString took 59 ms.
Constructed string of length 2000
Call to method buildString took 181 ms.
Constructed string of length 4000
Call to method buildString took 863 ms.
Constructed string of length 8000
Call to method buildString took 4154 ms.
Constructed string of length 16000

可以信任源代码吗?

Javassist 通过让您处理源代码而不是实际的字节码指令清单而使 classworking 变得容易。但是这种方便性也有一个缺点。正如我在 所有字节码的源代码中提到的,Javassist 所使用的源代码与 Java 语言并不完全一样。除了在代码中识别特殊的标识符外,Javassist 还实现了比 Java 语言规范所要求的更宽松的编译时代码检查。因此,如果不小心,就会从源代码中生成可能会产生令人感到意外的结果的字节码。

作为一个例子,清单 6 展示了在将方法开始时的拦截器代码所使用的局部变量的类型从 long 变为 int 时的情况。Javassist 会接受这个源代码并将它转换为有效的字节码,但是得到的时间是毫无意义的。如果试着直接在 Java 程序中编译这个赋值,您就会得到一个编译错误,因为它违反了 Java 语言的一个规则:一个窄化的赋值需要一个类型覆盖。

清单 6. 将一个 long 储存到一个 int 中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码[dennis]$ java -cp javassist.jar:. JassistTiming StringBuilder buildString
Interceptor method body:
{
int start = System.currentTimeMillis();
java.lang.String result = buildString$impl(?);
System.out.println("Call to method buildString took " +
 (System.currentTimeMillis()-start) + " ms.");
return result;
}
Added timing to method StringBuilder.buildString
[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Call to method buildString took 1060856922184 ms.
Constructed string of length 1000
Call to method buildString took 1060856922172 ms.
Constructed string of length 2000
Call to method buildString took 1060856922382 ms.
Constructed string of length 4000
Call to method buildString took 1060856922809 ms.
Constructed string of length 8000
Call to method buildString took 1060856926253 ms.
Constructed string of length 16000

取决于源代码中的内容,甚至可以让 Javassist 生成无效的字节码。清单7展示了这样的一个例子,其中我将 JassistTiming 代码修改为总是认为计时的方法返回一个 int 值。Javassist 同样会毫无问题地接受这个源代码,但是在我试图执行所生成的字节码时,它不能通过验证。

清单7. 将一个 String 储存到一个 int 中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码[dennis]$ java -cp javassist.jar:. JassistTiming StringBuilder buildString
Interceptor method body:
{
long start = System.currentTimeMillis();
int result = buildString$impl(?);
System.out.println("Call to method buildString took " +
 (System.currentTimeMillis()-start) + " ms.");
return result;
}
Added timing to method StringBuilder.buildString
[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Exception in thread "main" java.lang.VerifyError:
 (class: StringBuilder, method: buildString signature:
 (I)Ljava/lang/String;) Expecting to find integer on stack

只要对提供给 Javassist 的源代码加以小心,这就不算是个问题。不过,重要的是要认识到 Javassist 没有捕获代码中的所有错误,所以有可能会出现没有预见到的错误结果。

后续内容

Javassist 比我们在本文中所讨论的内容要丰富得多。下一个月,我们将进行更进一步的分析,看一看 Javassist 为批量修改类以及为在运行时装载类时对类进行动态修改而提供的一些特殊的功能。这些功能使 Javassist 成为应用程序中实现方面的一个很棒的工具,所以一定要继续跟随我们了解这个强大工具的全部内容。

相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
  • Javassist 是由东京技术学院的数学和计算机科学系的 Shigeru Chiba 所创建的。它最近加入了开放源代码 JBoss 应用服务器项目,并成为其中新增加的面向方面的编程功能的基础。Javassist 以 Mozilla Public License (MPL) 和 GNU Lesser General
    Public License (LGPL) 开放源代码许可证的形式发布。
  • 从“ Java
    bytecode: Understanding bytecode makes you a better programmer
    ” ( developerWorks, 2001年7月)中了解有关 Java 字节码设计的更多内容。
  • 要了解更多面向方面编程的内容吗?可以从“ Improve
    modularity with aspect-oriented programming
    ” ( developerWorks, 2002年1月)中找到关于使用 AspectJ 语言的概述。
  • 开放源代码 Jikes
    项目
    提供了一个非常快速和高度兼容 Java 编程语言的编译器。用它以老的方式生成您的字节码 – 从 Java 源代码生成。
  • 在 developerWorks
    Java 技术专区
    可以找到关于 Java 编程各方面的数百篇文章。

本文转载自: 掘金

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

Servlet必知必会

发表于 2017-09-22

Servlet简述

Servlet 是一个 Java 类,通常在 Web 应用 MVC 模式中担任 Controler 角色,它的任务是得到一个客户的请求,再发回一个响应,在接受客户请求后,调用模型对请求数据进行处理,将处理后的数据设置为请求属性,再发送到控制页面的 JSP 中。下面就通过一次完整的HTTP请求来介绍 Servlet 是如何工作的。

一次HTTP请求的到来

容器全盘控制着 Servlet 的一生,当用户点击一个链接比如:http://localhost:8080/testWeb/action.do 后,这个请求到达服务器和容器,Tomcat看到用户请求的是 testWeb 这个Web应用,于是到 testWeb 目录下去找 Web.xml (我们一般称之为部署描述文件,即DD),在DD中找 servlet-mapping 元素,与之匹配的
url-pattern,根据这个 url-pattern 的 servlet-name 映射到真正的 servlet-class ,容器根据此依据调用相应的 Servlet 类。

1
2
3
4
5
6
7
8
复制代码<servlet>
<servlet-name>ActionServletName</servlet-name>
<servlet-class>com.gyf.web.ActionServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ActionServletName</servlet-name>
<url-pattern>/action.do</url-pattern>
</servlet-mapping>

Servlet 的生命周期

通过上述过程,容器找到了应该调用的 Servlet ,如果这个 Servlet 类还没有被加载,容器会从头调用Servlet 的生命周期:

  1. 首先加载目标类(ActionServlet.class),接着调用Servlet的默认无参构造函数(注意我们不需要去覆盖Servlet的构造函数)。
  1. 接着调用 init() 方法,这个方法在 Servlet 的一生中只调用一次,如果你有其他的初始化代码(如得到一个数据库连接)。
  2. 接着调用 Service()方法,如果容器当初发现 Servlet 类已经被加载就会跳过前面两个步骤直接进入这个步骤,每次有HTTP请求到来时,都会调用目标 Servlet 的Service 方法,这个Service方法每次调用都会开启一个新线程,根据HTTP请求的类型决定是继续调用doGet(),还是doPost()。Service 方法在 Servlet 的一生中可以调用多次。
  3. 最后调用destroy()方法杀死这个 Servlet 类,在这个方法中可以进行垃圾回收清理资源。

Servlet生命周期 Servlet生命周期
注意在每个JVM上,每个特定的 Servlet 类只会有一个实例,所以不存在对于Servlet的每个实例这种说法。

Servlet 的继承结构和方法 Servlet 的继承结构和方法
ServletConfig 与 ServletContext


我们在 Servlet 输出一些固定信息时,可能会这样做

1
2
复制代码PrintWriter out=response.getWriter();
out.println("59833576*@qq.com");

如果我们要修改邮箱地址怎么办,就只有修改源代码,停止 Web 应用,重新编译,再启动 Web 应用。非常繁琐,在实际生产环境中,能不去动源代码就不去动源代码,那么我们可以用 ServletConfig 与 ServletContext 来解决这个问题。

ServletConfig

在部署描述文件中这样写:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码<servlet>
<servlet-name>ActionServletName</servlet-name>
<servlet-class>com.gyf.web.ActionServlet</servlet-class>
<init-param>
<param-name>adminEmail</param-name>
<param-value>59833576*@qq.com</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>ActionServletName</servlet-name>
<url-pattern>/action.do</url-pattern>
</servlet-mapping>

可以看到 init-param 是在 servlet 标签内的,这也意味着只能在该 servlet 类中使用,并不是全局的。

在 Servlet 中我们这么使用:

1
复制代码out.println(getServletConfig().getInitParameter("adminEmail"));

注意,不能在构造函数中调用这个方法,在init()后,Servlet 才得到ServletConfig对象。

####ServletContext

ServletContext是全局有效的,这一点从它的部署位置就可以看出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码<web-app 
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<servlet>
<servlet-name>Test</servlet-name>
<servlet-class>com.gyf.web.GetJarServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>Test</servlet-name>
<url-pattern>/servlet-api.jar</url-pattern>
</servlet-mapping>

<context-param>
<param-name>adminEmail</param-name>
<param-value>59833576*@qq.com</param-value>
</context-param>
</web-app>

context-param 就在web-app 标签下,所以它对所有的 Servlet 都是有效的,在 Servlet 代码中:

1
复制代码out.println(getServletContext().getInitParamter("adminEmail"));

要注意区分ServletContext 和 ServletConfig的区别和写法。

##监听者Listener

如果我们想在应用部署时就马上做一个事情要怎么做呢?这是我们就需要一个监听者。监听者分为很多种,每种的用途用法都不一样,比如刚才说的应用部署时就要做一个事情就需要 ServletContextListener。

ServletContextListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码package com.gyf;
import javax.servlet.*;
public class MyServletContextListener implements ServletContextListener
{
public void contextInitialized(ServletContextEvent event)
{
ServletContext sc=event.getServletContext();
String dogBreed=sc.getInitParameter("breed");
Dog d=new Dog(dogBreed);
sc.setAttribute("dog",d);
//得到数据库连接
//将数据保存进数据库
}
public void contextDestroyed(ServletContextEvent event)
{
//关闭数据库
}
}

方法很简单,我们只需要扩展 ServletContextListener 接口就行了,并将这个 .class文件 放进classes文件夹,最后在部署描述文件中写上该监听类的名字就行了:

1
2
3
4
5
复制代码<listener>
<listener-class>
com.gyf.MyServletContextListener
</listener-class>
</listener>

还有很多监听者类可供使用


上下文初始化参数线程安全


现在有了一个问题,既然 ServletContext 是全局可见的,那么如何保证保证其线程安全呢?有的同学可能会想在 doPost() 或 doGet() 方法上加 synchronized ,但是仔细想一想,这样做只能保证每个Servlet 只有一个线程在运行,但是一个Web应用可以有很多个Servlet,这样的话仍然不能保证它的线程安全。正确方法应该是这样做的:

1
2
3
4
5
复制代码synchronized (getServletContext())
{
getServletContext().setAttribute("foo",22);
getServletContext().setAttribute("bar",42);
}

每次使用ServletContext都要求先获得它的锁,这种方法才奏效

本文转载自: 掘金

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

【Python】在一段Python程序中使用多次事件循环 背

发表于 2017-09-21

背景

我们在Python异步程序编写中经常要用到如下的结构

1
2
3
4
5
6
7
8
复制代码import asyncio
async def doAsync():
await asyncio.sleep(0)
#...
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(doAsync())
loop.close()

这当然是很不错的,但当你第二次使用loop的时候程序就会抛出异常RuntimeError: Event loop is closed,这也无可厚非,理想的程序也应该是在一个时间循环中解决掉各种异步IO的问题。
但放在终端环境如Ipython中,如果想要练习Python的异步程序的编写的话每次都要重新开启终端未免太过于麻烦,这时候要探寻有没有更好的解决方案。

解决方案

我们可以使用asyncio.new_event_loop函数建立一个新的事件循环,并使用asyncio.set_event_loop设置全局的事件循环,这时候就可以多次运行异步的事件循环了,不过最好保存默认的asyncio.get_event_loop并在事件循环结束的时候还原回去。
最终我们的代码就像这样。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码import asyncio
async def doAsync():
await asyncio.sleep(0)
#...
def runEventLoop()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(doAsync())
loop.close()
if __name__ == "__main__":
oldloop = asyncio.get_event_loop()
runEventLoop()
runEventLoop()
asyncio.set_event_loop(oldloop)

感想

事件循环本来就是要一起做很多事情,在正式的Python代码中还是只用一个默认的事件循环比较好,平时的学习练习的话倒是随意了。

本文转载自: 掘金

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

C#NPOI对Excel操作(导出) NPOI简介

发表于 2017-09-21

语法都是基于.net的,第一次写文章,有错误希望读者提出,不胜感激。

NPOI简介

POI是用Java写的一种读取office文件的库,NPOI相当于POI的.net版本,它实现了电脑在没有安装微软office的情况下可以对EXCEL、WORD、Visio等文件的一系列操作,下文使用的是NPOI 2.2.1版本,需要的同学可以点击链接下载。
NPOI下载

导出EXCEL

我们先从数据库中随便取一张表,作为导出的示例,数据库使用的是sql server。
select * from Employees;
得到如下数据:

新建一个窗体项目,加上一个button,一个label(提示使用)

添加button的click事件,执行下列代码:

1
2
3
4
5
6
7
复制代码private void button1_Click(object sender, EventArgs e)  
{
//要执行的sql语句
string sql = "select * from Employees where cc_autoid < 50";
//调用函数导出Excel,函数实现在下面
ExportExcelFromSql(sql, @"D:\", 1);
}

ExportExcelFromSql函数的实现:
参数1是要执行的sql语句
参数2是要导出的路径(注意路径字符串前要转移,加上@)
参数3是导出的格式(1 || 2)
1是xls格式
2是xlsx格式
下文的SqlHelper也是自己封装ADO.net的静态类,这里不详细介绍了

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
复制代码private void ExportExcelFromSql(string sql,string path,int format)
{
//是用正则表达式判断输入的格式是否是1或者2
Regex reg = new Regex("[^12]");
string x = format.ToString();
if(reg.IsMatch(x))
{
return;
}
//执行sql语句
SqlDataReader reader = SqlHelper.ExecuteReader(sql, CommandType.Text);
//创建一个IWorkbook的集合,如果format为1就构造HSSFWork创建xls格式的EXCEL,为2就构造XSSFWork创建xlsx格式的EXCEL
List<IWorkbook> list = new List<IWorkbook>();
//判断是否有数据,如果没有,label做出提示
if (reader.HasRows)
{
list.Add(new HSSFWorkbook());//xls格式
list.Add(new XSSFWorkbook());//xlsx格式
//首先创建EXCEL工作簿,三元运算符判断格式
IWorkbook workbook = (format == 1) ? list[0] : list[1];
//创建一张工作表,这里工作表没有准确命名,可以在传一个参数 来确定表明,或者用正则表达式提出from后的数据库表名作为工作表的名称
ISheet sheet = workbook.CreateSheet("1");
//创建一行,作为表头
IRow row_head = sheet.CreateRow(0);
//循环得到数据库列名,创建第一行
for (int h = 0; h < reader.FieldCount; h++)
{
row_head.CreateCell(h).SetCellValue(reader.GetName(h));
}
int index = 1;
while (reader.Read())
{
IRow row = sheet.CreateRow(index);
index++;
for (int c = 0; c < reader.FieldCount; c++)
{
//这边得到每一行一列的数据类型,转换成string做switch判断
Type type = reader.GetFieldType(c);
string type_string = type.ToString().Replace("System.", "");
switch (type_string)
{
//考虑到数据库中的null值,这里用了可空值类型,如果为null,就调用SetCellType(CellType.Blank)插入EXCEL中,表示一个空单元格
case "Int32":
int? temp_int = reader.IsDBNull(c) ? null : (int?)reader.GetInt32(c);
if (temp_int == null)
{
row.CreateCell(c).SetCellType(CellType.Blank);
}
else
{
row.CreateCell(c).SetCellValue((int)temp_int);
}
break;
case "String":
string temp_string = reader.IsDBNull(c) ? null : reader.GetString(c);
if (temp_string == null)
{
row.CreateCell(c).SetCellType(CellType.Blank);
}
else
{
row.CreateCell(c).SetCellValue(temp_string);
}
break;
case "DateTime":
DateTime? temp_datetime = reader.IsDBNull(c) ? null : (DateTime?)reader.GetDateTime(c);
if (temp_datetime == null)
{
row.CreateCell(c).SetCellType(CellType.Blank);
}
else
{
ICellStyle cell_style = workbook.CreateCellStyle();
cell_style.DataFormat = HSSFDataFormat.GetBuiltinFormat("m/d/yy h:mm");
ICell cell = row.CreateCell(c);
cell.CellStyle = cell_style;
cell.SetCellValue((DateTime)temp_datetime);
}
break;
}
}
}
//最后使用文件流导出到path位置,还是三元运算符判断保存类型
using (FileStream fs = File.OpenWrite(path + @"T_customers" + ((format == 1) ? ".xls" : ".xlsx")))
{
workbook.Write(fs);
}
label1.Text = "导出EXCEL成功\r\n" + path + @"Employees.xlsx" + "\r\n" + System.DateTime.Now.ToString();
}
else
{
label1.Text = "没有读取到数据";
return;
}
}

执行以上代码:

最后打开导出的路径,打开文件:

此文章仅供学习参考
作者:千梦

本文转载自: 掘金

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

关于Spring+Mybatis事务管理中数据源的思考

发表于 2017-09-20

之前被同事问了一个问题:在我们的工程里,事务的开启跟关闭是由Spring负责的,但具体的SQL语句却是由Mybatis执行的。那么问题来了,Mybatis怎么保证自己执行的SQL语句是处在Spring的事务上下文中?

原文地址:www.jianshu.com/p/6a880d20a…

注:这篇文章重点不是分析Spring事务的实现原理,但却需要读者提前了解Spring事务原理的一些知识点,这样读起来才会容易些

现在公司主流的开发框架大部分是使用spring+mybatis来操作数据库,所有的事务操作都是交给spring去管理。当我们需要一个有事务上下文的数据库操作时,我们的做法就是写一个操作数据库的方法,并在该方法上面加上@Transactional注解就可以了。

仔细思考一下这个过程,@Transactional是由spring进行处理的,spring做的事情是从数据源(一般为数据库连接池,比如说druid,c3p0等)获取一个数据库连接,然后在进入方法逻辑前执行setAutoCommit(false)操作,最后在处理成功或者出现异常的时候分别执行commit或者rollback操作。

那么问题来了,开启跟结束事务是由spring获取到数据库连接以后进行操作的,但我们实际执行的update或者insert语句却是由mybatis获取数据库连接进行操作的,可以想到如果想让事务生效,那么spring跟mybatis使用的必须是同一个连接,真实情况是什么样呢?它们之间如何进行无缝衔接?让我们通过源码来分析一下。

首先如果想在spring中使用mybatis,我们除了引入mybatis依赖以外,还需要引入一个包:mybatis-spring。

1
2
3
4
5
复制代码<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>x.x.x</version>
</dependency>

可以猜测这个依赖包应该就是Spring跟Mybatis进行无缝连接的关键。

一般来说,我们在工程中的配置文件往往是这样:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码<!--会话工厂 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>

<!--spring事务管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>

<!--使用注释事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />

注:
1.会话工厂sqlSessionFactory跟Spring事务管理器transactionManager所使用的数据源dataSource必须是同一个。
2.这里的sqlSessionFactory类型是org.mybatis.spring.SqlSessionFactoryBean,该类是由我们引入的包mybatis-spring提供的。

看名字就知道SqlSessionFactoryBean是一个工厂bean,也就是说它交给Spring的真正实例是由getObject()方法提供的,那么我们去看下它真正实例初始化源码:

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
复制代码@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
//可以看出逻辑都在这里面
afterPropertiesSet();
}
return this.sqlSessionFactory;
}

@Override
public void afterPropertiesSet() throws Exception {
//此处省略一些校验逻辑
//...
this.sqlSessionFactory = buildSqlSessionFactory();
}

//最后来看这个最核心的方法
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
//...
//省略一些其他初始化信息,我们重点关注事务处理逻辑

if (this.transactionFactory == null) {
//可以看出,mybatis中把事务操作交给了SpringManagedTransactionFactory去做
this.transactionFactory = new SpringManagedTransactionFactory();
}

configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));

//省略后续逻辑
//...
}

下面我们再去看看SpringManagedTransactionFactory类的源码:

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
复制代码public class SpringManagedTransactionFactory implements TransactionFactory {

/**
* {@inheritDoc}
*/
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
return new SpringManagedTransaction(dataSource);
}

/**
* {@inheritDoc}
*/
@Override
public Transaction newTransaction(Connection conn) {
throw new UnsupportedOperationException("New Spring transactions require a DataSource");
}

/**
* {@inheritDoc}
*/
@Override
public void setProperties(Properties props) {
// not needed in this version
}

}

代码很少,且只有一个方法是有效的,看来离成功越来越近了,继续跟进去看看SpringManagedTransaction的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@Override
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}

private void openConnection() throws SQLException {
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
}

省略该类中其他部分,我们重点看获取连接的地方,这里最关键的地方就在this.connection = DataSourceUtils.getConnection(this.dataSource);,
DataSourceUtils全名是org.springframework.jdbc.datasource.DataSourceUtils,没错,它是由Spring提供的类,根据我们之前的猜测,Spring开启事务以后,Mybatis要想让自己的SQL语句处在这个事务上下文中操作,那必须拿到跟Spring开启事务同一个数据库连接才行,由于DataSourceUtils类是由Spring提供的,看来跟我们开始猜测的结果类似,我们接下来看看DataSourceUtils源码验证一下:

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
复制代码//获取数据库连接最终落在该方法上,我删除一些不重要的代码
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
//TransactionSynchronizationManager重点!!!有没有很熟悉的感觉??
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
Connection con = fetchConnection(dataSource);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
ConnectionHolder holderToUse = conHolder;
if (conHolder == null) {
holderToUse = new ConnectionHolder(con);
} else {
conHolder.setConnection(con);
}

holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
return con;
} else {
conHolder.requested();
if (!conHolder.hasConnection()) {
conHolder.setConnection(fetchConnection(dataSource));
}

return conHolder.getConnection();
}
}

看到TransactionSynchronizationManager有没有很亲切的感觉?对Spring事务管理源码熟悉的同学会马上联想到Spring开启事务以后,就是把相应的数据库连接放在这里,我截取源码看一下:

1
2
3
复制代码if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}

这段代码具体就是在我们上面配置的org.springframework.jdbc.datasource.DataSourceTransactionManager类中的doBegin方法里。至于TransactionSynchronizationManager类的实现原理其实我觉得你已经猜到了,没错,就是Java中经典类库ThreadLocal类!!!

最后补上一张图来说明spring+mybatis事务过程数据源获取逻辑:

Spring-Mybatis事务处理过程

Spring-Mybatis事务处理过程

本文转载自: 掘金

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

java垃圾回收算法之-coping复制 概述 应用场景 优

发表于 2017-09-20

之前的java垃圾回收算法之-标记清除 会导致内存碎片。下文的介绍的coping算法可以解决内存碎片问题。

概述


如果jvm使用了coping算法,一开始就会将可用内存分为两块,from域和to域, 每次只是使用from域,to域则空闲着。当from域内存不够了,开始执行GC操作,这个时候,会把from域存活的对象拷贝到to域,然后直接把from域进行内存清理。

应用场景


coping算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用coping算法进行拷贝时效率比较高。

jvm将Heap 内存划分为新生代与老年代,又将新生代划分为Eden(伊甸园) 与2块Survivor Space(幸存者区) ,然后在Eden –>Survivor Space 以及From Survivor Space 与To Survivor Space 之间实行Copying 算法。 不过jvm在应用coping算法时,并不是把内存按照1:1来划分的,这样太浪费内存空间了。一般的jvm都是8:1。也即是说,Eden区:From区:To区域的比例是

8:1:1

始终有90%的空间是可以用来创建对象的,而剩下的10%用来存放回收后存活的对象。

这里写图片描述

这里写图片描述

1、当Eden区满的时候,会触发第一次young gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发young gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
2、当后续Eden又发生young gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
3、可见部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代

注意:

1
复制代码万一存活对象数量比较多,那么To域的内存可能不够存放,这个时候会借助老年代的空间。

优点


在存活对象不多的情况下,性能高,能解决内存碎片和java垃圾回收算法之-标记清除 中导致的引用更新问题。

缺点


  • 会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;
  • 如果存活对象的数量比较大,coping的性能会变得很差。

原文链接


java垃圾回收算法之-coping复制

本文转载自: 掘金

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

吴恩达Coursera Deep Learning学习笔记

发表于 2017-09-20

【写在前面】作为一名终身学习实践者,我持之以恒地学习各种深度学习和机器学习的新知识。一个无聊的假日里我突然想到:反正一样要记笔记,为什么不把我学习的笔记写成博客供大家一起交流呢?于是,我就化身数据女侠,打算用博客分享的方式开启我的深度学习深度学习之旅。

本文仅供学习交流使用,侵权必删,不用于商业目的,转载须注明出处。

【学习笔记】

之前我们以逻辑回归 (logistic regression) 为例介绍了神经网络(),但它并没有隐藏层,所以并不算严格意义上的神经网络。在本文中,让我们随着Andrew一起深化神经网络,在sigmoid之前再增加一些ReLU神经元。最后我们会以疯狂收到AI科学家迷恋的可爱猫咪图案为例,用深度学习建立一个猫咪图案的识别模型。不过在这之前,让我们来看一下除了上次说到的sigmoid和ReLU之外,还有什么激活函数、他们之间又各有什么优劣——

激活函数 Activation Functions

1) sigmoid
除了在输出层(当输出是{0,1}的binary classification时)可能会用到之外,隐藏层中很少用到sigmoid,因为它的mean是0.5,同等情况下用均值为0的tanh函数取代。
2) tanh其实就是sigmoid的shifted版本,但输出从(0, 1)变为(-1, 1),所以下一层的输入是centered,更受欢迎。
3) ReLU (Rectified Linear Unit)在
吴恩达Coursera Deep Learning学习笔记 1 (上)中提到过ReLU激活函数,它在深度学习中比sigmoid和tanh更常用。这是因为当激活函数的输入z的值很大或很小的时候,sigmoid和tanh的梯度非常小,这会大大减缓梯度下降的学习速度。所以与sigmoid和tanh相比,ReLU的训练速度要快很多。
4) Leaky ReLULeaky ReLU比ReLU要表现得稍微好一丢丢但是实践中大家往往都用ReLU。


从浅神经网络到深神经网络

最开始的逻辑回归的例子中,并没有隐藏层。在接下来的课程中,Andrew分别介绍了1个隐藏层、2个隐藏层和L个隐藏层的神经网络。但是只要把前向传播和反向传播搞明白了,再加上之后会讲述的一些小撇步,就会发现其实都是换汤不换药。大家准备好了吗?和我一起深呼吸再一头扎进深度学习的海洋吧~

一般输入层是不计入神经网络的层数的,如果一个神经网络有L层,那么就意味着它有L-1个隐藏层和1个输出层。我们可以观察到输入层和输出层,但是不容易观察到中间数据是怎么变化的,因此在输入和输出层之间的部分叫隐藏层。

训练一个深神经网络大致分为以下步骤(吴恩达Coursera Deep Learning学习笔记 1 (上)也有详细说明):

  1. 定义神经网络的结构(超参数)
  2. 初始化参数
  3. 循环迭代
    3.1 在前向传播中,分为linear forward和activation forward。在linear
    forward中,Z[l]=W[l]A[l−1]+b[l],其中A[0]=X;在activation forward中,A[l]=g(Z[l])。期间要储存W、b、Z的值。最后算出当前的Loss。
    3.2 在反向传播中,分为activation backward和linear backward。
    3.3 更新参数。
    下图展现了一个L层的深度神经网络、其中L-1个隐藏层都用的是ReLU激活函数的训练步骤和过程。

一个L-1个ReLU隐藏层的训练过程

标注声明 Notations:
随着神经网络的模型越来越深,我们会有L-1个隐藏层,每一层都用小写的L即[l]标注为上标。z是上一层的输出(即这一层的输入)的线性组合,a是这一层通过激活函数对z的非线性变化,也叫激活值 (activation values)。训练数据中有m个样本,每一个样本都用(i)来标注为上标。每一个隐藏层l里都有n[l](上标内是小写的L不是1)个神经元,每一个神经元都用i标注为下标。

每一层、样本、单元的标注

超参数

随着我们的深度学习模型越来越复杂,我们要学习区分普通参数 (Parameters) 和超参数 (Hyperparameters)。 在上图的标注声明中出现的W和b是普通的参数,而超参数是指:
学习速率 (learning rate) alpha、循环次数 (# iterations)、隐藏层层数 (# hidden layers) L、每隐藏层中的神经元数量 (size of the hidden layers) n[l] ——上标内是小写的L不是1、激活函数 (choice of activation functions) g[l] ——上标内是小写的L不是1。除了这些超参数之外,之后还会学习到以下超参数:动量 (momentum)、小批量更新的样本数
(minibatch size)、正则化系数 (regularization weight),等等。

随机初始化 Random Initialization

吴恩达Coursera Deep Learning学习笔记 1 (上)中的逻辑回归 (logistic regression) 中没有隐藏层,所以将W直接初始化为0并无大碍。但是在初始化隐藏层的W时如果将每个神经元的权重都初始化为0,那么在之后的梯度下降中,每一个神经元的权重都会有相同的梯度和更新,这样的对称在梯度下降中永远无法打破,如此就算隐藏层中有一千一万个神经元,也只同于一个神经元。所以,为了打破这种对称的魔咒,在初始化参数时往往会加入一些微小的抖动,即用较小的随机数来初始化W,偏置项b可以初始化为0或者同样是较小的随机数。在Python中,可以用np.random.randn(a,b)
* 0.01来随机地初始化a*b的W矩阵,并用np.zeros((a, 1))来初始化a*1的b矩阵。

为什么是0.01呢?同sigmoid和tanh中所说,数据科学家通常会从将W initialize为很小的随机数,防止训练的速度过缓。但是如果神经网络很深的话,0.01这样小的数字未必最理想。但是总体来说,人们还是倾向于从较小的参数开始训练。

承接上面的超参数,对于每个隐藏层中的神经元数量,我们可以将这几个超参数设定为layer_dims的array,如layer_dims = [n_x, 4,3,2,1] 说明输入的X有n_x个特征,第一层有4个神经元,第二层有3个神经元,第三层有2个,最后一个输出单元。有一个容易搞错的地方,就是W[l]是n[l]*n[l-1]的矩阵,b[l]是n[l]*1的矩阵。所以初始化W和b就可以写成:
for l in range(1, L):
parameters[“W”+str(l)]
= np.random.randn(layer_dims[l],layer_dims[l-1])*0.01
parameters[“b”+str(l)] =np.zeros((layer_dims[l],1))
详见例2中的initialize_parameters_deep函数。

并不是很复杂有没有!那么,下面我们一起跟着Andrew来看几个神经网络的例子——


【例 1】用单个隐藏层的神经网络来分类平面数据

本例:4个神经元的隐藏层 (tanh) 加一个sigmoid的输出层

第三课的例子是Planar data classification with one hidden layer,即帮助大家搭建一个上图所示的浅神经网络(Shallow Neural Networks):一个4个单元的隐藏层 (tanh) 加一个sigmoid的输出层。

本例反向传播中各个梯度的计算(左为一个样本的情况,右为矢量化运算)

最后一步的prediction是用了0.5的cutoff,很简单:

将A2转化为Y-hat

最后的决策边界如下图,在训练数据上的精确度为90%,是不是比logistic regression表现强多啦?可见logistic regression不能学习到数据中的非线性关系,而神经网络可以(哪怕是本例中一个非常浅的神经网络)。

决策边界:一个有四个神经元的隐藏层的神经网络

其实本例中的模型也很简单,如果再复杂些可以做到更精确,但是可能会overfit,毕竟从上图中可以看出现有的模型已经抓住了数据中的大趋势。下面尝试了在隐藏层中设置不同个数的神经元,来看模型的精确度和决策边界是如何变化的:
Accuracy for 1 hidden units: 67.5 %
Accuracy for 2 hidden units: 67.25 %
Accuracy for 3 hidden units: 90.75 %
Accuracy
for 4 hidden units: 90.5 %
Accuracy for 5 hidden units: 91.25 %
Accuracy for 20 hidden units: 90.0 %
Accuracy for 50 hidden units: 90.25 %

隐藏层有5个神经元vs.20个神经元的决策边界

可以看到,在训练数据上,5个神经元的精确度是最高的,而当神经元数超过20时,决策边界就显示有overfitting的情况了。不过没事,之后会学习正则化 (regularization),能使很复杂的神经网络都不会出现overfitting。

这个例子的代码很简单,就不贴了。


【例 2】L层深度神经网络

第四节课的例子有三个:第一是一个ReLU+sigmoid的浅层神经网络,是为了后面的例子做铺垫;第二个将其深化,用了L-1个ReLU层,输出层也是sigmoid;第三个例子就是用前两个神经网络训练猫咪识别模型[吐血]。我将L层的模型和其猫咪识别器的训练过程精简地说一下。

设计神经网络与随机初始化参数
下图就是我们要搭建的L层神经网络,不过在这之前,让我们先挑个lucky number方便以后重复训练结果^_^
np.random.seed(1)

本例:[线性组合 -> ReLU激活] \ L-1次 -> 线性组合 -> sigmoid输出*

其次,让我们设计一下我们的神经网络。因为激活函数已经确定用ReLU了,所以在本例中我们只需设计layer_dims,就能确定输入的维度、层数和每层的神经元数。

def initialize_parameters_deep(layer_dims):
parameters = {}
L = len(layer_dims) # 根据我们一开始设计的模型超参数,读取L(其实是L+1)

for l in range(1, L): # 设定W1到W(L-1)和b1和b(L-1),一共有L-1层(其实是L)
parameters[‘W’ + str(l)] = np.random.randn(layer_dims[l], layer_dims[l-1])
* 0.01
parameters[‘b’ + str(l)] = np.zeros((layer_dims[l], 1))
assert(parameters[‘W’ + str(l)].shape == (layer_dims[l], layer_dims[l-1]))
assert(parameters[‘b’ +
str(l)].shape == (layer_dims[l], 1))
return parameters

和具体的数据结合,就知道了输入的维度和样本的数量。假设我们的训练数据中有209张图片,每张都是64*64像素,那么输入特征数n_x就是64*64*3 = 12288,m就是209,如下图:

每一层参数的维度

如果我们将W和b参数设为parameters,每一个初始化的W和b都是parameters这个list中的一个元素,那么L-1个循环隐藏层其实就是len(parameters)//2。下图是一个ReLU层加一个sigmoid层的一个loop,怎么将同样的计算复制到我们的L层深度神经网络中呢?

线性组合->ReLU->线性组合->sigmoid的前向与反向传播的例子

前向传播

前向传播分为linear forward和activation forward,前者计算Z,后者计算A=g(Z),g视激活函数的不同而不同。因为activation forward这步中包括了linear的值,所以名为linear_activation_forward函数。由于反向传播的梯度计算中会用到W、b、Z的值,所以我们将每一个iteration中将每个神经元的这些值暂时储存在caches这个大列表中,再在下一轮循环中覆盖掉。代码如下:

def linear_forward(A, W, b):
Z = np.dot(W, A) + b
assert(Z.shape == (W.shape[0], A.shape[1]))
cache = (A, W, b)
return Z, cache

def linear_activation_forward(A_prev, W, b, activation):
if activation == “sigmoid”:
Z, linear_cache = linear_forward(A_prev, W, b)
A, activation_cache
= sigmoid(Z)
elif activation == “relu”:
Z, linear_cache = linear_forward(A_prev, W, b)
A, activation_cache = relu(Z)
assert (A.shape
== (W.shape[0], A_prev.shape[1]))
cache = (linear_cache, activation_cache)
return A, cache

在定义了每一个神经单元的linear-activation forward之后,我们来定义这个L层神经网络的前向传播:

def L_model_forward(X, parameters):
caches = []
A = X
L = len(parameters) // 2 # 因为之前设定的parameters包含了每一层W和b的初始值,所以层数是这个列表长度的一半

for l in range(1, L): # L-1个隐藏层用ReLU激活函数
A_prev = A
A, cache = linear_activation_forward(A_prev,
parameters[‘W’ + str(l)], parameters[‘b’ + str(l)], activation = “relu”)
caches.append(cache)
AL, cache = linear_activation_forward(A, parameters[‘W’ + str(L)], parameters[‘b’ + str(L)],
activation = “sigmoid”) # 第L个层用sigmoid函数
caches.append(cache)
assert(AL.shape == (1,X.shape[1]))
return AL, caches

前向传播的尽头是计算当前参数下的损失~不过正如在后面L_model_backward函数中看到的,我们这里直接计算dL/dAL,并不计算L,这里计算cost是为了在训练过程检查代价是不是在稳定下降,以确保我们使用了合适的学习率。

def compute_cost(AL, Y):
m = Y.shape[1]
cost = -np.sum(Y*np.log(AL) + (1-Y)*np.log(1-AL))/m
cost = np.squeeze(cost)
# 将类似于 [[17]] 的cost变成 17
assert(cost.shape == ())
return cost

反向传播

反向传播和前向传播的函数设计是对称的,但是会比前向传播复杂一丢丢,需要小心各种线性代数中的运算规则——这也是为什么在前向传播中我们都在return前加入了维度检查(assert + shape)。下图显示了每一个神经元在反向传播中的输入和输出。现在我们看到之前在前向传播中缓存的用处了。如果我不储存W和Z的值,我就没有办法在反向线性传播中计算dW,db同理。

反向线性传播中的输入和输出

def linear_backward(dZ, cache):
A_prev, W, b = cache
m = A_prev.shape[1]
dW = np.dot(dZ, A_prev.T)/m
db = np.sum(dZ, axis=1, keepdims=True)/m
dA_prev
= np.dot(W.T, dZ)
assert (dA_prev.shape == A_prev.shape)
assert (dW.shape == W.shape)
assert (db.shape == b.shape)
return dA_prev, dW, db

上述的公式用线性代数表示为下图:

反向传播中参数梯度的计算

Andrew贴心地为大家提供了写好的函数:relu_backward和sigmoid_backward,如果我们自己写的话,需要在前向传播中储存A的值,否则在很多反向传播中就不知道dA/dZ,因为有些激活函数的导数是A的函数,比如sigmoid函数和tanh函数。

def linear_activation_backward(dA, cache, activation):
linear_cache, activation_cache = cache
if activation == “relu”:
dZ = relu_backward(dA, activation_cache)

   dA\_prev, dW, db = linear\_backward(dZ, linear\_cache)  
elif activation == "sigmoid":  
    dZ = sigmoid\_backward(dA, activation\_cache)  
    dA\_prev,

dW, db = linear_backward(dZ, linear_cache)
return dA_prev, dW, db

同前向传播一样,我们将dL/dAL反向传播,通过每一层的linear-activation backward构建整个完整的反向传播体系:

def L_model_backward(AL, Y, caches):
grads = {}
L = len(caches) # 层数
m = AL.shape[1]
Y = Y.reshape(AL.shape) # 改变Y的维度,确保其与AL的维度统一
dAL = - (np.divide(Y,
AL) - np.divide(1 - Y, 1 - AL)) # 代价函数对输出层输出AL的导数,就不计算具体的cost了
current_cache = caches[L-1]
grads[“dA” + str(L)], grads[“dW” + str(L)], grads[“db” + str(L)] = linear_activation_backward(dAL, current_cache,
activation = “sigmoid”)
for l in reversed(range(L-1)):
current_cache = caches[l]
dA_prev_temp, dW_temp, db_temp = linear_activation_backward(grads[“dA”+str(l+2)],
current_cache, activation = “relu”)
grads[“dA” + str(l + 1)] = dA_prev_temp
grads[“dW” + str(l + 1)] = dW_temp
grads[“db” + str(l + 1)]
= db_temp
return grads

一般现实工作中不会用线性代数如此折磨你,就算要自己一步一步这么写,也可以加入梯度检查等等来为你增添信心,具体以后再分享~

参数更新

至此我们已经在一个循环中计算出了当前W和b的梯度,最后就是用梯度下降的定义更新参数。在下一个例子中我们会看到如何用我们已经写好的每一步的函数,使用for loop执行梯度下降,最后得到训练好的模型。

def update_parameters(parameters, grads, learning_rate):
L = len(parameters) // 2
for l in range(L):
parameters[“W” + str(l+1)] = parameters[“W” + str(l+1)] - learning_rate*grads[“dW”

  • str(l + 1)]
    parameters["b" + str(l+1)] = parameters["b" + str(l+1)] - learning\_rate\*grads["db" + str(l + 1)]  
    return parameters

【例 3】继续AI科学家对猫的执念……

下面我们用例2中的L层深度神经网络来识别一张图是不是猫咪[捂脸],因为代码有点多所以分成了2和3的两个例子。假设train_x_orig是我们原始的输入,已经将图片像素数据提取并flatten为适合训练的数据,这里我们将每一个样本从64*64*3的输入变成一个12288*1的矢量,然后将值标准化到0-1之间:

train_x_flatten = train_x_orig.reshape(train_x_orig.shape[0], -1).Ttrain_x = train_x_flatten/255.

image2vector conversion

设计一个神经网络:layers_dims = [12288, 20, 7, 5, 1],即每个样本有12288个像素输入,第一层20个ReLU神经元,第二层7个,第三层5个,最后一个sigmoid。

L层的神经网络来识别图像中的喵喵

终于可以调用我们之前辛辛苦苦写好的函数啦!之前写的函数都是每一个iteration中的每一步骤,现在我们将每一个loop循环num_iterations次。

parameters = initialize_parameters_deep(layers_dims)
for i in range(0, num_iterations):
AL, caches = L_model_forward(X, parameters)
cost = compute_cost(AL, Y)
grads = L_model_backward(AL,
Y, caches)
parameters = update_parameters(parameters, grads, learning_rate)

这里的parameters就是训练好的参数,我们就可以用它来预测新的萌萌哒猫猫啦。读取一张num_px*num_px图片的像素再将其RGB转换为num_px*num_px*3的方法,请注意这里的图片尺寸需和训练数据中的一样:

fname = “images/“ + my_image
np.array(ndimage.imread(fname, flatten=False))
scipy.misc.imresize(image, size=(num_px,num_px)).reshape((num_px*num_px*3,1))

最后我们的模型在训练数据上的精确度为98.6%。然后我们就可以用类似于predict(test_x, test_y, parameters)这样的方法就能预测这个图片是不是一个喵喵啦!最后得到在训练数据上的精确度为80%,但是让我们来看看剩下20%没有正确预测的样本是什么样子的……

基础模型没有正确预测的样本例子

除了第五张姿势扭捏的猫猫外,2和4中的猫猫我们也没有很好地识别出来。不过不用担心,卷积神经网络 (Convolutional Neural Networks) 会比image2vector更适合于处理图片数据,所以敬请期待以后的更新!

本文转载自: 掘金

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

【Java】 通过反射,动态修改注解的某个属性值

发表于 2017-09-18

昨晚看到一条问题,大意是楼主希望可以动态得建立多个Spring 的定时任务。

这个题目我并不是很熟悉,不过根据题目描述和查阅相关 Spring 创建定时任务 的资料,发现这也许涉及到通过Java代码动态修改注解的属性值。

今天对此尝试了一番,发现通过反射来动态修改注解的属性值是可以做到的:

众所周知,java/lang/reflect 这个包下面都是Java的反射类和工具。

Annotation 注解,也是位于这个包里的。注解自从Java 5.0版本引入后,就成为了Java平台中非常重要的一部分,常见的如 @Override、 @Deprecated。

关于注解更详细的信息和使用方法,网上已经有很多资料,这里就不再赘述了。

一个注解通过 @Retention 指定其生命周期,本文所讨论的动态修改注解属性值,建立在 @Retention(RetentionPolicy.RUNTIM) 这种情况。毕竟这种注解才能在运行时(runtime)通过反射机制进行操作。

那么现在我们定义一个 @Foo 注解,它有一个类型为 String 的 value 属性,该注解应用再Field上:

1
2
3
4
5
6
7
8
9
复制代码
/**
* Created by krun on 2017/9/18.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Foo {
String value();
}

再定义一个普通的Java对象 Bar,它有一个私有的String属性 val,并为它设置属性值为"fff" 的 @Foo 注解:

1
2
3
4
5
6
复制代码
public class Bar {

@Foo ("fff")
private String val;
}

接下来在 main 方法中我们来尝试修改 Bar.val 上的 @Foo注解的属性值为 "ddd"。

先是正常的获取注解属性值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码
/**
* Created by krun on 2017/9/18.
*/
public class Main {
public static void main(String ...args) throws NoSuchFieldException {
//获取Bar实例
Bar bar = new Bar();
//获取Bar的val字段
Field field = Bar.class.getDeclaredField("val");
//获取val字段上的Foo注解实例
Foo foo = field.getAnnotation(Foo.class);
//获取Foo注解实例的 value 属性值
String value = foo.value();
//打印该值
System.out.println(value); // fff
}
}

首先,我们要知道注解的值是存在哪里的。

在 String value = foo.value(); 处下断点,我们跑一下可以发现:

当前栈中有这么几个变量,不过其中有一点很特别:foo,其实是个Proxy实例。

Proxy也是 java/lang/reflect下的东西,它的作用是为一个Java类生成一个代理,就像这样:

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
复制代码
public interface A {
String func1();
}

public class B implements A {

@Override
public String func1() { //do something ... }

public String func2() { //do something ... };
}

public static void main(String ...args) {
B bInstance = new B();

B bProxy = Proxy.newProxyInstance(
B.class.getClassLoader(), // B 类的类加载器
B.class.getInterfaces(), // B 类所实现的接口,如果你想拦截B类的某个方法,必须让这个方法在某个接口中声明并让B类实现该接口
new InvocationHandler() { // 调用处理器,任何对 B类所实现的接口方法的调用都会触发此处理器
@Override
public Object invoke (Object proxy, // 这个是代理的实例,method.invoke时不能使用这个,否则会死循环
Method method, // 触发的接口方法
Object[] args // 此次调用该方法的参数
) throws Throwable {
System.out.println(String.format("调用 %s 之前", method.getName()));
/**
* 这里必须使用B类的某个具体实现类的实例,因为触发时这里的method只是一个接口方法的引用,
* 也就是说它是空的,你需要为它指定具有逻辑的上下文(bInstance)。
*/
Object obj = method.invoke(bInstance, args);
System.out.println(String.format("调用 %s 之后", method.getName()));
return obj; //返回调用结果
}
}
);
}

这样你就可以拦截这个Java类的某个方法调用,但是你只能拦截到 func1的调用,想想为什么?

那么注意了:

ClassLoader 这是个class就会有,注解也不例外。那么注解和interfaces有什么关系?

注解本质上就是一个接口,它的实质定义为: interface SomeAnnotation extends Annotation。
这个 Annotation 接口位于 java/lang/annotation 包,它的注释中第一句话就是 The common interface extended by all annotation types.

如此说来,Foo 注解本身只是个接口,这就意味着它没有任何代码逻辑,那么它的 value 属性究竟是存在哪里的呢?

展开 foo 可以发现:

这个 Proxy 实例持有一个 AnnotationInvocationHandler,还记得之前提到过如何创建一个 Proxy 实例么? 第三个参数就是一个 InvocationHandler。
看名字这个handler即是Annotation所特有的,我们看一下它的代码:

1
2
3
4
5
6
7
8
9
10
复制代码
class AnnotationInvocationHandler implements InvocationHandler, Serializable {

private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
private transient volatile Method[] memberMethods = null;

/* 后续无关代码就省略了,想看的话可以查看 sun/reflect/annotation/AnnotationInvocationHandler */

}

我们一眼就可以看到一个有意思的名字: memberValues,这是一个Map,而断点中可以看到这是一个 LinknedHashMap,key为注解的属性名称,value即为注解的属性值。

现在我们找到了注解的属性值存在哪里了,那么接下来的事就好办了:

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
复制代码/**
* Created by krun on 2017/9/18.
*/
public class Main {
public static void main(String ...args) throws NoSuchFieldException, IllegalAccessException {
//获取Bar实例
Bar bar = new Bar();
//获取Bar的val字段
Field field = Bar.class.getDeclaredField("val");
//获取val字段上的Foo注解实例
Foo foo = field.getAnnotation(Foo.class);
//获取 foo 这个代理实例所持有的 InvocationHandler
InvocationHandler h = Proxy.getInvocationHandler(foo);
// 获取 AnnotationInvocationHandler 的 memberValues 字段
Field hField = h.getClass().getDeclaredField("memberValues");
// 因为这个字段事 private final 修饰,所以要打开权限
hField.setAccessible(true);
// 获取 memberValues
Map memberValues = (Map) hField.get(h);
// 修改 value 属性值
memberValues.put("value", "ddd");
// 获取 foo 的 value 属性值
String value = foo.value();
System.out.println(value); // ddd
}
}

本文转载自: 掘金

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

MySQL · 源码分析 · MySQL 半同步复制数据一致

发表于 2017-09-18

简介

MySQL Replication为MySQL用户提供了高可用性和可扩展性解决方案。本文介绍了MySQL Replication的主要发展历程,然后通过三个参数rpl_semi_sync_master_wait_point、sync_binlog、sync_relay_log的配置简要分析了MySQL半同步的数据一致性。

MySQL Replication的发展

在2000年,MySQL 3.23.15版本引入了Replication。Replication作为一种准实时同步方式,得到广泛应用。

这个时候的Replicaton的实现涉及到两个线程,一个在Master,一个在Slave。Slave的I/O和SQL功能是作为一个线程,从Master获取到event后直接apply,没有relay log。这种方式使得读取event的速度会被Slave replay速度拖慢,当主备存在较大延迟时候,会导致大量binary log没有备份到Slave端。

在2002年,MySQL 4.0.2版本将Slave端event读取和执行独立成两个线程(IO线程和SQL线程),同时引入了relay log。IO线程读取event后写入relay log,SQL线程从relay log中读取event然后执行。这样即使SQL线程执行慢,Master的binary log也会尽可能的同步到Slave。当Master宕机,切换到Slave,不会出现大量数据丢失。

MySQL在2010年5.5版本之前,一直采用的是异步复制。主库的事务执行不会管备库的同步进度,如果备库落后,主库不幸crash,那么就会导致数据丢失。

MySQL在5.5中引入了半同步复制,主库在应答客户端提交的事务前需要保证至少一个从库接收并写到relay log中。那么半同步复制是否可以做到不丢失数据呢。

在2016年,MySQL在5.7.17中引入了Group Replication。

MySQL 半同步复制的数据一致性

源码剖析

以下源码版本均为官方MySQL 5.7。
MySQL semi-sync是以插件方式引入,在plugin/semisync目录下。这里以semi-sync主要的函数调用为入口,学习semi-sync源码。

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
复制代码plugin/semisync/semisync_master.cc
403 /*******************************************************************************
404 *
405 * <ReplSemiSyncMaster> class: the basic code layer for sync-replication master.
406 * <ReplSemiSyncSlave> class: the basic code layer for sync-replication slave.
407 *
408 * The most important functions during semi-syn replication listed:
409 *
410 * Master:
//实际由Ack_receiver线程调用,处理semi-sync复制状态,获取备库最新binlog位点,唤醒对应线程
411 * . reportReplyBinlog(): called by the binlog dump thread when it receives
412 * the slave's status information.
//根据semi-sync运行状态设置数据包头semi-sync标记
413 * . updateSyncHeader(): based on transaction waiting information, decide
414 * whether to request the slave to reply.
//存储当前binlog 文件名和偏移量,更新当前最大的事务 binlog 位置
415 * . writeTranxInBinlog(): called by the transaction thread when it finishes
416 * writing all transaction events in binlog.
//实现客户端同步等待逻辑
417 * . commitTrx(): transaction thread wait for the slave reply.
418 *
419 * Slave:
//确认网络包头是否有semi-sync标记
420 * . slaveReadSyncHeader(): read the semi-sync header from the master, get the
421 * sync status and get the payload for events.
//给Master发送ACK报文
422 * . slaveReply(): reply to the master about the replication progress.
423 *
424 ******************************************************************************/
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
复制代码Ack_receiver线程,不断遍历slave,通过select监听slave网络包,处理semi-sync复制状态,唤醒等待线程。
plugin/semisync/semisync_master_ack_receiver.cc Ack_receiver::run()
->plugin/semisync/semisync_master.cc ReplSemiSyncMaster::reportReplyPacket
->plugin/semisync/semisync_master.cc ReplSemiSyncMaster::reportReplyBinlog

binlog Dump线程。如果slave是semi-slave,通过add_slave将slave添加到监听队列,在发送网络包时根据semi-sync运行状态设置包头的semi-sync标记。
sql/rpl_binlog_sender.cc Binlog_sender::run()
->sql/rpl_binlog_sender.cc Binlog_sender::send_binlog
->sql/rpl_binlog_sender.cc Binlog_sender::send_events
->sql/rpl_binlog_sender.cc Binlog_sender::before_send_hook
->plugin/semisync/semisync_master_plugin.cc repl_semi_before_send_event
->plugin/semisync/semisync_master.cc ReplSemiSyncMaster::updateSyncHeader

事务提交阶段,在flush binlog后,存储当前binlog 文件名和偏移量,更新当前最大的事务 binlog 位置。
sql/binlog.cc MYSQL_BIN_LOG::ordered_commit
->plugin/semisync/semisync_master_plugin.cc repl_semi_report_binlog_update//after_flush
->plugin/semisync/semisync_master.cc repl_semisync.writeTranxInBinlog

事务提交阶段,客户端等待处理逻辑,分为after_sync和after_commit两种情况
sql/binlog.cc MYSQL_BIN_LOG::ordered_commit
->sql/binlog.cc process_after_commit_stage_queue || call_after_sync_hook
->plugin/semisync/semisync_master_plugin.cc repl_semi_report_commit || repl_semi_report_binlog_sync
->plugin/semisync/semisync_master.cc ReplSemiSyncMaster::commitTrx

Slave IO线程,读取数据后后检查包头是否有semi-sync标记。
sql/rpl_slave.cc handle_slave_io
->plugin/semisync/semisync_slave_plugin.cc repl_semi_slave_read_event
->plugin/semisync/semisync_slave.cc ReplSemiSyncSlave::slaveReadSyncHeader

Slave IO线程,在queue event后,在需要回复Master ACK报文的时候,回复Master ACK报文。
sql/rpl_slave.cc handle_slave_io
->plugin/semisync/semisync_slave_plugin.cc repl_semi_slave_queue_event
->plugin/semisync/semisync_slave.cc ReplSemiSyncSlave::slaveReply

首先半同步方式,主库在等待备库ack时候,如果超时会退化为异步,这就可能导致数据丢失。在接下来分析中,先假设rpl_semi_sync_master_timeout足够大,不会退化为异步方式。

这里通过三个参数rpl_semi_sync_master_wait_point、sync_binlog、sync_relay_log的配置来对semi-sync做数据一致性的分析。

rpl_semi_sync_master_wait_point的配置

源码剖析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码plugin/semisync/semisync_master_plugin.cc

68 int repl_semi_report_binlog_sync(Binlog_storage_param *param,
69 const char *log_file,
70 my_off_t log_pos)
71 {
72 if (rpl_semi_sync_master_wait_point == WAIT_AFTER_SYNC)
73 return repl_semisync.commitTrx(log_file, log_pos);
74 return 0;
75 }

97 int repl_semi_report_commit(Trans_param *param)
...
102 if (rpl_semi_sync_master_wait_point == WAIT_AFTER_COMMIT &&
106 return repl_semisync.commitTrx(binlog_name, param->log_pos);

配置为WAIT_AFTER_COMMIT

after_commit.png
当rpl_semi_sync_master_wait_point为WAIT_AFTER_COMMIT时,commitTrx的调用在engine层commit之后(在ordered_commit函数中process_after_commit_stage_queue调用),如上图所示。即在等待Slave
ACK时候,虽然没有返回当前客户端,但事务已经提交,其他客户端会读取到已提交事务。如果Slave端还没有读到该事务的events,同时主库发生了crash,然后切换到备库。那么之前读到的事务就不见了,出现了幻读,如下图所示。图片引自Loss-less Semi-Synchronous Replication on MySQL 5.7.2 。

failover.png

配置为WAIT_AFTER_SYNC

MySQL针对上述问题,在5.7.2引入了Loss-less Semi-Synchronous,在调用binlog sync之后,engine层commit之前等待Slave ACK。这样只有在确认Slave收到事务events后,事务才会提交。在commit之前等待Slave ACK,同时可以堆积事务,利于group commit,有利于提升性能。如下图所示,图片引自Loss-less Semi-Synchronous Replication on MySQL 5.7.2 :

after_sync.png

其实上图流程中存在着会导致主备数据不一致,使主备同步失败的情形。见下面sync_binlog配置的分析。

sync_binlog的配置

源码剖析:

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
复制代码sql/binlog.cc ordered_commit
//当sync_period(sync_binlog)为1时,在sync之后update binlog end pos
9002 update_binlog_end_pos_after_sync= (get_sync_period() == 1);
...
9021 if (!update_binlog_end_pos_after_sync)
//更新binlog end position,dump线程会发送更新后的events
9022 update_binlog_end_pos();
...
//
9057 std::pair<bool, bool> result= sync_binlog_file(false);
...
9061 if (update_binlog_end_pos_after_sync)
9062 {
...
9068 update_binlog_end_pos(tmp_thd->get_trans_pos());
9069 }



sql/binlog.cc sync_binlog_file
8618 std::pair<bool, bool>
8619 MYSQL_BIN_LOG::sync_binlog_file(bool force)
8620 {
8621 bool synced= false;
8622 unsigned int sync_period= get_sync_period();//sync_binlog值
//sync_period为0不做sync操作,其他值为达到sync调用次数后sync
8623 if (force || (sync_period && ++sync_counter >= sync_period))
8624 {

配置分析

当sync_binlog为0的时候,binlog sync磁盘由操作系统负责。当不为0的时候,其数值为定期sync磁盘的binlog commit group数。当sync_binlog值大于1的时候,sync binlog操作可能并没有使binlog落盘。如果没有落盘,事务在提交前,Master掉电,然后恢复,那么这个时候该事务被回滚。但是Slave上可能已经收到了该事务的events并且执行,这个时候就会出现Slave事务比Master多的情况,主备同步会失败。所以如果要保持主备一致,需要设置sync_binlog为1。

WAIT_AFTER_SYNC和WAIT_AFTER_COMMIT两图中Send Events的位置,也可能导致主备数据不一致,出现同步失败的情形。实际在rpl_semi_sync_master_wait_point分析的图中是sync binlog大于1的情况。根据上面源码,流程如下图所示。Master依次执行flush binlog, update binlog position, sync binlog。如果Master在update binlog position后,sync binlog前掉电,Master再次启动后原事务就会被回滚。但可能出现Slave获取到Events,这也会导致Slave数据比Master多,主备同步失败。

sync_after_update.png

由于上面的原因,sync_binlog设置为1的时候,MySQL会update binlog end pos after sync。流程如下图所示。这时候,对于每一个事务都需要sync binlog,同时sync binlog和网络发送events会是一个串行的过程,性能下降明显。

update_after_sync.png

sync_relay_log的配置

源码剖析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码sql/rpl_slave.cc handle_slave_io

5764 if (queue_event(mi, event_buf, event_len))
...
5771 if (RUN_HOOK(binlog_relay_io, after_queue_event,
5772 (thd, mi, event_buf, event_len, synced)))

after_queue_event
->plugin/semisync/semisync_slave_plugin.cc repl_semi_slave_queue_event
->plugin/semisync/semisync_slave.cc ReplSemiSyncSlave::slaveReply

queue_event
->sql/binlog.cc MYSQL_BIN_LOG::append_buffer(const char* buf, uint len, Master_info *mi)
->sql/binlog.cc after_append_to_relay_log(mi);
->sql/binlog.cc flush_and_sync(0)
->sql/binlog.cc sync_binlog_file(force)

配置分析

在Slave的IO线程中get_sync_period获得的是sync_relay_log的值,与sync_binlog对sync控制一样。当sync_relay_log不是1的时候,semisync返回给Master的position可能没有sync到磁盘。在gtid_mode下,在保证前面两个配置正确的情况下,sync_relay_log不是1的时候,仅发生Master或Slave的一次Crash并不会发生数据丢失或者主备同步失败情况。如果发生Slave没有sync relay log,Master端事务提交,客户端观察到事务提交,然后Slave端Crash。这样Slave端就会丢失掉已经回复Master
ACK的事务events。

slave_crash.png

但当Slave再次启动,如果没有来得及从Master端同步丢失的事务Events,Master就Crash。这个时候,用户访问Slave就会发现数据丢失。

slave_up_master_down.png

通过上面这个Case,MySQL semisync如果要保证任意时刻发生一台机器宕机都不丢失数据,需要同时设置sync_relay_log为1。对relay log的sync操作是在queue_event中,对每个event都要sync,所以sync_relay_log设置为1的时候,事务响应时间会受到影响,对于涉及数据比较多的事务延迟会增加很多。

MySQL 三节点

在一主一从的主备semisync的数据一致性分析中放弃了高可用,当主备之间网络抖动或者一台宕机的情况下停止提供服务。要做到高可用,很自然我们可以想到一主两从,这样解决某一网络抖动或一台宕机时候的可用性问题。但是,前文叙述要保证数据一致性配置要求依然存在,即正常情况下的性能不会有改善。同时需要解决Master宕机时候,如何选取新主机的问题,如何避免多主的情形。

tri_nodes.png

选取新主机时一定要读取两个从机,看哪一个从机有最新的日志,否则可能导致数据丢失。这样的三节点方案就类似分布式Quorum机制,写的时候需要保证写成功三节点中的法定集合,确定新主的时候需要读取法定集合。利用分布式一致性协议Paxos/Raft可以解决数据一致性问题,选主问题和多主问题,因此近些年,国内数据库团队大多实现了基于Paxos/Raft的三节点方案。近来MySQL官方也以插件形式引入了支持多主集群的Group Replication方案。

总结

可以看到从replication功能引入后,官方MySQL一直在不停的完善,前进。同时我们可以发现当前原生的MySQL主备复制实现实际上很难在满足数据一致性的前提下做到高可用、高性能。

本文转载自: 掘金

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

Vuex 源码分析

发表于 2017-09-13

本文解读的Vuex版本为2.3.1

Vuex代码结构

Vuex的代码并不多,但麻雀虽小,五脏俱全,下面来看一下其中的实现细节。

源码分析

入口文件

入口文件src/index.js:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions } from './helpers'

export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions
}

这是Vuex对外暴露的API,其中核心部分是Store,然后是install,它是一个vue插件所必须的方法。Store
和install都在store.js文件中。mapState、mapMutations、mapGetters、mapActions为四个辅助函数,用来将store中的相关属性映射到组件中。

install方法

Vuejs的插件都应该有一个install方法。先看下我们通常使用Vuex的姿势:

1
2
3
4
复制代码import Vue from 'vue'
import Vuex from 'vuex'
...
Vue.use(Vuex)

install方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码export function install (_Vue) {
if (Vue) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
return
}
Vue = _Vue
applyMixin(Vue)
}

// auto install in dist mode
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}

方法的入参_Vue就是use的时候传入的Vue构造器。
install方法很简单,先判断下如果Vue已经有值,就抛出错误。这里的Vue是在代码最前面声明的一个内部变量。

1
复制代码let Vue // bind on install

这是为了保证install方法只执行一次。
install方法的最后调用了applyMixin方法。这个方法定义在src/mixin.js中:

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
复制代码export default function (Vue) {
const version = Number(Vue.version.split('.')[0])

if (version >= 2) {
const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}

/**
* Vuex init hook, injected into each instances init hooks list.
*/

function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
this.$store = options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}

方法判断了一下当前vue的版本,当vue版本>=2的时候,就在Vue上添加了一个全局mixin,要么在init阶段,要么在beforeCreate阶段。Vue上添加的全局mixin会影响到每一个组件。mixin的各种混入方式不同,同名钩子函数将混合为一个数组,因此都将被调用。并且,混合对象的钩子将在组件自身钩子之前。

来看下这个mixin方法vueInit做了些什么:
this.$options用来获取实例的初始化选项,当传入了store的时候,就把这个store挂载到实例的$store上,没有的话,并且实例有parent的,就把parent的$store挂载到当前实例上。这样,我们在Vue的组件中就可以通过this.$store.xxx访问Vuex的各种数据和状态了。

Store构造函数

Vuex中代码最多的就是store.js, 它的构造函数就是Vuex的主体流程。

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
复制代码  constructor (options = {}) {
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)

const {
plugins = [],
strict = false
} = options

let {
state = {}
} = options
if (typeof state === 'function') {
state = state()
}

// store internal state
this._committing = false
this._actions = Object.create(null)
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()

// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}

// strict mode
this.strict = strict

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
}

依然,先来看看使用Store的通常姿势,便于我们知道方法的入参:

1
2
3
4
5
6
7
8
9
10
11
复制代码export default new Vuex.Store({
state,
mutations
actions,
getters,
modules: {
...
},
plugins,
strict: false
})

store构造函数的最开始,进行了2个判断。

1
2
复制代码assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)

这里的assert是util.js里的一个方法。

1
2
3
复制代码export function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}

先判断一下Vue是否存在,是为了保证在这之前store已经install过了。另外,Vuex依赖Promise,这里也进行了判断。
assert这个函数虽然简单,但这种编程方式值得我们学习。
接着往下看:

1
2
3
4
5
6
7
8
9
10
11
复制代码const {
plugins = [],
strict = false
} = options

let {
state = {}
} = options
if (typeof state === 'function') {
state = state()
}

这里使用解构并设置默认值的方式来获取传入的值,分别得到了plugins, strict 和state。传入的state也可以是一个方法,方法的返回值作为state。

然后是定义了一些内部变量:

1
2
3
4
5
6
7
8
9
复制代码// store internal state
this._committing = false
this._actions = Object.create(null)
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()

this._committing 表示提交状态,作用是保证对 Vuex 中 state 的修改只能在 mutation 的回调函数中,而不能在外部随意修改state。
this._actions 用来存放用户定义的所有的 actions。
this._mutations 用来存放用户定义所有的 mutatins。
this._wrappedGetters 用来存放用户定义的所有 getters。
this._modules 用来存储用户定义的所有modules
this._modulesNamespaceMap 存放module和其namespace的对应关系。
this._subscribers 用来存储所有对 mutation 变化的订阅者。
this._watcherVM 是一个 Vue 对象的实例,主要是利用 Vue 实例方法 $watch 来观测变化的。
这些参数后面会用到,我们再一一展开。

继续往下看:

1
2
3
4
5
6
7
8
9
复制代码// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}

如同代码的注释一样,绑定Store类的dispatch和commit方法到当前store实例上。dispatch 和 commit 的实现我们稍后会分析。this.strict 表示是否开启严格模式,在严格模式下会观测所有的 state 的变化,建议在开发环境时开启严格模式,线上环境要关闭严格模式,否则会有一定的性能开销。

构造函数的最后:

1
2
3
4
5
6
7
8
9
10
11
复制代码// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
Vuex的初始化核心

installModule

使用单一状态树,导致应用的所有状态集中到一个很大的对象。但是,当应用变得很大时,store 对象会变得臃肿不堪。

为了解决以上问题,Vuex 允许我们将 store 分割到模块(module)。每个模块拥有自己的 state、mutation、action、getters、甚至是嵌套子模块——从上至下进行类似的分割。

1
2
3
4
复制代码// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)

在进入installModule方法之前,有必要先看下方法的入参this._modules.root是什么。

1
复制代码this._modules = new ModuleCollection(options)

这里主要用到了src/module/module-collection.js 和 src/module/module.js

module-collection.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.root = new Module(rawRootModule, false)

// register all nested modules
if (rawRootModule.modules) {
forEachValue(rawRootModule.modules, (rawModule, key) => {
this.register([key], rawModule, false)
})
}
}
...
}

module-collection的构造函数里先定义了实例的root属性,为一个Module实例。然后遍历options里的modules,依次注册。

看下这个Module的构造函数:

1
2
3
4
5
6
7
8
9
10
复制代码export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
this._children = Object.create(null)
this._rawModule = rawModule
const rawState = rawModule.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
...
}

这里的rawModule一层一层的传过来,也就是new Store时候的options。
module实例的_children目前为null,然后设置了实例的_rawModule和state。

回到module-collection构造函数的register方法, 及它用到的相关方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码register (path, rawModule, runtime = true) {
const parent = this.get(path.slice(0, -1))
const newModule = new Module(rawModule, runtime)
parent.addChild(path[path.length - 1], newModule)

// register nested modules
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}

get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}

addChild (key, module) {
this._children[key] = module
}

get方法的入参path为一个数组,例如[‘subModule’, ‘subsubModule’], 这里使用reduce方法,一层一层的取值, this.get(path.slice(0, -1))取到当前module的父module。然后再调用Module类的addChild方法,将改module添加到父module的_children对象上。

然后,如果rawModule上有传入modules的话,就递归一次注册。

看下得到的_modules数据结构:

扯了一大圈,就是为了说明installModule函数的入参,接着回到installModule方法。

1
2
复制代码const isRoot = !path.length
const namespace = store._modules.getNamespace(path)

通过path的length来判断是不是root module。

来看一下getNamespace这个方法:

1
2
3
4
5
6
7
复制代码getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}

又使用reduce方法来累加module的名字。这里的module.namespaced是定义module的时候的参数,例如:

1
2
3
4
5
6
7
复制代码export default {
state,
getters,
actions,
mutations,
namespaced: true
}

所以像下面这样定义的store,得到的selectLabelRule的namespace就是’selectLabelRule/‘

1
2
3
4
5
6
7
8
9
10
复制代码export default new Vuex.Store({
state,
actions,
getters,
mutations,
modules: {
selectLabelRule
},
strict: debug
})

接着看installModule方法:

1
2
3
4
复制代码// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}

传入了namespaced为true的话,将module根据其namespace放到内部变量_modulesNamespaceMap对象上。

然后

1
2
3
4
5
6
7
8
复制代码// set state
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}

getNestedState跟前面的getNamespace类似,也是用reduce来获得当前父module的state,最后调用Vue.set将state添加到父module的state上。

看下这里的_withCommit方法:

1
2
3
4
5
6
复制代码_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}

this._committing在Store的构造函数里声明过,初始值为false。这里由于我们是在修改 state,Vuex 中所有对 state 的修改都会用 _withCommit函数包装,保证在同步修改 state 的过程中 this._committing 的值始终为true。这样当我们观测 state 的变化时,如果 this._committing 的值不为 true,则能检查到这个状态修改是有问题的。

看到这里,可能会有点困惑,举个例子来直观感受一下,以 Vuex 源码中的 example/shopping-cart 为例,打开 store/index.js,有这么一段代码:

1
2
3
4
5
6
7
8
9
10
复制代码export default new Vuex.Store({
actions,
getters,
modules: {
cart,
products
},
strict: debug,
plugins: debug ? [createLogger()] : []
})

这里有两个子 module,cart 和 products,我们打开 store/modules/cart.js,看一下 cart 模块中的 state 定义,代码如下:

1
2
3
4
复制代码const state = {
added: [],
checkoutStatus: null
}

运行这个项目,打开浏览器,利用 Vue 的调试工具来看一下 Vuex 中的状态,如下图所示:

来看installModule方法的最后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码const local = module.context = makeLocalContext(store, namespace, path)

module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})

module.forEachAction((action, key) => {
const namespacedType = namespace + key
registerAction(store, namespacedType, action, local)
})

module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})

module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})

local为接下来几个方法的入参,我们又要跑偏去看一下makeLocalContext这个方法了:

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
复制代码/**
* make localized dispatch, commit, getters and state
* if there is no namespace, just use root ones
*/
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === ''

const local = {
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args

if (!options || !options.root) {
type = namespace + type
if (!store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return
}
}

return store.dispatch(type, payload)
},

commit: noNamespace ? store.commit : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args

if (!options || !options.root) {
type = namespace + type
if (!store._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}

store.commit(type, payload, options)
}
}

// getters and state object must be gotten lazily
// because they will be changed by vm update
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})

return local
}

就像方法的注释所说的,方法用来得到局部的dispatch,commit,getters 和 state, 如果没有namespace的话,就用根store的dispatch, commit等等

以local.dispath为例:
没有namespace为’’的时候,直接使用this.dispatch。有namespace的时候,就在type前加上namespace再dispath。

local参数说完了,接来是分别注册mutation,action和getter。以注册mutation为例说明:

1
2
3
4
复制代码module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
1
2
3
4
5
6
复制代码function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler(local.state, payload)
})
}

根据mutation的名字找到内部变量_mutations里的数组。然后,将mutation的回到函数push到里面。
例如有这样一个mutation:

1
2
3
4
5
复制代码mutation: {
increment (state, n) {
state.count += n
}
}

就会在_mutations[increment]里放入其回调函数。

commit

前面说到mutation被放到了_mutations对象里。接下来看一下,Store构造函数里最开始的将Store类的dispatch和commit放到当前实例上,那commit一个mutation的执行情况是什么呢?

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
复制代码  commit (_type, _payload, _options) {
// check object-style commit
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)

const mutation = { type, payload }
const entry = this._mutations[type]
if (!entry) {
console.error(`[vuex] unknown mutation type: ${type}`)
return
}
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
this._subscribers.forEach(sub => sub(mutation, this.state))

if (options && options.silent) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}

方法的最开始用unifyObjectStyle来获取参数,这是因为commit的传参方式有两种:

1
2
3
复制代码store.commit('increment', {
amount: 10
})

提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

1
2
3
4
复制代码store.commit({
type: 'increment',
amount: 10
})
1
2
3
4
5
6
7
8
9
10
11
复制代码function unifyObjectStyle (type, payload, options) {
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}

assert(typeof type === 'string', `Expects string as the type, but found ${typeof type}.`)

return { type, payload, options }
}

如果传入的是对象,就做参数转换。
然后判断需要commit的mutation是否注册过了,this._mutations[type],没有就抛错。
然后循环调用_mutations里的每一个mutation回调函数。
然后执行每一个mutation的subscribe回调函数。

Vuex辅助函数

Vuex提供的辅助函数有4个:

以mapGetters为例,看下mapGetters的用法:

代码在src/helpers.js里:

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
复制代码export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
normalizeMap(getters).forEach(({ key, val }) => {
val = namespace + val
res[key] = function mappedGetter () {
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
if (!(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
return this.$store.getters[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})


function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}

function normalizeNamespace (fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/'
}
return fn(namespace, map)
}
}

normalizeNamespace方法使用函数式编程的方式,接收一个方法,返回一个方法。
mapGetters接收的参数是一个数组或者一个对象:

1
2
3
4
5
6
7
8
复制代码computed: {
// 使用对象展开运算符将 getters 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
1
2
3
4
复制代码mapGetters({
// 映射 this.doneCount 为 store.getters.doneTodosCount
doneCount: 'doneTodosCount'
})

这里是没有传namespace的情况,看下方法的具体实现。
normalizeNamespace开始进行了参数跳转,传入的数组或对象给map,namespace为’’ , 然后执行fn(namespace, map)
接着是normalizeMap方法,返回一个数组,这种形式:

1
2
3
4
复制代码{
key: doneCount,
val: doneTodosCount
}

然后往res对象上塞方法,得到如下形式的对象:

1
2
3
4
5
复制代码{
doneCount: function() {
return this.$store.getters[doneTodosCount]
}
}

也就是最开始mapGetters想要的效果:

完

by kaola/fangwentian

本文转载自: 掘金

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

1…953954955956

开发者博客

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