EdmondFrank's 时光足迹

この先は暗い夜道だけかもしれない それでも信じて進むんだ。星がその道を少しでも照らしてくれるのを。
或许前路永夜,即便如此我也要前进,因为星光即使微弱也会我为照亮前途。
——《四月は君の嘘》

Java Reflection

Java Reflection - Private Fields and Methods

Note: This only works when running the code as a standalone Java application, like you do with unit tests and regular applications. if you try to do this inside a Java Applet, you will need to fiddle around with SecurityManager.

Accessing Private Fields

To access a private field you will need to call Class.getDeclaredFiled(String name) method

The methods Class.getField(String name) only return public fields.

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.lang.reflect.Field;

class PrivateObject {
    private String privateString = null;
    public PrivateObject(String privateString) {
        this.privateString = privateString;
    }
}
public class test {
    public static void main(String[] args) throws NoSuchFieldException {
        PrivateObject privateObject = new PrivateObject("The Private Value");
        Field privateStringField = privateObject.getClass().getDeclaredField("privateString");
        privateStringField.setAccessible(true);
        try {
            String oldFieldValue = (String) privateStringField.get(privateObject);
            System.out.println("fieldValue: " + oldFieldValue);
            String newFieldValue = oldFieldValue.replace("The", "That");
            privateStringField.set(privateObject, newFieldValue);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

PS: by calling Field.setAccessible(true) just turn off the access checks for this particular Field instance, for reflection only. Now you can access it even if it is private/protected or package scope. even if the caller is not part of those scopes But you still can’t access the field using normal code which would be disallowed by compiler.

Accessing Private Methods

To access a private method you will need to call the Class.getDeclaredMethod(String name, Class[] parameterTypes)

The methods Class.getMethod(String name, Class[] parameterTypes) only return public methods

Example

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
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

class PrivateObject {
    private String privateString = null;
    public PrivateObject(String privateString) {
        this.privateString = privateString;
    }

    private String getPrivateString() {
        return this.privateString;
    }
}
public class test {
    public static void main(String[] args) throws NoSuchMethodException {
        PrivateObject privateObject = new PrivateObject("The Private Value");

        try {
            Method privateStringMethod = PrivateObject.class.getDeclaredMethod("getPrivateString", null);
            privateStringMethod.setAccessible(true);
            String returnValue = (String) privateStringMethod.invoke(privateObject, null);

            System.out.println("returnValue = " + returnValue);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

Java Dev Tools and Debugging Notes

Jave Dev tools and Debugging Notes

  1. Java
    1. Maven
      1. use maven with proxy and skip tests:
      2. Structure
    2. JShell
    3. Javap
    4. Jar Files
      1. compile and package as a FAT-JAR
      2. differences between “java -cp” and “java -jar”
    5. Debugging
      1. Exec args:
      2. Jdb

Java

Some notes about Java dev tools and debuggers

Maven

build lifecycle targets

  • validate: validate the project is correct and all necessary information is available
  • compile: compile the source code of the project
  • test: test the compiled source code using a suitable unit testing framework. There tests should not require the code be packaged or deployed
  • package: take the compiled code and package it in its distributable format, such as JAR
  • verify: run any checks on results of integration tests to ensure quality criteria are met
  • install: install the package into the local repository, for use as a dependency in other projects locally
  • deploy: done in the build environment, copies the final package to the remote repository for sharing with other developers and projects

use maven with proxy and skip tests:

-Dmaven.test.skip=true -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=1081 -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=1081

Structure

  1. Expected directory structure:
    • Java files are in src/main/java as well as src/test/java.
    • Resource files are under src/main/resources and src/test/resources.
  2. mvn archetype:generate: Generates a skeleton of a project based on your inputs (package name, versioning, project name, etc)
  3. Edit pom.xml and set the jdk version there..
  4. mvn package - compile, test, bundle.

JShell

Java REPL(Read Eval Print Loop) import after Java 9

  • /list -start - shows modules imported at startup.
  • /edit - edit that line in a new window.
  • /set editor “vi” - use vi instead of the default graphical edit pad.
  • /save abc.java - save current buffer to file.
  • /load abc.java - load from file into shell.
  • /-1 - execute last snippet.
  • /1 - execute first snippet.
  • /drop N - drop Nth snippet.
  • /vars - show only variables that were defined in snippets.
  • /types - show only classes that were defined in snippets.

Javap

javap TestDecompile.class - decompile .class file to human readable format. Does not show content of methods though. javap -c TestDecompile.class - show jvm bytecode in human readable -form, including methods.

Jar Files

These are zip files that have a META-INF folder with a Manifest.mf file inside.

compile and package as a FAT-JAR

<?xml version="1.0" encoding="UTF-8"?>
<build>
   <finalName>indexer-spider</finalName>
   <plugins>
      <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-assembly-plugin</artifactId>
         <version>2.4.1</version>
         <configuration>
            <!--  get all project dependencies  -->
            <descriptorRefs>
               <descriptorRef>jar-with-dependencies</descriptorRef>
            </descriptorRefs>
            <!--  MainClass in mainfest make a executable jar  -->
            <archive>
               <manifest>
                  <mainClass>org.apache.maven.indexer.examples.BasicUsageExample</mainClass>
               </manifest>
            </archive>
         </configuration>
         <executions>
            <execution>
               <id>make-assembly</id>
               <!--  bind to the packaging phase  -->
               <phase>package</phase>
               <goals>
                  <goal>single</goal>
               </goals>
            </execution>
         </executions>
      </plugin>
      <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-compiler-plugin</artifactId>
         <configuration>
            <source>8</source>
            <target>8</target>
         </configuration>
      </plugin>
   </plugins>
</build>

differences between “java -cp” and “java -jar”

  • There won’t be any difference in terms of performance.
  • java -cp: must specify the required classes and jar’s in the classpath for running a java class file.
  • java -jar: jvm finds the class that it needs to run from /META-INF/MANIFEST.MF file inside the jar file

Debugging

  • jps - Shows all runnning java processes.
  • jvisualvm - If you have it, it shows the java processes on the system with details on threads, profiler etc.
  • debugging mode - Use these args to start a process in debugging mode

Exec args:

-Xagentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=<port>

  • agentlib=jdwp - Load the jdwp agent, for debugging.
  • transport=dt_socket - for connecting a debugging client over the network.
  • server=y - this one is the server half of the debugging.
  • suspend=y - don’t start executing until a client debugger attaches.
  • address=7777 - port we listen on.

Jdb

  • jdb -attach : attach debug process start with debugging mode.

Rails 常量加载机制

0x1 基本介绍

首先,在编写Rails应用时,代码会预加载:通过约定类定义所在的文件名与类名一致映射,实现自动加载

Rails 通过config.cache_classes参数来设置常见加载的模式,主要有以下两种形式:

  • Kernel#require(一般用于生产环境,只加载一次)
  • Kernel#load(一般用于开发环境)

除了加载的方式不同,在config.cache_classes = false时,Rails还会启用Reloader中间件 在代码发生变更时,通过remove_constant/const_missing等方法实现么常量、模块热替换

1
2
3
4
# railties-4.0.13/lib/rails/application.rb:384
unless config.cache_classes
    middleware.use ::ActionDispatch::Reloader, lambda { app.reload_dependencies? }
end

下面,本文逐步解析下Ruby及Rails下的常量加载机制

0x2 常量刷新机载

Ruby中常见的常量:

  • 模块 module
  • 类 class
  • 自定义常量

其中,既然module和class在Ruby中本质就是常量的话,类和模块定义的嵌套创建的命名空间也是常量了

Ruby 的常量嵌套从内向外展开,嵌套通过Module.nesting方法审查

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
module X
  module Y
    def self.test
      puts Module.nesting
    end
  end
end
X::Y.test

module A
  module B
     def self.test
      puts Module.nesting
    end
  end
end
A::B.test

module X::Y
  module A::B
    def self.test
      puts Module.nesting
    end
  end
end
A::B.test

# >>
#[X::Y, X]
#[A::B, A]
#[A::B, X::Y]

从上面的例子看出,嵌套中的类和模块的名称与所在的命名空间没有必然联系

嵌套是解释器维护的一个内部堆栈,根据下述规则修改: 1. 执行 class 关键字后面的定义体时,类对象入栈;执行完毕后出栈。 2. 执行 module 关键字后面的定义体时,模块对象入栈;执行完毕后出栈。 3. 执行 class << object 打开的单例类时,类对象入栈;执行完毕后出栈。 4. 调用 instance_eval 时如果传入字符串参数,接收者的单例类入栈求值的代码所在的嵌套层次。 5. 调用 class_evalmodule_eval 时如果传入字符串参数,接收者入栈求值的代码所在的嵌套层次. 6. 顶层代码中由 Kernel#load 解释嵌套是空的,除非调用 load 时把第二个参数设为真值;如果是这样,Ruby 会创建一个匿名模块,将其入栈。

定义类和模块的本质是为常量赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C
end
# => 本质:在Object中创建一个常量C,并将一个类对象存储进去

class Project < ApplicationRecord
end
 => 本质:Project = Class.new(ApplicationRecord)


module Admin
end
# => 本质:Admin = Module.new

Admin.name # => "Admin"

常量赋值的一条特殊规则:如果被赋值的对象是匿名类或模块,Ruby 会把对象的名称设为常量的名称。

自此之后常量和实例发生的事情无关紧要。例如,可以把常量删除,类对象可以赋值给其他常量,或者不再存储于常量中,等等。名称一旦设定就不会再变。

0x3 常量解析

0x31 映射

当常量存储在模块中,常量就会和类和模块中的常量表关联映射(类似哈希表)

1
2
3
module Colors
    RED = '0xff0000'
end

解析模块定义体时,会在Colors常量中的常量表新建一条映射,把"RED"映射到字符串"0xff0000"

0x32 Ruby下的解析

相对常量、绝对常量、限定常量

1
2
Billing::Invoice #此时,Billing为相对常量,Invoice为限定常量
::Billing::Invoice #此时,Billing为绝对常量(顶层常量)在Object中查找

相对常量解析: 在代码中的特定位置,假如使用 cref 表示嵌套中的第一个元素,如果没有嵌套,则表示 Object。

  1. 嵌套不为空,在嵌套元素中按元素顺序查找,元素祖先忽略不记
  2. 未果,向上回溯,进去cref的祖先链
  3. 未果,当cref为module时,进入Object查找常量
  4. 未果,在cref上调用const_missing,默认抛出NameError异常,可覆写

限定常量解析: 上面例子 Invoice 由 Billing 限定,解析算法如下

  1. 在 Billing 及其祖先中查找 Invoice 常量
  2. 未果,调用 Billing 的const_missing方法,默认抛出NameError异常

但Rails 的自动加载机制没有仿照这个算法,查找的起点是要自动加载的常量名称和限定的类或模块对象

如果缺失限定常量,Rails 不会在父级命名空间中查找。

但是有一点要留意:缺失常量时,Rails 不知道它是相对引用还是限定引用。

如果类或模块的父级命名空间中没有缺失的常量,Rails 假定引用的是相对常量。否则是限定常量。

还有在Rails开发环境中,常量时惰性加载的。遇到不存在的常量再触发const_missing使用Rails的自动加载机制

但在生产环境中,预先把所有 autoload 目录下的文件都加载过了。没有触发const_missing使用Ruby本身的常量查找

0x4 加载机制

config.cache_classes 设为 false 时,Rails 会重新自动加载常量

在应用运行的过程中,如果相关的逻辑有变,会重新加载代码。为此,Rails 会监控下述文件:

  • config/routes.rb
  • 本地化文件
  • autoload_paths 中的 Ruby 文件
  • db/schema.rb 和 db/structure.sql

如果这些文件中的内容有变,有个中间件会发现,然后重新加载代码。

主要原理: 1. 先覆写 const_missing 方法,按需去load对应依赖 2. 监听文件变化,自动加载机制会记录自动加载的常量 3. 检测到发生变更,重新加载机制使用 Module#remove_const 方法把它们从相应的类和模块中删除 4. 这样,运行代码时那些常量就变成未知了,从而按需重新加载文件。

但是,因为类之间的依赖极难处理。Rails默认reloader模块经常比较极端,不止重新加载有变化的代码,而是重载一切

0x41 Ruby Module#autoload 的缺陷

Module#autoload 是Ruby 提供的惰性常量加载机制,可以遍历应用树调用autoload把文件名和常规的常量名对应起来

但是,Module#autoload 只能使用 require 加载文件,因此无法重新加载。

不仅如此,它使用的还仅是 require 关键字,而不是 Kernel#require 方法。

因此,删除文件后,它无法移除声明。如果使用 Module#remove_const 把常量删除了,不会触发 Module#autoload

综上,在Rails的常量自动加载机制中使用了覆写Module#const_missing 的方式来实现

Rails(ActiveSupport) 中的会根据触发 const_missing 的常量名称来猜测并尝试加载对应的文件, 以加载 Auth::User 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# demo/role.rb
module Demo
  class Role
    User
  end
end

# demo/user.rb
module Demo
  class User
    "class Demo::User loaded"
  end
end

# app/models/auth/user.rb
module Auth
  class User
  end
end

Demo::Role 找不到 User 常量, 触发 const_missing(const_name), 此处 const_name == 'User'

ActiveSupport 中先拼接出来一个查询的起点 “#{Demo::Role.name}::#{const_name}”, 即 Demo::Role::User

首先尝试查找 autoload_paths 下的 demo/role/user.rb, 没找到

然后往上走一层, 尝试查找 autoload_paths 下的 demo/user.rb, 找到了

Magit Inside Emacs

基本介绍

Magit 是 Emacs 下对git的封装,利用Magit可以让你在Emacs中即可完成对git仓库的管理(Emacs果然是一个伪装成编辑器的操作系统)

其次,Magit本不是Emacs的内置插件,使用时需要自己安装;具体的安装方法在Magit的官网上已经有相关教程了,这里我就不再赘述了

日常使用

下面主要是列举一些日常开发和git管理中比较常使用的一些功能:

1. M-x:magit-status(C-x g)

magit-status.png 该命令就类似于git status(查看项目的当前状态);但是,在Magit中显示的状态信息会比git status更加丰富 其中包括:HEAD、Tag、未追踪文件、Stash列表、未staged文件、未push文件、最近commit等信息 然后,在相应的条目上回车还可以进行看到更加详细的内容,包括对应文件的修改、具体的commit信息等等

2. (?) Magit Help

magit-help.png 在magit status buffer中键入?可以提示Magit的功能列表以及其相对应的key bindings,新手通过这样一个帮助列表,就可以找到对应的git功能一一操作试试,一段时间后就可以熟悉整个magit的操作了

3. (s/S)Stage/Stage all

Stage命令就类似于git add操作,在你修改了相关的git管理下的文件后,若是还未运行git add时,该文件处于Unstaged的状态 处于Unstaged状态的文件在git commit的时候其变更内容就不会提交;而运行git add [filename] 之后文件就变成Stage状态了, 此时如果再执行git commit,对应的文件变更内容则提交到本地,然后文件状态变更未Unpushed

4. (u/U) Unstage/Unstage all

Unstage命令就是Stage命令的反向操作,其对应git reset HEAD [filename],在Magit中Stage/Unstage不仅能够作用于单个文件、所有的changes,还能作用于某个文件的部分区域上;在magit展开文件的diff时,你还发现在文件差异中用@@符号区分的差异区域,在对应的区域内键入Stage/Unstage命令即可仅仅存在该区域中的变更,然后在commit提交时,也可以单单提交这一部分变更

diff.png

5. © Commit

在magit-status-buffer中键入c键即为最常用的git commit指令,当然后除了最普通的Commit(cc)操作外 Magit还支持许多commit的扩展命令

key bindings command description
cc Commit 最普通的 git commit
ce Extend 当前Staged的文件合并到上一次提交中 git commit –amend –no-edit
ca Amend 只修改上次提交的日志 git commit –amend

6. (F) Pull

Pull命令对应git pull在git管理中用于拉取远程仓库代码,常用的组合命令有以下:

key bindings description
Fu pull from upstream
Fp pull from pushremote
Fe pull from elsewhere 会引导你从哪pull

对于Fu 和 Fp来说,upstream是pushremote的上级,这样的场景对应fork分支开发的工作流; 比如User A 有个仓库 Project,User B fork了Project,这样对于User B来说 UserA/Project就是upstream,而pushremote是UserB/Project 另外,在Magit中只有设置了pushremote分支,这样magit status buffer才会显示有哪些变更没有push和pull

7. (P) Push

对应git push 命令

key bindings command description
pu push to upstream 最普通的git push
pe push to elsewhere 会引导你push到哪个远程分支
po push another branch to 会引导你push到哪个分支
pT push a tag push 一个tag标签
pt push all tag push 所有tag标签

除此之外,上面的key bindings组合还可以添加对应的扩展参数,比如强制push,即p-Fu

8. (l) Log查看日志

对应git log,查看git日志记录

key bindings description
ll 查看当前分支的日志
lo log other 查看其他分支的日志

在具体的commit上使用 l 键还可以根据给出的命令组合进一步查看commit提交的详情信息

9. (a/A) cherry picking

对应git cherry-pick,选择某一次的commit在当前分支重新commit一次,适用于合并代码但又不想merge整个PR和分支的场景

10. (z)Stash

对应git Stash,将临时的未commit的变更内容暂存起来 常用命令有:

key bindings command description
zz git stash 暂存
zp git stash pop 恢复

除此之外,还有一个有意思的用法是,当你希望单单想stash变更文件列表中的一个文件时,可以先将目标文件Stage在index索引区,然后适用 zi 组合暂存index区域,这样就可以实现单一文件的stash功能

11. (k) Discard/Delete

对应git中的checkout之类的命令,用作于放弃更改和删除相关的操作,例如,放弃一个Unstage文件的更改、删除一个Stash、删除一个@@区域差异等等

12. (x) Resetting

类似git reset命令,放弃最近的n次提交,这n次的提交内容变成staged状态,之后可以进行合并提交或者丢弃 只需要在日志日光标定位到想要丢弃的log,即可回滚到这一次的提交状态

13. (m) Merge

对应git中合并分支的操作,常用的组合命令为 mm ,之后会提示选择与哪个分支进行merge

14. ® Rebase

对应git中的变基操作

key bindings description
ru rebase on upstream
rp rebase on pushremote
re 会提示你以哪个分支为基点进行rebase

Railsçš„ActiveSupport::Concern

1)简单内部类

1
2
3
4
5
6
7
8
module ActiveSupport::Concern
  .....
  class MultipleIncludedBlocks < StandardError #:nodoc:
    def initialize
      super "Cannot define multiple 'included' blocks for a Concern"
    end
  end
end

ActiveSupport::Concern简单定义了一个内部的错误类型, 这个自定义错误类型主要用于提醒我们在扩展了ActiveSupport::Concern的模块中

只能够显式调用模块方法ActiveSupport::Concern::included一次,

第二次调用的话就会抛出这个自定义的错误类型。

2)设置“胎记”

我们前面说过扩展了ActiveSupport::Concern的模块我把它简称为依赖模块,那我们怎么知道一个模块是不是依赖模块呢?答案就在这个类方法。

1
2
3
4
5
6
module ActiveSupport::Concern
  .....
  def self.extended(base) #:nodoc:
    base.instance_variable_set(:@_dependencies, [])
  end
end

写过Ruby的应该都知道,这个是当一个模块被扩展(extend)之后就会被调用的一个回调方法,

并以扩展它的模块做为参数(base)传入该回调方法。

当一个模块扩展了ActiveSupport::Concern之后就会在模块内部设置一个实例变量

@_dependencies并设置为空数组,我们姑且把它看做是ActiveSupport::Concern的“胎记”,

后面我们会利用这个“胎记”所包含的依赖项,优雅地增强我们的终端模块。

3) 收集并扩展相关方法

在分析后面的方法之前我们先来简单看一下的原理。

我们都知道在Ruby里面如何定义一个类,并且定义相关的类方法,和实例方法

(我们只需要用class关键字就能够很容易做到这一点),但不知道大家是否了解还有这样一种方式?

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
instance_block = Proc.new do
  def instance_method
    puts "I am instance method"
  end

  def self.class_method_in_instance_block
    puts "I am class method in instance block"
  end
end

class_block = Proc.new do
  def class_method
    puts "I am class method"
  end
end


Example = Class.new

Example.class_eval(&instance_block)
ClassModule = Module.new
ClassModule.module_eval(&class_block)
Example.extend(ClassModule)

Example.class_method
Example.class_method_in_instance_block
Example.new.instance_method

运行结果

I am class method I am class method in instance block I am instance method

运行结果有点意思,下面简单来分析一下: 我们以代码块的方式收集了类方法class_method,class_method_in_instance_block,

以及实例方法instance_method。然后我们创建一个空的类Example,

用Class#class_eval方法来打开类,在类的上下文环境下运行块instance_block,

其实这就相当于我们在类上下文中运行相应的语句,这样就能够得到Example#instance_method,

Example::class_method_in_instance_block这两个方法了。 另外,我们从已有的知识中了解到,可以通过Class#extend扩展模块的方式来获得类方法。

为此我们可以把class_block这个代码块包裹在模块ClassModule中,

最后我们只需要扩展(extend)这个模块就可以得到相应的类方法Example#class_method了。

ActiveSupport::Concern其实就是这种黑科技,

这里我简单把接下来的过程分为两部分

1)收集方法。

2)对收集的方法进行功能扩展

