最近动态

Java

jvm学习(java se8)

1.各种jvm特点介绍(包含以前的):

  1. Sun Classic VM (jdk1.1 版本的虚拟机)

    特点: 全世界第一个商用的虚拟机,只能使用纯解释器的方式来执行java代码

  2. Exact VM (jdk1.2 solari平台)

    特点: 开始具备高性能vm的特点,有准确性内存管理,解释器和编译器混合使用以及二级即时编译特性,但是只在jdk1.2的solari平台使用过

  3. HotSpot VM (jdk1.3发布)

    特点: 热点代码追踪技术等等

  4. KVM (j2ME 时代sun公司的产物)

    特点:简单,轻便,就是比较慢

  5. JRockit VM (sun公司的产品,2008年呗Oracle收购之后就呗HotSpot整合了)

    特点: 快,有垃圾收集器,MissionControl控制套件(用来控制生产环境内存泄漏的)

  6. J9 VM (IBM 为自家java程序开发的一个VM)

    特点: 高度模块化,任意平台通用(SE,EE,ME),快

  7. DalVik (android 平台的VM)

    特点: 性能好,快.只限android平台使用

扩展: 目前主流的 Java 虚拟机有哪些?

2.JVM 的内存管理

jvm的内存管理分为两大部分,线程共享区和线程独占区,具体划分看下图:

接下来重点讲解几个重要的概念:

2.1动态常量池

存在于方法区中,常量都是不变的,为什么叫动态常量池呢,就是因为这个池可以动态的增加(String的intern()方法),看以下代码:

String a = "abc";
String b = "abc";

logger.info(a == b);  // 1

String c = new String("abc");

logger.info(b == c);  // 2

logger.info(b == c.intern()); // 3

在这里变量a和b就是一个常量,值存放在java的线程共享区的方法区中,引用存放在线程独享区中的虚拟机栈中,在方法区中存在一个StringTable相当于一个HashSet,只会存在一个“abc”实例,所以1对应的输出为true。
变量c的引用存放在线程独享区中的虚拟机栈中,值因为是一个变量,存放在线程共享区中的java堆中,所以和b变量的地址不同,2的对应输出为false。
当调用c.intern()方法时,jvm会将对应的值从java堆移动到方法区中,即动态常量池中。所以3对应的输出为true,对应的流程图如下:

2.2直接内存

java的NIO使用,javaNIO是一种面向缓冲的技术实现框架,其中缓冲的内存使用的不是java堆内存,使用的是直接内存,当然同样受限于物理机器的内存。

2.3对象在内存中的布局

对象的创建步骤:

变量包括数据头(header)和变量值(instanceData)和填充(padding),数据头包含了变量的各种运行时数据(哈希值,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳…)和类型指针。变量值就是变量的值,填充是由于数据的大小只能为8的倍数,当数据大小不为8的倍数时,就会进行填充位数。

3. 垃圾回收

3.1 在哪里进行垃圾回收?

大部分是线程共享区的java堆,小部分是线程共享区的方法区(可设置关闭这个区的 GC)

3.2什么时候开始回收?

如今的JVM在java堆里面分了两个代,新生代和老年代。新生代里面有三个区,Eden区和两个survivor区。当Eden区满的时候,会触发一次 Minor GC,会将Eden区中剩余的对象拷贝至一个survivor区中,对象的存活年龄加1,清空Eden区,再次存放对象,等Eden区再次满的时候,触发 Minor GC,将Eden区中剩余的对象和survivor区剩余的对象移动至另一个survivor区中,存活年龄再+1。等年龄增长到了默认值的时候,剩余的对象就会从新生代复制到老年代。当老年代快要满的时候(虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小),如果大于,直接触发 Minor GC. 如果不停的 Minor GC 下去,老年代还是要满了,就会根据配置的参数来决定是要同时执行FULL GC 和 Minor GC 还是只执行 Minor GC 。

3.3 怎么回收?

回收算法有两类:

  • 引用计数收集算法

    判断对象的实例是否不再被引用了。当引用为0的时候,就把他标记清除。这样有个问题就是当别的对象实例引用它而引用指针为null的时候,这个就不会被收集。导致垃圾存留。这个算法已经不被使用了。

  • 可达性分析法

    可达性分析法都是需要从根GC 扫描判断对象是否被引用,被引用了就标记起来,扫描完了后再扫描一遍内存,查看哪些对象没被标记,没被标记则为待回收对象。针对所有的对象衍生出三种收集方法

    1. 复制法:新开辟一片内存区域,每标记一个对象,就将标记对象复制一份到新内存上。虽然需要消耗一定的内存,但在对象数量特别少的时候这个算法特别高效(新生代的Eden区使用的就是它)。
    2. 标记-清除算法: 清除所有待回收对象,然后不对内存进行整理,导致内存空间不连续,生成内存碎片。
    3. 标记-整理算法: 在清除完所有待回收对象之后,对内存进行整理排序,虽然成本增加了,但效率确实提高了不少。
      根GC 对象的选择: 虚拟机栈中所引用的对象,方法区中类的引用的对象,方法区中常量的引用的对象,本地方法区中引用的对象。四种选择方案

3.4 垃圾收集器

  • Serial 垃圾收集器
    Serial垃圾收集器是最初的垃圾收集器,在GC的时候需要停止用户线程,如果待回收对象太大会造成卡顿现象,适合于client端的java应用。

  • ParNew 垃圾收集器
    在Serial垃圾收集器的基础上增加了并发收集的实现。可和Cms垃圾收集器一起工作(ParNew负责新生代的收集工作),

  • Parallel 垃圾收集器
    也是并行的多线程收集器,只不过关注点在于控制垃圾收集的吞吐量(其他的关注点在于减少垃圾的收集时间)。

  • Cms垃圾收集器
    这个是可以并发执行的一款垃圾收集器,执行流程如下图:

  • G1 垃圾收集器
    目前为止最牛逼的垃圾收集器,智能型垃圾收集器,虽然原理和流程和Cms垃圾收集器一样,但是里面却有几个不同的概念,比如Region,可预测时间模型等等。

