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