  1. 方法收集 来看看依赖模块如何收集方法的?
1
2
3
4
5
6
7
8
9
10
11
12
module ActiveSupport::Concern
  .....
  def included(base = nil, &block)
    if base.nil?
      raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)

      @_included_block = block
    else
      super
    end
  end
end

首先我们来看ActiveSupport::Concern#included,

当ActiveSupport::Concern被扩展之后这个included方法就会变成相应模块的类方法了。

咦,这不是一个模块被包含之后才会被调用的回调函数吗?没错,为了不影响它原来的功能rails团队采用了个巧妙的做法,

当我们在扩展了ActiveSupport::Concern的模块的上下文中显式调用included方法,

并且不带任何参数而只传入代码块的情况下,便会在模块内部设置一个@_included_block实例变量来接收这个代码块,

换句话说这个实例变量就是我们将来需要在终端模块上下文运行的代码块。

而在其他情况下则通过super关键字来调用原始版本的included方法。

这样既增强了included方法又不影响原始方法的使用。

另外我们也注意到了,在一个模块里面included方法只能被显式调用一次,

否则会抛出MultipleIncludedBlocks这个自定义的错误,这便是前面定义的内部类的应用场景。

OK,收集完需要在模块上下文运行的代码,我们接下来要收集类方法了。