参考: 深入理解JVM(3)——7种垃圾收集器
扩展: JVM内存管理及GC机制

4 内存分配

4.1 对象优先分配到Eden区上

在jvm中,除大对象以外,所有的对象几乎都是优先分配Eden区里面,看以下代码:

1
2
3
4
5
6
7
8
9
10
11
public static final int M = 1024 * 1024;
public static void main(String[] args) {
byte[] b1 = new byte[2 * M];
byte[] b2 = new byte[2 * M];
byte[] b3 = new byte[2 * M];
byte[] b4 = new byte[4 * M];
System.gc();
}

配置运行参数为: -Xms20m -Xmx20m -Xmn10m -verbose:gc -XX:+PrintGCDetails -XX:SurvivorRatio=8 运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
[GC (Allocation Failure) [PSYoungGen: 6914K->952K(9216K)] 6914K->5056K(19456K), 0.0045301 secs] [Times: user=0.05 sys=0.00, real=0.00 secs]
[GC (System.gc()) --[PSYoungGen: 7252K->7252K(9216K)] 11356K->13412K(19456K), 0.0019800 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 7252K->4840K(9216K)] [ParOldGen: 6160K->6145K(10240K)] 13412K->10986K(19456K), [Metaspace: 3494K->3494K(1056768K)], 0.0071106 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 9216K, used 5087K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 62% used [0x00000000ff600000,0x00000000ffaf7c38,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 6145K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 60% used [0x00000000fec00000,0x00000000ff200518,0x00000000ff600000)
Metaspace used 3509K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K

这里设置了Eden区大小为8M,两个Survivor区各1M,老年代10M。开始将b1,b2,b3都分配在Eden区,在分配b4的时候,会用到空间分配担保(-XX:+HandlePromotionFailure)技术(jdk8默认开启),然后会将b1,b2,b3 分配至老年代,b4分配到Eden区,占4M

4.2 大对象优先分配到老年代

在jvm的理解中,大对象一般不会变化的,所以会将大对象放入老年代。大对象的确定是动态确定的,根据你设置的Eden去大小而定,大于Eden区,就是大对象,直接将其放入老年代储存。

4.3多次存活的对象分配到老年代

jvm有一个存活阈值,对象生存年龄大于这个阈值就会将对象存入老年代,对象实际进入年老代的年龄是虚拟机在运行时根据内存使用情况动态计算的,有可能存活一两次就进去了。设置阈值的参数是: -XX:MaxTenuringThreshold=15 这个参数指定的是阈值年龄的最大值,默认是15

4.4 逃逸分析和栈上分配

对象放入堆区之前会进行逃逸分析,当方法内的对象不引用别的方法区域内的对象或者全局变量,则称该变量不发生逃逸,会将该变量进行栈上分配,也就是分配到线程独占区的虚拟机栈区里面。否则分配到java堆里面。

本节扩展【JVM】12_空间分配担保

5 性能分析工具

5.1 Jps

jps 就是查看java进程id和开启进程具体命令的一个工具.类似linux的ps命令. 常用使用方法为: jps -mlv (-m: 查看运行的参数,-l: 具体到包名的运行类, -v: 查看运行的vm参数)。 结果截图:

5.2 Jstat

jstat 是用来查看java进程的各种详细信息,类似linux的top命令,它有多个运行参数。常用命令如下: jstat -gcutil [vmid] vmid 即java进程Id(PID),vmid 可以根据jps命令获取到。例如: jstat -gcutil 20412,得到的结果如下所示:

S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
0.81   0.00  78.06  32.64  93.06  89.60    784   17.194    70   72.187   89.381

S0: survivor 0 区占用的百分比(KB)
S1: survivor 1 区占用的百分比(KB)
E: Eden 区占用的百分比(KB)
O: 老年代占用的百分比(KB)
M: Metadata 空间利用率(KB)
CCS: 类压缩空间的百分比(KB)
YGC: Minor GC总次数
YGCT:Minor GC总耗时(s)
FGC: Full GC 总次数
FGCT: Full GC总耗时(s)
GCT: 用于垃圾回收的总时间(s)

更多Jstat 选项信息参考jstat命令官方文档

5.3 Jinfo

jinfo命令是用来查看并修改java进程运行的VM参数的命令。使用方法: jinfo -flag [vmid], 例如:

+ 表示启用了这个参数(用的是Parallel GC)
更多使用方法: jinfo -help

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Usage:
jinfo [option] <pid>
(to connect to running process)
jinfo [option] <executable <core>
(to connect to a core file)
jinfo [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server)
where <option> is one of:
-flag <name> to print the value of the named VM flag
-flag [+|-]<name> to enable or disable the named VM flag # 这个参数就是开启或关闭某个VM功能
-flag <name>=<value> to set the named VM flag to the given value # 这个就相当于开启并设置参数值
-flags to print VM flags
-sysprops to print Java system properties # 列出系统所有属性(windows可能用不了)
<no option> to print both of the above
-h | -help to print this help message

5.4 Jmap

jmap 用来查看java进程的堆栈信息,可根据选项打印到控制台或者保存到文件里面.
帮助信息: jmap -help

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
Usage:
jmap [option] <pid>
(to connect to running process)
jmap [option] <executable <core>
(to connect to a core file)
jmap [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server)
where <option> is one of:
<none> to print same info as Solaris pmap
-heap to print java heap summary
-histo[:live] to print histogram of java object heap; if the "live" # 直接在控制台打印java进程的内存使用信息
suboption is specified, only count live objects
-clstats to print class loader statistics
-finalizerinfo to print information on objects awaiting finalization
-dump:<dump-options> to dump java heap in hprof binary format
dump-options:
live dump only live objects; if not specified,
all objects in the heap are dumped.
format=b binary format
file=<file> dump heap to <file>
Example: jmap -dump:live,format=b,file=heap.bin <pid>
-F force. Use with -dump:<dump-options> <pid> or -histo
to force a heap dump or histogram when <pid> does not
respond. The "live" suboption is not supported
in this mode.
-h | -help to print this help message
-J<flag> to pass <flag> directly to the runtime system

例如: jmap -dump:format=b,file=f:\jmap.bin 13756
该命令会将 pid为13756进程id的信息保存到f盘的jmap.bin文件里,怎么打开?请看下文

5.5 Jhat

jhat命令就是用来打开jmap命令生成的二进制文件的,应为它是非常耗费内存和cpu的,所以一般会在自己本地环境运行这个命令来查看java进程内存使用信息,而不会在服务端做这种事。打开后,会在http服务器中将这些信息展示出来,默认端口为7000,可通过 -port 参数指定。
帮助信息: jhat -help

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
Usage: jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file>
-J<flag> Pass <flag> directly to the runtime system. For
example, -J-mx512m to use a maximum heap size of 512MB
-stack false: Turn off tracking object allocation call stack.
-refs false: Turn off tracking of references to objects
-port <port>: Set the port for the HTTP server. Defaults to 7000
-exclude <file>: Specify a file that lists data members that should
be excluded from the reachableFrom query.
-baseline <file>: Specify a baseline object dump. Objects in
both heap dumps with the same ID and same class will
be marked as not being "new".
-debug <int>: Set debug level.
0: No debug output
1: Debug hprof file parsing
2: Debug hprof file parsing, no server
-version Report version number
-h|-help Print this help and exit
<file> The file to read
For a dump file that contains multiple heap dumps,
you may specify which dump in the file
by appending "#<number>" to the file name, i.e. "foo.hprof#3".
All boolean options default to "true"

例如: jhat f:\jmap.bin
等待一会,看到 Started HTTP server on port 7000 字样的时候,打开 http://127.0.0.1:7000 就可以看到所有的实例信息了,这里我们主要看两个地方,一个是 http://127.0.0.1:7000/histo/ ,查看Heap Histogram,查看的是所有类型对象的实例总数和实例大小。还有一个是 http://127.0.0.1:7000/oql/, 一个oql(Object query Language)查询工具,和sql一样,可以根据条件筛选出指定的对象,例如下面这个语句就是用来查询所有字符串长度大于1000的字符串对象:

1
select s from java.lang.String s where s.value.length > 1000

更多有关oql的语法可以查看他的帮助文档 http://127.0.0.1:7000/oqlhelp/

5.6 Jstack

jstack 命令就是用来查看java进程的线程信息的一个工具。帮助: jstack -help

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Usage:
jstack [-l] <pid>
(to connect to running process)
jstack -F [-m] [-l] <pid>
(to connect to a hung process)
jstack [-m] [-l] <executable> <core>
(to connect to a core file)
jstack [-m] [-l] [server_id@]<remote server IP or hostname>
(to connect to a remote debug server)
Options:
-F to force a thread dump. Use when jstack <pid> does not respond (process is hung)
-m to print both java and native frames (mixed mode)
-l long listing. Prints additional information about locks
-h or -help to print this help message

例如: jstack -l 13756
会在控制台打印出许多有关的线程信息,其实也就是Thread的getAllStackTraces() 方法返回出来的信息,截取其中的一段显示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"Netty Builtin Server 1" #16 prio=5 os_prio=0 tid=0x0000000018f2b000 nid=0x2100 runnable [0x000000001bf3f000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.WindowsSelectorImpl$SubSelector.poll0(Native Method)
at sun.nio.ch.WindowsSelectorImpl$SubSelector.poll(WindowsSelectorImpl.java:296)
at sun.nio.ch.WindowsSelectorImpl$SubSelector.access$400(WindowsSelectorImpl.java:278)
at sun.nio.ch.WindowsSelectorImpl.doSelect(WindowsSelectorImpl.java:159)
at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
- locked <0x00000000e155f760> (a io.netty.channel.nio.SelectedSelectionKeySet)
- locked <0x00000000e155f790> (a java.util.Collections$UnmodifiableSet)
- locked <0x00000000e29d20e8> (a sun.nio.ch.WindowsSelectorImpl)
at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
at io.netty.channel.nio.SelectedSelectionKeySetSelector.select(SelectedSelectionKeySetSelector.java:62)
at io.netty.channel.nio.NioEventLoop.select(NioEventLoop.java:752)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:408)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:858)
at java.lang.Thread.run(Thread.java:745)
Locked ownable synchronizers:
- None