1
2
3
4
5
6
7
8
9
10
module ActiveSupport::Concern
  .....
  def class_methods(&class_methods_module_definition)
    mod = const_defined?(:ClassMethods, false) ?
      const_get(:ClassMethods) :
      const_set(:ClassMethods, Module.new)

    mod.module_eval(&class_methods_module_definition)
  end
end

其实这个东西跟前面的included方法原理差不多,也是通过代码块来收集方法,只是有一点点不同,

我们需要把这个收集到的代码块包裹到一个模块中,以后再扩展这个模块。

首先通过const_defined?来判断常量ClassMethods是否存在,

该方法的第二个参数false表示只从当前模块查找该常量,而不会从祖先链中去查找。

如果没有则以ClassMethods为名定义一个模块,

然后以Module#module_eval方法打开该模块并在模块的上下文运行我们所接收的代码块class_methods_module_definition。

这样模块ClassMethods就会包含对应的方法了。

在以后的日子里我们只需要扩展ClassMethods,该模块里面的方法就能成为目标模块的类方法了。

  1. 功能增强 收集完相关功能之后可以来看如何增强我们的终端模块了。在分析代码之前,

先来认识一下append_features,我们需要知道的是它会在模块被包含的时候调用,并且它会在included回调方法之前被调用。

接下来我们看看扩展功能的源代码,它应该是ActiveSupport::Concern里面最复杂的方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module ActiveSupport::Concern
  ....
  def append_features(base)
    if base.instance_variable_defined?(:@_dependencies)
      base.instance_variable_get(:@_dependencies) << self
      return false
    else
      return false if base < self
      @_dependencies.each { |dep| base.include(dep) }
      super
      base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
      base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
    end
  end
end

现在我们可以看到之前设置“胎记”起作用了,设置了“胎记”的依赖模块也可以被其他的依赖模块所包含,

但他们并不会进行相应的功能扩展,他们会做的只是在“胎记”列表@_dependencies里面添加对应的依赖选项。

然后返回false。这也是我们第一个条件分支的逻辑。 当我们的依赖模块终于被终端模块(不含“胎记”的模块)包含的时候我们程序便可以走else分支的逻辑。

如果该依赖模块已经在终端模块的祖先链中的话则表明这个模块已经在终端模块增强过了,我们就没必要做重复工作,直接返回false。

否则的话继续执行后面的代码,接下来这个语句有点意思 @_dependencies.each { |dep| base.include(dep) }

我们会以终端模块的身份去包含当前依赖模块的@_dependencies列表里面的所有模块,

这个时候我们@_dependencies的模块又会进入各自相应的append_features方法,并且都会走else分支,

然后查看各自@_dependencies接下来又会以终端模块的身份再去包含列表里的那些模块,

以此类推。这递归的过程就像是链式反应,这样就能保证不管各个依赖模块之间的依赖关系如何,

我们的终端模块都不用太过在意了,反正最后都会被我们终端模块给包含掉。

接下来调用super关键字来调用原始的append_features方法,保证了原始的功能。