5.7Jconsole 和 VisualVM

Jconsole是jdk自带的可视化分析工具,VisualVM是一个集众多jvm分析插件工具于一身的jvm强大实时可视化分析工具,Jconsole在VisualVM中就是一个插件而已,主页VisualVM。能有效分析内存分配和线程等等指标。VisualVM启动前需要配置jdk的路径,否则会显示找不到jdk。

6 Class文件结构

Class 文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部都是程序运行的必要数据。根据 Java 虚拟机规范的规定,Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。无符号数相当于java中的基本数据类型,以 u1、u2、u4、u8 来分别代表 1、2、4、8 个字节的无符号数。表相当于抽象数据类型,由多个无符号数或其他表作为数据项构成的复合数据类型,所有的表都习惯性地以“_info”结尾。整个 Class 文件本质上就是一张表,它由如下所示的数据项构成:

名称 类型 数量
magic U4 1
minor_version U2 1
major_version U2 1
constant_pool_count U2 1
constant_pool cp_info constant_pool_count - 1
access_flags U2 1
this_class U2 1
super_class U2 1
interfaces_count U2 1
interfaces U2 interfaces_count
fields_count U2 1
fields field_info fields_count
methods_count U2 1
methods method_info methods_count
attributes_count U2 1
attributes attribute_info attributes_count