毕竟我们这里做的是对原始功能加强,而并不是要复写掉原始功能。

最后我们在每一个依赖模块内部都会执行大家所熟悉的语句 base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods) base.class_eval(&@included_block) if instance_variable_defined?(:@included_block)

最最最后,咱们的终端模块就会具备我们预先定义好的类方法,实例方法。

并且可以直接运行一些预先定义好需要在模块上下文运行的类方法了。

优雅的Ruby

源码分析暂时告一段落,为了让我们对ActiveSupport::Concern印象更加深刻一些。最后我用一个简单的例子来展示一下这个库的优雅之处

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
require 'active_support'

module A
  extend ActiveSupport::Concern

  included do
    def number1
      "number1 method"
    end
  end

  class_methods do
    def active1
      "active1 class method"
    end
  end
end

module B
  extend ActiveSupport::Concern
  include A

  included do
    def number2
      "number2 method"
    end
  end

  class_methods do
    def active2
      "active2 class method"
    end
  end
end

module C
  extend ActiveSupport::Concern
  include B

  included do
    def number3
      "number3 method"
    end

    def number2
      "number2 in C"
    end
  end

  class_methods do
    def active3
      "active3 class method"
    end

    def active2
      "active2 in C"
    end
  end
end

class Example
  include C
end