这个表的顺序就是class文件的解析顺序,接下来看一个 java 类(Hello.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
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class Hello extends FileInputStream implements Runnable,ActionListener{
private String name;
public Hello(String name) throws FileNotFoundException {
super(name);
}
@Override
public void actionPerformed(ActionEvent e) {
}
@Override
public void run() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

编译后的字节码为(Hello.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
CA FE BA BE 00 00 00 34 00 1F 0A 00 04 00 18 09
00 03 00 19 07 00 1A 07 00 1B 07 00 1C 07 00 1D
01 00 04 6E 61 6D 65 01 00 12 4C 6A 61 76 61 2F
6C 61 6E 67 2F 53 74 72 69 6E 67 3B 01 00 06 3C
69 6E 69 74 3E 01 00 15 28 4C 6A 61 76 61 2F 6C
61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 01 00 04
43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65
72 54 61 62 6C 65 01 00 0A 45 78 63 65 70 74 69
6F 6E 73 07 00 1E 01 00 0F 61 63 74 69 6F 6E 50
65 72 66 6F 72 6D 65 64 01 00 1F 28 4C 6A 61 76
61 2F 61 77 74 2F 65 76 65 6E 74 2F 41 63 74 69
6F 6E 45 76 65 6E 74 3B 29 56 01 00 03 72 75 6E
01 00 03 28 29 56 01 00 07 67 65 74 4E 61 6D 65
01 00 14 28 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F
53 74 72 69 6E 67 3B 01 00 07 73 65 74 4E 61 6D
65 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00
0A 48 65 6C 6C 6F 2E 6A 61 76 61 0C 00 09 00 0A
0C 00 07 00 08 01 00 05 48 65 6C 6C 6F 01 00 17
6A 61 76 61 2F 69 6F 2F 46 69 6C 65 49 6E 70 75
74 53 74 72 65 61 6D 01 00 12 6A 61 76 61 2F 6C
61 6E 67 2F 52 75 6E 6E 61 62 6C 65 01 00 1D 6A
61 76 61 2F 61 77 74 2F 65 76 65 6E 74 2F 41 63
74 69 6F 6E 4C 69 73 74 65 6E 65 72 01 00 1D 6A
61 76 61 2F 69 6F 2F 46 69 6C 65 4E 6F 74 46 6F
75 6E 64 45 78 63 65 70 74 69 6F 6E 00 21 00 03
00 04 00 02 00 05 00 06 00 01 00 02 00 07 00 08
00 00 00 05 00 01 00 09 00 0A 00 02 00 0B 00 00
00 22 00 02 00 02 00 00 00 06 2A 2B B7 00 01 B1
00 00 00 01 00 0C 00 00 00 0A 00 02 00 00 00 0B
00 05 00 0C 00 0D 00 00 00 04 00 01 00 0E 00 01
00 0F 00 10 00 01 00 0B 00 00 00 19 00 00 00 02
00 00 00 01 B1 00 00 00 01 00 0C 00 00 00 06 00
01 00 00 00 11 00 01 00 11 00 12 00 01 00 0B 00
00 00 19 00 00 00 01 00 00 00 01 B1 00 00 00 01
00 0C 00 00 00 06 00 01 00 00 00 16 00 01 00 13
00 14 00 01 00 0B 00 00 00 1D 00 01 00 01 00 00
00 05 2A B4 00 02 B0 00 00 00 01 00 0C 00 00 00
06 00 01 00 00 00 19 00 01 00 15 00 0A 00 01 00
0B 00 00 00 22 00 02 00 02 00 00 00 06 2A 2B B5
00 02 B1 00 00 00 01 00 0C 00 00 00 0A 00 02 00
00 00 1D 00 05 00 1E 00 01 00 16 00 00 00 02 00
17

这里的两个数字为一个字节,且都是10进制,首先名字为magic(魔数)的占四个字节,即 CA FE BA BE, 这个是标识这个文件为class文件的,不可能随便改个后缀名就是class文件了吧,读作: 咖啡baby(是不是和java的标志很像). 第二个是 minor_version(次版本), 占两个字节,即 0000, 表明这个class文件的此版本号(class 文件作为一个规范也是有版本号的),接下来是主版本号0034,转成16进制为52,即jdk8对应的版本号。
接下来是constant_pool(常量池)相关的知识点了,常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于 Java 层面的常量概念,如文本字符串、被声明为 final 的常量值等。而符号引用总结起来则包括了下面三类常量:

  • 类和接口的全限定名(即带有包名的 Class 名,如:org.lxh.test.TestClass)
  • 字段的名称和描述符(private、static 等描述符)
  • 方法的名称和描述符(private、static 等描述符)
    虚拟机在加载 Class 文件时才会进行动态连接,也就是说,Class 文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。

这里说明下符号引用和直接引用的区别与关联:

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

constant_pool 的类型 cp_info 的通用结构如下:

1
2
3
4
cp_info {
u1 tag;
u1 info[];
}

每个constant(常量)开头都是由1个字节的tag标识其属于哪种cp_info,下面是一个字节的info数组(其它信息,这里只是放在一个数组里面进行通用的描述)。在jdk8中,各种各样的constant_pool_tags如下表所示(共14种):

常量类型 值(10进制)
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

这里面就包括上面所说的两大类常量:

  • 字面量: Utf8,Integer,Float,Long,Double,String
  • 引用类型: Class,Fieldref,Methodref,InterfaceMethodref,NameAndType,MethodHandle,MethodType,InvokeDynamic

接下来我们解析一下常量池部分
constatn_pool_count(常量池数量),001F表明有30个常量([1,31)),接下来是constant_pool(常量池), 由cp_info组成,首先根据tag找对应的tag信息(1字节),开始是0A, 二进制的10(#1),找到对应的 CONSTANT_Methodref 类常量(表明这是个引用类型的常量),去官方文档找它的结构如下:

1
2
3
4
5
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

然后找class_index , 两个字节,0004,记住它引用的是4号常量(记作#4),继续,name_and_type_index, 0018, 即24,引用的是24号常量(#24)。
再看下一个常量(#2): 09,表明是一个CONSTANT_Fieldref 类型的引用类型的常量。对应的结构为

1
2
3
4
5
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

和method一样,得到他的引用变量为3号常量(#3)和25号常量(#25).继续这样寻找下去,直到找到 CONSTANT_Utf8 字面量类型的常量,,然后根据他的结构信息

1
2
3
4
5
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}

得到它的具体值,length表明的是接下来有几个字节数。下来就是根据这写10进制数找到对应的ascii表的字母了。 其实以上信息可以用jdk自带的javap命令帮你读取出来:

执行: javap -verbose Hello.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
28
29
30
31
32
33
34
35
36
37
38
39
40
PS C:\Users\liu\Desktop> javap -verbose .\Hello.class
Classfile /C:/Users/liu/Desktop/Hello.class
Last modified 2017-11-11; size 657 bytes
MD5 checksum 8ed8a940ffff9d0b7b27a59e1e90abe7
Compiled from "Hello.java"
public class Hello extends java.io.FileInputStream implements java.lang.Runnable,java.awt.event.ActionListener
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#24 // java/io/FileInputStream."<init>":(Ljava/lang/String;)V
#2 = Fieldref #3.#25 // Hello.name:Ljava/lang/String;
#3 = Class #26 // Hello
#4 = Class #27 // java/io/FileInputStream
#5 = Class #28 // java/lang/Runnable
#6 = Class #29 // java/awt/event/ActionListener
#7 = Utf8 name
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 <init>
#10 = Utf8 (Ljava/lang/String;)V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 Exceptions
#14 = Class #30 // java/io/FileNotFoundException
#15 = Utf8 actionPerformed
#16 = Utf8 (Ljava/awt/event/ActionEvent;)V
#17 = Utf8 run
#18 = Utf8 ()V
#19 = Utf8 getName
#20 = Utf8 ()Ljava/lang/String;
#21 = Utf8 setName
#22 = Utf8 SourceFile
#23 = Utf8 Hello.java
#24 = NameAndType #9:#10 // "<init>":(Ljava/lang/String;)V
#25 = NameAndType #7:#8 // name:Ljava/lang/String;
#26 = Utf8 Hello
#27 = Utf8 java/io/FileInputStream
#28 = Utf8 java/lang/Runnable
#29 = Utf8 java/awt/event/ActionListener
#30 = Utf8 java/io/FileNotFoundException

下面这些#号开头的都是我们的常量信息。
常量信息结束后开始解析我们的 access_flags (访问标记),用工具可以看出来我们这里的访问标记为0021,这个是啥意思呢,还是看官方文档给我们的信息

标识名 值(16进制)
ACC_PUBLIC 0x0001
ACC_FINAL 0x0010
ACC_SUPER 0x0020
ACC_INTERFACE 0x0200
ACC_ABSTRACT 0x0400
ACC_SYNTHETIC 0x1000
ACC_ANNOTATION 0x2000
ACC_ENUM 0x4000

对几个特殊的标识解释一下:

  • ACC_SUPER : 是否有超类(简单解释)
  • ACC_SYNTHETIC: 非源码中的类
  • ACC_ABSTRACT: 声明为抽象的,不可实例化的类。

这就是我们的类的开始, 这个0021 就是 0020 加上 0001 的结果。也就是说这个类是PUBLIC的而且有父类。
接下来是this_class,占了两个字节,为0003,指向的是3号常量,可以查看上面的常量信息,看到是 Hello 这个类。接下来是 super_class,两个字节,0004,是 java/io/FileInputStream这个类, 下来是interfaces_count,这是因为java是单继承多实现机制的,才会有的这个变量.是0002,表明共有两个接口类,接下来看interfaces,共有两个,分别是0005 和 0006, 代表的是5号和6号变量,Runable 和 ActionListener,
下面继续就是变量,方法,属性的语义部分了,和类差不多,有访问标记,名字和修饰符,不同的是属性就是传参的属性和些格式的问题,可以参考官方文档或者《java虚拟机规范(java se8版)》。其实我们的javap命令还有些输出上面没贴出来,这里加上注释,就能看出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
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
{
public Hello(java.lang.String) throws java.io.FileNotFoundException; #构造函数,从这里可以看出编译器会为我们的类生成一个默认的构造函数方法
descriptor: (Ljava/lang/String;)V # 这个就是属性了,括号里面就是属性类型,L开头就是引用类型,括号外面的V标识返回类型为void类型
flags: ACC_PUBLIC # 访问标记为Public类型
Code: # 这个Code标识方法
stack=2, locals=2, args_size=2 #stack表示方法的执行栈深度,最大为2, locals是局部变量表所需空间,用来存放方法内部的变量的,args_size为方法参数的数量,非static方法都会加上类的引用this参数,所以这里为2
0: aload_0 # 这些是具体的指令名
1: aload_1
2: invokespecial #1 // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
5: return
LineNumberTable:
line 11: 0
line 12: 5
Exceptions: # Exceptions标识方法抛出的异常
throws java.io.FileNotFoundException
public void actionPerformed(java.awt.event.ActionEvent);
descriptor: (Ljava/awt/event/ActionEvent;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 17: 0
public void run();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 22: 0
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 25: 0
public void setName(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field name:Ljava/lang/String;
5: return
LineNumberTable:
line 29: 0
line 30: 5
}

扩展:jvm解析系列][六]class里的常量池,访问标志,类的继承关系

7. 类加载机制

先来看类加载流程图

注意: 这里的加载和连接时并行进行的(加载一开始,连接也开始进行),和流程图画的不同。
再来细看每一步jvm做的事情:

7.1 加载

加载需做一下三件事:

  1. 通过一个类的全限定名来获取其定义的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

第一条中的二进制流并不只是从class文件中获取,共有以下几种获取方式:

  • class文件
  • 网络(Applet)
  • 计算生成(jdk的动态代理)
  • jar包
  • 其它文件生成(jsp等)

加载是通过类加载器来进行的,可以自定义类加载器(重写ClassLoader类的loadClass方法)来实现自定义类的加载过程。最终会将加载的二进制流储存进线程共享的方法区中,并且会在java堆中生成一个java.lang.Class对象,这样可以通过这个对象访问方法区中的数据。

7.2 验证

验证主要是验证Class文件中的字节流符合当前虚拟机的规范,主要包括一下四种

  • 文件格式验证(魔数)
  • 元数据验证(java数据类型)
  • 字节码验证(对代码进行校验,保证虚拟机安全运行)
  • 引用验证(看常量池中的各种引用符号进行匹配性校验)

7.3 准备

准备是正式的给类变量分配内存和初始值的一个阶段,所有属于类的变量(static)就分配到方法区里面,实例变量则会在对象实例化时随着对象一块分配在java堆中,同时根据不同类型的变量赋予不同的初始值。注意,这个时候并不会直接给变量赋予代码里的那个具体数值(常量除外)。还有几点:

  • 对于基本数据类型来说,类变量(static)和全局变量,如果没有给他显示的赋予值,虚拟机会为它赋予默认的初始值.如果是局部变量没有赋予初始值的话,就会报编译错误。
  • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
  • 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

7.4 解析

前面提到过符号引用这个概念,这个阶段就是将符号引用转为直接引用的过程。解析阶段的顺序不一定是在初始化之前,也有可能是在初始化之后,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行。

7.5 初始化

初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器\()方法的过程。
这里简单说明下\()方法的执行规则:

  1. \()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。
  2. \()方法与实例构造器\()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的\()方法执行之前,父类的\()方法已经执行完毕。因此,在虚拟机中第一个被执行的\()方法的类肯定是java.lang.Object。
  3. \()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成\()方法。
  4. 接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成\()方法。但是与类不同的是:执行接口的\()方法不需要先执行父接口的\()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的\()方法。
  5. 多线程情况下只会给类执行一遍\()方法.

7.6 类加载器与双亲委派模型

一张图说明类加载器的执行方式:

  • 启动类加载器:Bootstrap ClassLoader,跟上面相同。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
  • 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  • 自定义类加载器: 重写ClassLoader中的loadClass方法就可自定义实现。主要优点是高度的灵活性,可实现代码的热部署,class文件的解密等等。

下麦呢看一下官方实现的类加载器loadClass方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(var1)) {
Class var4 = this.findLoadedClass(var1); // 检查这个类是否被加载过
if (var4 == null) {
long var5 = System.nanoTime();
try {
if (this.parent != null) {
var4 = this.parent.loadClass(var1, false); // 递归调用父类加载器去尝试加载( 应用程序类加载器 ---> 扩展类加载器 ----> 启动类加载器 )
} else {
var4 = this.findBootstrapClassOrNull(var1);
}
} catch (ClassNotFoundException var10) {
;
}
if (var4 == null) { // 如果还是加载不成功,就自己去实现加载功能。
long var7 = System.nanoTime();
var4 = this.findClass(var1);
PerfCounter.getParentDelegationTime().addTime(var7 - var5);
PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
PerfCounter.getFindClasses().increment();
}
}
if (var2) {
this.resolveClass(var4);
}
return var4;
}
}

扩展:【深入Java虚拟机】之四:类加载机制

Read More

默认配图
Java

无状态RESTFul web服务实现

项目架构图:

这里只写我司的认证中心设计,先从认证流程图开始:

先从第二步开始(Oauth2的认证 /oauth/login 接口):

/oauth/login 资源接口

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
public Response<Map<String, Object>> login(String username,
String password, HttpServletRequest request,
HttpServletResponse response)
throws HttpRequestMethodNotSupportedException {
// 非空校验(前端也有部分校验)
Asserts.notEmpty(username);
Asserts.notEmpty(password);
// Oauth2的 密码授权模式
Map<String, String> map = Maps.newHashMap();
// 获取数据库配置的 clientId
String clientId = ${clientId};
map.put("username", username);
map.put("password", password);
map.put("grant_type", "password");
map.put("scope", "read write trust"); // 权限共有 read write trust 三种
// 构建客户端的Authentication
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
clientId, "", Lists.newArrayList());
// 调用Spring security Oauth2 的postAccessToken 生成 Oauth2AccessToken
ResponseEntity<OAuth2AccessToken> responseEntity = tokenEndpoint
.postAccessToken(token, map);
OAuth2AccessToken oAuth2AccessToken = responseEntity.getBody();
JwtAuthenticationToken authenticationToken = null;
if (oAuth2AccessToken != null) {
// 对生成的token包装成Jwt的实现, tokenProvider是自己写的一个接口,共有两个方法,下文会分析
authenticationToken = tokenProvider.createToken(request, response,
oAuth2AccessToken);
}
// 把一些不敏感的信息提取出来
Map<String, Object> oauthInfo = extract(authenticationToken);
// 存入redis
tokenRedisService.putOauthInfo(authenticationToken.getAccessToken(),oauthInfo);
return Response.ok(oauthInfo);
}

这里首先构建了一个UsernamePasswordAuthenticationToken类型的Authentication.然后调用TokenPoint的postAccessToken方法去创建一个Oauth2AccessToken, 接下来通过Oauth2AccessToken创建一个JwtAuthenticationToken, 在创建JwtAuthentiationToken的时候就会将access_token等信息写入客户端Cookie, 再从token里面取一些不敏感信息返回。

TokenEnpoint的PostAccessToken方法(Spring 官方实现)

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
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
}
if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
}
if (isAuthCodeRequest(parameters)) {
// The scope was requested or determined during the authorization step
if (!tokenRequest.getScope().isEmpty()) {
logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String> emptySet());
}
}
if (isRefreshTokenRequest(parameters)) {
// A refresh token has its own default scopes, so we should ignore any added by the factory here.
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
// 有五种类型的Granter,我司只实现了两种。
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}

接下来将第一步的实现。因为每个服务都是一个资源服务器,所以在资源服务器的配置文件里配置了对应的信息:

资源服务器的配置文件(省略部分信息):

1
2
3
4
5
6
7
8
9
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID)
.accessDeniedHandler(new ThrowAccessDeniedHandler(objectMapper))
.authenticationEntryPoint(new ThrowEntryPoint(objectMapper))
.stateless(true)
// 重点是这个配置,通过这个来实现
.tokenServices(new UAALoadBalancerUserInfoTokenServices(loadBalancerClient,resourceServerProperties.getServiceId(), resourceServerProperties.getUserInfoUri()));
}