p "#{A.instance_variable_get("@_dependencies")}"
p "#{B.instance_variable_get("@_dependencies")}"
p "#{C.instance_variable_get("@_dependencies")}"

puts Example.active3
puts Example.active2
puts Example.active1

puts Example.new.number3
puts Example.new.number2
puts Example.new.number1

最后的打印结果是

“[]” “[A]” “[B]” active3 class method active2 in C active1 class method number3 method number2 in C number1 method

利用ActiveSupport::Concern我们可以用少量的代码,优雅地定义我们的扩展模块,并在需要的时候进行功能增强。

在该例子中我通过打印“胎记”可以知道依赖模块之间的依赖关系,

而且他们之间的依赖关系终端模块根本不会在意,它只需要包含一个依赖模块,

便可以得到有依赖关系其他依赖模块中所定义的功能了。

另外,我在模块C中做了些手脚,复写了它所依赖的模块B中所收集的方法,

是为了展示我们所收集方法在各个有依赖关系模块中的优先级关系。

文本建模算法入门-1



文本建模算法入门(一)

0x1 文本建模

文本并不像其他数值型的数据一样可以比较轻易的通过运算、函数、方程、矩阵等来表达他们之间的相互关系。

在处理一篇文本的时候,假设每一个文本储存为一篇文档,那么从人的视角来看,这可以说是一段有序的词序列
统计学家将这些序列的生成,生动地描绘成了一个“上帝的游戏”,即人类产生的所有语料的文本都可以看作是:一个伟大的上帝在天堂中掷骰子形成的。我们所看到的文本其实就是这个游戏掷了若干次后产生的序列。