这里是配置获取用户信息的地址(userInfoUrl).和官方的UserInfoTokenServices不同的是这里用的是从注册中心获取认证中心的实例地址,用到了Ribbon的LoadBalancerClient来根据serviceId获取认证中心的物理地址。核心的getMap方法实现如下:

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
protected Response getMap(String path, String accessToken) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Getting user info from: " + path);
}
try {
OAuth2RestOperations restTemplate = this.restTemplate;
if (restTemplate == null) {
BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
resource.setClientId(this.clientId);
restTemplate = new OAuth2RestTemplate(resource);
}
OAuth2AccessToken existingToken = ((OAuth2RestOperations)restTemplate).getOAuth2ClientContext().getAccessToken();
if (existingToken == null || !accessToken.equals(existingToken.getValue())) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(accessToken);
token.setTokenType(this.tokenType);
((OAuth2RestOperations)restTemplate).getOAuth2ClientContext().setAccessToken(token);
}
return (Response)((OAuth2RestOperations)restTemplate).getForEntity(path, Response.class, new Object[0]).getBody();
} catch (Exception var6) {
LOGGER.warn("Could not fetch user details: " + var6.getClass() + ", " + var6.getMessage());
return null;
}
}
}

和官方的实现不同就是返回值不同。接下来再看认证中心对应的接口实现(/oauth/me):

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
@RequestMapping(method = RequestMethod.GET, value ="/oauth/me")
public Map<String, Object> me(
@PathVariable(required = false) String service,
OAuth2Authentication auth) {
Map<String, Object> me = Collections.EMPTY_MAP;
//往用户信息里面添加权限信息
List<SimpleGrantedAuthorityImpl> authorities = new ArrayList<SimpleGrantedAuthorityImpl>();
logger.info(
"/oauth/me request... OAuth2Authentication[{}] service[{}]",
auth, service);
// 认证失败
if (auth == null) {
throw new OAuth2Exception("Authorization message is not null.");
}
OAuth2Request request = auth.getOAuth2Request();
if (request == null) {
me.clear();
throw new OAuth2Exception("OAuth2Request message is not null.");
}
Authentication authentication = auth.getUserAuthentication();
// 登录用户授权模式 password,authorization_code,refresh_token
if (authentication instanceof UsernamePasswordAuthenticationToken) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authentication;
UserDetails userInfo = userService
.selectByUserNameOrMobileOrEmail(usernamePasswordAuthenticationToken
.getName());
// 将密码清空
userInfo.setLoginPasswd("");
me.clear();
// 授权部分
//...
userInfo.setAuthorities(authorities);
me.put("name", userInfo.getLoginName());
// USERINFO_FIELDS_CACHE 常量是UserDetail的所有属性集合
for (Field field : USERINFO_FIELDS_CACHE) {
// 找寻所有的get方法
Method m = ReflectionUtils.findMethod(userInfo.getClass(),
SecurityUtils.getMethodName(field.getName()));
if (null != m) {
me.put(field.getName(),
ReflectionUtils.invokeMethod(m, userInfo));
}
}
return Response.ok(me);
}
// 如果不是这几种授权类型,抛出异常
me.clear();
throw new OAuth2Exception("Bad request.");
}

在认证中心创建token时对token还进行了部分操作,这里就不细说了。数据库表设计的话是Oauth2官方提供的那个Schema加上根据Rbac法则设计的业务表。这里看看流程即可。学的只是个思想。

Read More

默认配图
Java

Jenkins实现CI

目前公司初步涉及微服务架构,那么持续集成就比较重要了,采用的CI的流程如图:

流程说明:

公司的项目需要使用到windows服务器,所以初步就这样做了,从svn拉取最新的项目进行构建,构建完成之后调用脚本文件发送最新的项目war包至开发和测试的应用启动目录,再配置一个监控目录文件改动的脚本,进而实现启动最新的项目,但每次去服务器的jenkins控制台启动项目不方便,而且不是每个开发者都会jenkins,所以在控制jenkins上我又写了一个ps脚本,通过远程在公司内网调用jenkins实现项目的构建,这样一套流程下来,每个开发者提交了项目之后只需启动我给的脚本过一会就能看到上线的效果了.

具体实现流程:

  1. jenkins安装 进入: Jenkin下载页面 选择 Generic Java package(.war)
  2. jenkins配置(maven,jdk,svn路径) ,maven项目支持需安装 Maven Release Plug-in Plug-in 插件
  3. 创建maven项目,配置好就行(开始不需要填写构建后操作),立即构建会在jenkins安装目录下生成workspace和项目目录,我们需要的就是 workspace目录,
  4. 将项目构建成功后的ps脚本拷贝至workspace目录,在项目配置页里面配置调用脚本命令(cmd 调用ps脚本需要特殊配置,下文会讲解)
  5. 到这一步就完成了将最新的项目war包拷贝至开发,测试服务器的功能了,然后在启动远程调用的ps脚本,就可以通过调用jenkins api启动项目的构建了