那么,在这个游戏中,我们最需要关注的两个核心问题就出现了:

  1. 上帝有怎样的骰子?
  2. 上帝是怎么掷的骰子?

对应这两个问题,各大学家持着不同的观点,于是便有了以下三种模型:

  1. Unigram Model
  2. Topic Model(PLSA)
  3. LDA

0x11 Unigram Model

Unigram Model是非常简单直接的,它假设

  1. 上帝只有一个V面的骰子,每一个面对应一个词,同时每个面的概率不一样。(这里可以通过“老千骰子”来理解下,有些面因为被做过了“手脚”,所以抛到的几率就大了,那如果没个面都这样,那么每个面抛到的几率也就不一样了)
  2. 每抛一次骰子,抛出的面就对应有一个词,那么抛n次骰子后,按抛掷顺序产生的序列就生成了一篇n个词的文档

enter image description here

那现在我们把上帝这个唯一的骰子各个面的概率记为,然后我们把掷这个V面骰子的实验记作

那么对应一篇文档d,该文档生成的概率就是

而文档和文档之间我们认为是独立的,所以如果语料中有多篇文档,则该语料生成的概率就是

在Unigram Model中,我们又假设了文档之间是独立可交换的。即,词与词之间的顺序对文档表示并不造成影响,一篇文档相当于一个袋子,里面装着一些词。这样的模型也称为词袋模型(Bag-of-words)。

那么,如果语料中的总词频是N,在N个词中,如果我们关注每一个词的发生次数,则正好是一个多项分布

此时,语料的概率是

0x110 贝叶斯Unigram Model假设

在贝叶斯统计学派看来,上帝拥有唯一一个固定的骰子是不合理的。于是他们觉得以上模型的骰子不应该唯一固定,而应该也是一个随机变量。

那这样我们就可以将整个掷骰子的游戏过程更新成以下形式:

  1. 上帝有一个装着无穷多骰子的罐子,里面有各种骰子,每个骰子有V个面。每一个面对应一个词
  2. 上帝从罐子抽出一个骰子,然后用这个骰子不断的抛,每抛一次骰子,抛出的面就对应有一个词,那么抛n次骰子后,按抛掷顺序产生的序列就生成了一篇n个词的文档
    enter image description here

在以上的假设之下,由于我们事先并不知道上帝用了哪个骰子,所以每个骰子都是有可能被使用的,只是使用的概率由先验分布决定,对每一个具体的骰子,由该骰子产生数据的概率是,所以最终数据产生的概率就是对每个骰子上产生的数据概率进行积分累加求和

在贝叶斯分析的框架之下,此处的先验分布概率其实就是一个多项分布的概率,其中一个比较好的选择即多项分布对应的共轭分布:Dirichlet分布

0x12 PLSA Topic Model

再来回顾Unigram Model我们发现:这个模型的假设过于简单,和人类真实的书写有着较大的差距

从人类视角看,我们在日常构思文章中,我们往往要先确定自己文章的主旨,包含了哪些主题,然后再围绕着这些主题展开阐述。

例如,一篇关于现代教育的文章,它可能就包含了这些主题:教育方法、多媒体技术、互联网等。篇幅上可能以教育方法为主,而其他为辅。然后,在不同的主题里面就包含了许多主题领域内常见的关键词。例如,谈到互联网时,我们会提及Web、Tcp等。

这样一种直观的想法就在PLSA模型中进行了明确的体现,如果我们再利用这个想法更新“掷骰子”的游戏就有以下情形:

  1. 上帝有两种类型骰子,一类是doc-topic骰子,骰子共k个面代表了k个主题;一类是topic-word骰子,骰子V个面,每面对应主题内的一个词
  2. 生成文章的时候,首先要先创造一个特定的doc-topic骰子,使得骰子内的主题围绕文章要阐述的主题
  3. 投掷doc-topic骰子,得到一个主题z
  4. 根据得到的主题z,找到对应的topic-word骰子,投掷它得到一个词
  5. 不断重复3,4步,直至文章生成完成

enter image description here

在上面的游戏规则中,文档与文档之间顺序无关,同一个文档内的词的顺序也是无关的。所以还是一个bag-of-words模型。

那么,在第m篇文档Dm中每个词的生成概率为:

:对应游戏中K个topic-word骰子中第z个骰子对应的词列表

:文档对应的第z个主题,即对应的doc-topic

所以整篇文档的生成概率就为:

0x13 LDA(Latent Dirichlet Allocation)

就像Unigram Model 加入贝叶斯框架那样,doc-topic和topic-word都是模型的参数,即随机变量。于是类似对Unigram Model的改造对以上两个骰子模型加入先验分布。然后由于都对应着多项分布,因此先验分布依旧选择Drichlet分布。于是得到的这个新模型就是LDA(Latent Dirichlet Allocation)模型
enter image description here