脚本讲解:

  1. 首先是构建成功后的ps脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# ps version 2.0 scripts
# this script is used to copy project to $PROJECTS_HOME after it loaded by jenkins projects
#################### HOW TO USE IT ##########################
# first copy this script to $JENKINS_HOME (default= "C:\Users\Administrator\.jenkins")
# then in jenkins project config page ( url = "http://server:port/job/project/configure" it must be maven project) find Post Steps block
# choose " Execute Windows batch command " and write next code in the input area
### code in jenkins config page start #####
# @echo off
# cd ..
# powershell.exe -noprofile Set-ExecutionPolicy Unrestricted
# powershell.exe -file automate.ps1 -job %JOB_NAME%
### code end ###############################
# this script will auto close after execute over
param(
[string]$job=$(throw "Parameter missing: -job %JOB_NAME%")
)
$configFile="ps.json"
$targetDrive="D:"
$targetDir="project-dev"
# 运行中脚本的绝对路劲
$scriptPath=Split-Path -Parent $MyInvocation.MyCommand.Definition
if(!(Test-Path $configFile)){
throw "$configFile 配置文件不存在!"
}
# 初始化函数
function init{
#write-host "init 方法开始执行"
$targetPath=$targetDrive+"\"+$targetDir
if(!(Test-Path -path $targetPath)){
if(!(Test-Path -path $targetDrive)){
throw " $targetDrive 盘符不存在,请创建一个再重试。 "
}
New-Item -path $targetPath -type directory
}
return $targetPath
}
function main{
#write-host "main 方法开始执行"
$targetPath=init
write-debug "$targetPath"
Start-Sleep -Seconds 10
$modules=Get-Content $configFile -encoding utf8 | ConvertFrom-Json
foreach($module in $modules){
write-debug "$module.server : $module.port"
if($module.server -eq $job){
# stopserver
stopServer $module
copyFile $module $targetPath
}
}
}
function copyFile($module,$targetPath){
#Write-Debug "copyFile 方法开始执行"
if(($module -eq "") -or ($module -eq $null)){
throw "$module must not be null"
}
$date=get-date -format MMdd_HHmm
$bacName=$module.server+"_"+$date+"_back"+$module.suffix
Write-Host "当前脚本执行目录: $scriptPath"
$proFloder=$module.server
$copyFileName=$module.fullName+"-"+$module.version+$module.suffix
Write-Host "备份文件名: $bacName"
if(Test-Path -path $targetPath){
$copyFilePath=$targetPath+"\"+$proFloder
if(Test-Path -path $copyFilePath){
$fileName=$module.fullName+"-"+$module.version+$module.suffix
if(Test-Path -path $module.server){
$copy1=$copyFilePath+"\"+$fileName
$copy2=$copyFilePath+"\"+$bacName
Copy-Item $copy1 $copy2
}else{
Write-Debug "无需备份"
}
}else{
Write-Debug "目标文件夹不存在"
New-Item -type directory -Path $copyFilePath
}
cd $scriptPath
$loadProPath=""
# 公司项目结构不统一,加上这个判断获取文件路径
if($module.hasSecond){
$loadProPath=$proFloder+"\"+$module.fullName+"\target"
}else{
$loadProPath=$proFloder+"\target"
}
if(Test-Path -path $loadProPath){
cd $loadProPath
if(Test-Path -path $copyFileName){
Copy-Item $copyFileName $copyFilePath
}else{
write-host "待复制的文件不存在,请重新构建"
}
}
}else{
Write-Debug "$targetPath 未创建成功" #可能权限问题,没遇到过
}
}
# jenkins和开发服务器放在一起的做法,否则关闭功能单独放在一个脚本里实现
function stopServer($module){
#Write-Debug "stopServer 方法开始执行"
$port=$module.port
$content=(netstat -ano | findstr $port) #这里调用的是cmd语法,ps的没这么直接的功能
if(($content -eq "") -or ($content -eq $null)){
echo "当前端口 $port 没程序正在运行"
return
}
$content=$content.split("\n")[0]
$proPID=$content.split(" ")[-1]
if($proPID -match "[\d]+"){
Stop-Process -Id $proPID -Force
}else{
echo "进程id为: $proPID 的进程停止不了,请手动关闭后再试" #这种情况还没遇到过
}
}
main

main 函数为脚本的入口,write-debug 是测试的时候输出用的,write-host是正式运行时输出用的
由于直接在jenkins里面运行项目会导致关闭困难,所以我们就在外面运行了,上面实现了文件的复制(开发服务器 + 测试服务器),下面就来实现文件的监控,运行,关闭了。文件拷贝至服务器,我们需要监控文件夹的改变才能知道文件更新了,进而才能重启项目,实现更新

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
write-host "jar监听"
$floder="X:\project-test" # 测试服务器上的监控文件
#文件类型
$jar="*.jar"
#时间间隔
$timeout = 1 # 60s
$projects_path = "configure.txt"
$bat_path = "..\project-test\bats"
#创建监控对象
if(test-path "$floder"){
Write-Host "文件夹已存在,不需要创建!"
}else{
md $floder
}
#监控所有jar文件的改变
$jar_watch = new-object System.IO.FileSystemWatcher $floder,$jar
$jar_watch.IncludeSubDirectories = $true
Write-Host "Ctrl+C 退出文件夹的监控"
while($true){
$jar_result = $jar_watch.WaitForChanged('Changed',$timeout)
if ($jar_result.TimedOut -eq $false)
{
$lastof = $jar_result.Name.LastIndexOf("\")
#覆盖的项目名称
$jar_name = $jar_result.Name.Substring($lastof + 1)
#write-host $jar_name
#读取项目数组
$array_projects = Get-Content $projects_path
#write-host $array_projects.Contains($war_name)
foreach($pa in $array_projects) {
write-host $pa
if($jar_name.contains($pa)) {
$bat_name = "start"+$pa.Split(".")[0]+".bat"
write-host $jar_name
cd $bat_path
start $bat_name
}
}
}
}
write-host "取消监控"

这是jar类型文件的监控,只监控change事件的文件改变。然后先启动关闭脚本关闭项目,再启动启动脚本启动项目。启动和关闭就不贴了。上面有关闭的部分代码,启动的话就和普通命令一样了。接下来再看看远程api操作jenkins进行构建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
##################### HOW TO USE IT ########################
## 自动构建项目脚本,通过jenkins的远程api调用来实现的
## 构建的项目是根据文件名字而变化的,所以必须保证文件名的正确性(大小写敏感)
$user = "xx"
$password = "xx"
$pair = "$($user):$($password)"
$job = $MyInvocation.MyCommand.Name.Split('.')[0]
$url = "//serverurl/job/"+ $job + "/build"
$encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
$basicAuthValue = "Basic $encodedCreds"
$basicHeader = @{
Authorization = $basicAuthValue
}
Invoke-WebRequest -Uri http:$url -Method Post -Headers $basicHeader

这是通过设置headers来进行basic authentication的。/job/jobname/build/ 来进行执行构建请求。jobname是根据文件名来获取的,所以只需要一个文件,改个名字,其他微服务就都能跑了。

在有些系统里powershell脚本默认是记事本打开的,需要改变默认打开方式,在打开方式这里选择

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe 为默认打开方式,首次执行powershell脚本还需要在cmd窗口执行以下命令 powershell.exe -noprofile Set-ExecutionPolicy Unrestricted 开启执行权限

Read More

默认配图
返回顶部