在LDA模型中,上帝的游戏规则就又被更新成如下情形:

  1. 上帝有两个罐子,第一个装着都哦doc-topic骰子,第二个装着topic-word骰子
  2. 上帝随机的从第二个罐子中独立抽出K个topic-doc骰子,编号1-K
  3. 每次生成新文档时,从第一个罐子随机抽一个doc-topic骰子
  4. 投掷得到的doc-topic骰子。得到一个主题编号z
  5. 在K个主题骰子里面选择编号为z的骰子,投掷骰子,得到一个词
  6. 重复4,5步,直至文档生成完成

0x2 后记

至此,入门篇(一)的内容就写到这了。由于是第一篇,文章中避过了许多较为复杂的数学证明和计算,尤其是关于LDA模型的。这样是为了,先建立对文本建模思路和过程的直观认识,而不是一上来就深究细节。

加上笔者目前也是在学习阶段,后面的细节再慢慢地一一补充,大家共同进步 !!
\(^_^)/

SLAM的复兴-无人驾驶的未来?

SLAM的前世今生

SLAM,全称(simultaneous localization and mapping),中文译做,即时定位与地图构建。是指运动物体根据传感器的信息,一边计算自身位置,一边构建环境地图的过程,解决机器人等在未知环境下运动时的定位与地图构建问题。目前,SLAM的主要应用于机器人、无人机、无人驾驶、AR、VR等领域。其用途包括传感器自身的定位,以及后续的路径规划、运动性能、场景理解。

作为一种基础技术,最早可以追溯到当初的军事潜艇定位技术。而今年来随着扫地机器人的盛行,再次令他名声大噪。加上最近的无人驾驶与机器人的寻迹的崛起以及基于三维视觉的VSLAM的出现又让它越来越显主流。

目前用在SLAM上的Sensor主要分两大类,分别是激光雷达和摄像头。

SLAM到底做了些什么?

既然SLAM技术代表了即时定位与地图构建,那么可以说这就是一个机器人或设备尝试去根据他周围的环境创建地图的过程,并且还可以在该地图中实时定位自己。

这不是一件容易的事,这项技术现在还处在技术研究和设计的前沿。而为了成功实施SLAM有一大障碍是不可i避免的,那就是地图定位问题,两个问题同时引入就变成了我们常说的典型的鸡与蛋问题。为了能够成功地顺利的根据环境创建地图,我们的设备就必须先知道它的方向和位置 ;然而,这些定位信息设备也只能从预先存在的环境地图中获得。

面对这一障碍,SLAM技术通常通过使用GPS数据构建预先存在的环境地图来克服这一复杂的鸡与蛋问题。随着机器人或设备在环境中移动,生成的地图会被迭代地改进。而这项技术的真正挑战是准确性。随着机器人或设备在空间中移动与寻迹,测量评估必须不断进行,并且该技术必须考虑设备移动和测量方法不准确而引起的“噪音”。

enter image description here

这个gif是宾大的教授kumar做的特别有名的一个demo,是在无人机上利用二维激光雷达做的SLAM。

VSLAM又是什么?

VSLAM(基于视觉的定位与建图):随着计算机视觉的迅速发展,视觉SLAM因为信息量大,适用范围广等优点受到广泛关注。

(1)基于深度摄像机的Vslam,跟激光SLAM类似,通过收集到的点云数据,能直接计算障碍物距离;

(2)基于单目、鱼眼相机的VSLAM方案,利用多帧图像来估计自身的位姿变化,再通过累计位姿变化来计算距离物体的距离,并进行定位与地图构建;

enter image description here

以上为百度 VSLAM demo展示。

VSLAM能够为自动驾驶带来些什么?

结合自动驾驶的场景,可以推出VSLAM的应用点主要是:

  • gps缺失场景下的长时间定位,如室内,楼房中。
  • 补偿行驶过程中gps信号不稳定造成的定位跳跃,如山洞,高楼群,野外山区等。

VSLAM的精度及鲁棒性越高,适用的场景越宽广。如果VSLAM能在任何场景无限长定位保持高精度,那其他定位技术就可以下岗了,虽然按目前看来这个可能性很小,因此需要尝试结合IMU,编码器等设备融合。至于最终的技术形态,目前还没有定论。

在无人驾驶汽车上,目前比较显著的瓶颈还在计算方面,数据收发,障碍物感知,融合定位,路径规划,每个模块都需要占据相当的计算资源。而无论视觉还是激光SLAM本身就是对计算力消耗极大的一个模块,如果加上高频率的IMU进行融合优化,则计算力更加捉襟见肘。因此是否值得为视觉SLAM分配原本就那么有限的计算资源,如何识别场景对SLAM模块进行激活都是需要仔细衡量的。