Java动态代理

代理模式

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。

举一个例子:老师要批作业,可以找学习委员代收,学一委员整理好之后,再送到老师的办公室。这个场景中,学习委员就是一个代理。

##为什么要用代理模式

  • 中介隔离作用:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
  • 开闭原则,增加功能:代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。

静态代理

静态代理,就是采用代理模式思想,通过硬编码定义一个代理类,来实现代理的功能。由于静态代理灵活性较弱,在实际应用中,大多数采用动态代理模式。

动态代理模式

jdk动态代理

Jdk动态代理是基于接口的。

  • Step1 定义一个interface Hello
1
2
3
public interface Hello {
void sayHello(String str);
}
  • Step2 定义HelloImpl
1
2
3
4
5
6
7
8
9
10
11
12
public final class HelloImpl implements Hello {
private int[] array;

public HelloImpl() {
this.array = new int[1024*10];
}

@Override
public void sayHello(String str) {
System.out.println("hello " + str);
}
}
  • Step3 实现InvocationHandler的invoke方法
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
public class LogInvocationHandler implements InvocationHandler {
private Hello hello;

public LogInvocationHandler(Hello hello) {
this.hello = hello;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("sayHello".equals(method.getName())) {
System.out.println("You said: " + Arrays.toString(args));
}
return method.invoke(hello, args);
}


public static void main(String[] args) {
Object proxy = Proxy.newProxyInstance(LogInvocationHandler.class.getClassLoader(), // 1. 类加载器
new Class<?>[]{Hello.class}, // 2. 代理需要实现的接口,可以有多个
new LogInvocationHandler(new HelloImpl()));// 3. 方法调用的实际处理者

Hello hello = (Hello) proxy;
hello.sayHello("world");
System.out.println(hello);
}
}

但是Jdk动态代理使用上,有局限性。不能所有的类,都要实现一个interface,这会大大增加代码的冗余。如果使用继承的话,就能够解决这个问题。

CGLIB

cglib通过继承来实现动态代理,cglib的底层实现,是基于节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。如下是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
ClassFile { 
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

magic: 作为一个魔数,确定这个文件是否是一个能被虚拟机接受的class文件,值固定为0xCAFEBABE
minor_versionmajor_version:分别表示class文件的副,主版本号,不同版本的虚拟机实现支持的Class文件版本号不同。
constant_pool_count:常量池计数器,constant_pool_count的值等于常量池表中的成员数加1。
constant_pool:常量池,constant_pool是一种表结构,包含class文件结构及其子结构中引用的所有字符常量、类或接口名、字段名和其他常量。
access_flagsaccess_flags是一种访问标志,表示这个类或者接口的访问权限及属性,包括有ACC_PUBLICACC_FINALACC_SUPER等等。
this_class:类索引,指向常量池表中项的一个索引。
super_class:父类索引,这个值必须为0或者是对常量池中项的一个有效索引值,如果为0,表示这个class只能是Object类,只有它是唯一没有父类的类。
interfaces_count:接口计算器,表示当前类或者接口的直接父接口数量。
interfaces[]:接口表,里面的每个成员的值必须是一个对常量池表中项的一个有效索引值。
fields_count:字段计算器,表示当前class文件中fields表的成员个数,每个成员都是一个field_info
fields:字段表,每个成员都是一个完整的fields_info结构,表示当前类或接口中某个字段的完整描述,不包括父类或父接口的部分。
methods_count:方法计数器,表示当前class文件methos表的成员个数。
methods:方法表,每个成员都是一个完整的method_info结构,可以表示类或接口中定义的所有方法,包括实例方法,类方法,以及类或接口初始化方法。
attributes_count:属性表,其中是每一个attribute_info,包含以下这些属性,InnerClassesEnclosingMethodSyntheticSignatureAnnonation等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 动态代理不能是final类
* 如果是final类,会抛出异常:java.lang.IllegalArgumentException: Cannot subclass final class com.wsy.learn.jvm.autoproxy.cglib.HelloConcrete
*/
public class HelloConcrete {
/**
* 注意,此处如果定义的是final方法,则不会进入intercept方法进行方法增强
* @param str
* @return
*/
public final String sayHello(String str) {
return "HelloConcrete: " + str;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("You said: " + Arrays.toString(args));
return proxy.invokeSuper(obj, args);
}

public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloConcrete.class);
enhancer.setCallback(new MyMethodInterceptor());

HelloConcrete hello = (HelloConcrete)enhancer.create();
System.out.println(hello.sayHello("I love you!"));
}
}

动态代理最佳实践 - SpringAOP

关于什么是SpringAOP,在此不继续阐述。

springAOP既采用了jdk proxy,也采用了cglib。
spring会根据情况,来选择jdk动态代理还是cglib动态代理。

  • 如果要代理的对象实现了接口,则默认采用jdk proxy。
  • 如果要代理的对象实现了接口,也可以强制使用cglib。需要配置<aop:aspectj-autoproxy proxy-target-class=”true”/>
  • 如果要代理的对象没有实现接口,则采用cglib动态代理。
  • 但是注意,敲黑板!敲黑板!敲黑板!
    • 采用cglib的前提下,final类不能进行动态代理会抛出异常java.lang.IllegalArgumentException: Cannot subclass final class xxx(final类不能被继承,所以你懂得)。
    • 采用cglib的前提下,final方法也不能进行动态代理,不过不会抛出异常,而是直接跳过功能增强。
    • 采用jdk proxy,final类和final方法都能正常工作。

JVM内存区域

JVM内存区域总体介绍​

其中VM Stack(虚拟机栈),Native Method Stack(本地方法栈),Program Counter Register(程序计数器),是线程私有的。Method Area(方法区),Heap(堆)是线程共享的。

注意:没有特殊说明,本文中所讲的都是基于HotSpot虚拟机。

img

##程序计数器

程序计数器是线程私有的,是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型中(也仅是概念模型,各个虚拟机可能通过更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

多线程场景下,为了线程切换后能恢复到正确的位置,所以每个线程需要一个独立的程序计数器。

如果线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,这个计数器值则为空(Undefined)。次内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈

Java虚拟机栈,也是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编译器可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double)、对象引用(reference类型,它不等同于对象本身,可能是指向对象起始地址的引用指针,也可能是代表对象的句柄)。局部变量表所需的内存空间在编译期间完成分配,方法在帧中分配多大的空间,也是完全确定的。

这个区域存在两种异常情况:

  • 当栈的深度超过虚拟机所允许的深度的时候,抛出StackOverflowError。
  • 如果虚拟机栈允许动态扩展,如果无法申请到足够的内存,则会抛出OutOfMemoryError。

本地方法栈

本地方法栈的功能和Java虚拟机栈很接近,它们的区别是Java虚拟机栈是为虚拟机执行Java代码(字节码)服务,本地方法栈则为虚拟机使用到的Native方法服务。本地方法栈,也存在StackOverflowError和OutOfMemoryError。

Java堆

对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建。此内存区域的唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存。但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有对象都分配到堆上也变得不那么绝对了。

Java堆是垃圾收集器管理的主要区域,从内存回收角度来看,由于现在收集器基本都采用分代收集算法,Java堆中还分为了新生代和老年代。当堆无法继续扩展的时候,将会抛出OutOfMemoryError。

方法区

方法区(Method Area)也是线程共享区域,它用于存储被虚拟机加载的类元信息、常量、静态变量、及时编译器编译后的代码等数据。JDK8之前,虚拟机采用永久代实现方法区,类元信息、常量、静态变量等都存在堆中,和年轻代、老年代是连续的。

1
2
3
4
5
-XX:PermSize
方法区初始大小
-XX:MaxPermSize
方法区最大大小
超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen

Jdk8之后,官方废弃了永久代,在本地内存中开辟了一块空间,叫做元空间(MetaSpace),专门存储类的元数据信息。常量、静态变量存储到了堆上。

直接内存

很多人忽略了直接内存,举一个例子,JDK NIO中,Buffer调用的Native方法直接分配是堆外内存,也就是直接占用系统内存。所以在调整JVM参数的时候,要给系统留一些Buffer。避免内存动态扩展时出现问题。

ThreadLocal原理分析

ThreadLocal总体介绍

​ ThreadLocal类在并发编程中,每个线程实例,都有一份独立的副本,采用以空间换时间的方式,处理并发。

在Thread类中,ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocal.ThreadLocalMap就是线程变量的容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  static class ThreadLocalMap {

/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}

​ 如上代码可以看到Entry继承了WeakReference,在构造函数中super(key),key就是指ThreadLocal引用本身this。跟WeakHashMap不同的是,ThreadLocal的构造函数,没有指定ReferenceQueue。ThreadLocal用一种不同的方式,来避免内存泄漏,在下面的章节中,会详细介绍ThreadLocal的方式。

​ ThreadLocal的API,就不在此阐述,很简单。

每个线程独立备份

​ 为了达到这个目的,有两个方案。

## 方案A

 ThreadLocal 维护一个 Map,键是 Thread,值是它在该 Thread 内的实例。增加线程与减少线程均需要写 Map,故需保证该 Map 线程安全。线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存泄漏。
  • 结论:加锁势必会降低ThreadLocal的性能。猜测JDK为了性能考虑,没有采用此方案

方案B

Map 由 Thread 维护,从而使得每个 Thread 只访问自己的 Map,那就不存在多线程写的问题,也就不需要锁。该方案虽然没有锁的问题,但是由于每个线程访问某 ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLocal 变量与具体实例的映射,如果不删除这些引用(映射),则这些 ThreadLocal 不能被回收,可能会造成内存泄漏。
  • 结论:JDK采用了B方案,也有对应的方案来解决内存泄漏问题

#JDK如何解决内存泄漏问题

WeakHashMap原理分析

WeakHashMap总体介绍

​ WeakHashMap继承自AbstractMap,实现了Map接口,拥有了Map的基础功能。但是与常用的Map(比如HashMap)不同的是,WeakHashMap底层的存储单元Entry采用WeakReference,在GC的时候,会回收K,V。所以WeakHashMap天然比较适合用作缓存,即使K,V丢失,也不会对业务造成影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;

/**
* Creates new entry.
*/
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}

...
}

​ 如上代码可以看到Entry继承了WeakReference,在构造函数中super(key, queue)传入了key和ReferenceQueue。

​ 在调用get(),put(),size()等方法的时候,会执行一个私有方法expungeStaleEntries,如下所示。

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
private void expungeStaleEntries() {
//迭代弱引用队列中要被回收的对象
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
//操作链表
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
//敲黑板,这行代码很重要,如果不执行e.value = null,map的value会泄漏。
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}

#图解WeakHashMap原理

img

​ 图1

​ 如图1所示,key1,key2,key3虚引用指向Entry1,Entry2,Entry3,每个entry又分别指向v1,v2,v3。GC之后,key2虚引用被垃圾回收。通过执行expungeStaleEntries方法,使得Entry2变成游离态,Entry2也和v2脱离了引用关系。所以Entry2,v2都变成了可被垃圾回收的状态。

img

​ 图2

java引用详解

一般情况下,java开发中用的都是强引用。例如如下代码所示。

1
Object ref = new Object();

实际上在java中,还有一种不太常用的“弱引用”类型。弱引用中分为了软引用(SoftReference),弱引用(WeakReference),虚引用(PhantomReference)。所谓的强引用,其实就是FinalReference。

问题随之而来,为什么引用要分为强弱?弱引用又适用于何种场景?本文结合源码,带大家了解java引用的世界。

引用强弱之分

如果大家对JVM内存回收(GC)有一定了解,一定知道内存回收的前提是对象引用“不可达‘’,才能在回收阶段真正的释放掉对象占用的内存。强引用下,如果对象的引用一直处于可达状态,这块内存是没有办法回收的。在内存资源充沛的情况下还好,但是如果内存资源吃紧,为了服务的可用性,一些“不必要”,或者说“不那么重要的”内存,是不是可以释放掉?这个时候,弱引用就派上了用场。各个引用类的对比,请参考下标

引用类型 回收方式 必须配合ReferenceQueue 说明
FinalReference 不回收 默认情况下使用强引用
SoftReference 内存不充足情况下,GC之后回收,如果使用了queue同WeakReference一样的处理 非必要对象,为了内存空间
WeakReference GC立刻回收,如果使用了queue,需手动把要回收的对象置为null 非必要对象,为了内存空间
PhantomReference(幽灵引用) GC立刻回收 get默认返回null,使用虚引用的目的是通过queue监听对象回收

弱引用典型应用

WeakHashMap,ThreadLocal,JVM缓存实现。

源码分析

下图是java.lang.ref包下的类图,四种引用全部继承自Reference。

img

JDK把引用分为了四个状态。

状态 判定条件 说明 源码注释
Active (queue==ReferenceQueue\ \ ReferenceQueue.NULL) && next == null 新创建的引用对象是这个状态。在GC检测到引用对象已经到达合适的Reachability(可达性)时,GC会根据引用对象在创建时是否指定ReferenceQueue参数进行状态转移,如果指定则转移到pending,否则直接转移到Inactive。 Active: Subject to special treatment by the garbage collector. Some time after the collector detects that the reachability of the referent has changed to the appropriate state, it changes the instance’s state to either Pending or Inactive, depending upon whether or not the instance was registered with a queue when it was created. In the former case it also adds the instance to the pending-Reference list. Newly-created instances are Active.
Pending queue == RefrenceQueue && next == this (jvm设置) pending-Refence链表中引用都是这个状态,它们等着被内部线程ReferenceHandler处理入队(调用RefenceQueue.enqueue方法),没有注册的实例不会进入此状态。 An element of the pending-Reference list, waiting to be enqueued by the Reference-handler thread. Unregistered instances are never in this state.
Enqueued queue == ReferenceQueue.ENQUEUED && next == 下一个要处理的Reference对象,或者链表中最后一个next == this 相应的对象已经为待回收并放到queue中,准备由外部线程来询问queue获取相应的数据。调用ReferenceQueue.enqued方法后的Reference对象处于这个状态,当Reference实例从队列中移除之后,它的状态改为Inactive,没有注册的实例不会进入该状态。 Enqueued: An element of the queue with which the instance was registered when it was created. When an instance is removed from its ReferenceQueue, it is made Inactive. Unregistered instances are never in this state.
Inactive queue == ReferenceQueue.NULL && next == this 即此Reference对象已由外部queue中获取到,并且已经处理掉了。即意味着此对象是可以被回收的,并且对内部封装的对象也可以被回收掉了(具体的回收运行,取决于Clear动作是否被调用,可以理解为进入此状态的Reference对象是应该被回收掉的)一旦Reference对象变成Inactive,它的状态就不会再变化。 Nothing more to do. Once an instance becomes Inactive its state will never change again.

基于上表的讲解,画出了如下引用状态流程图。

img

java类加载之ClassLoader

何为类加载

大家知道Java是一门编译型语言,源代码文件是以.java结尾的。通过编译器编译之后,会形成一个字节码文件,也就是class文件。在一个应用中,不同的class文件中封装着不同的功能,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

类加载器

BootstrapClassLoader:称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:

1
2
3
4
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}

如下是BootstrapClassLoader加载的类:

file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/classes

ExtClassLoader

称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。

AppClassLoader

称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。可以说应用程序自己定义的类,都是由AppClassLoader负责加载。

CustomClassLoader

除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。

ClassLoader加载类原理

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

##为何需要双亲委派

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

JDK如何判定Class相同

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。

如何定义CustomClassLoader

如下代码,展示了本地目录的类加载器。

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 class FileSystemClassLoader extends ClassLoader {

private String rootDir;

public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}

private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

private String classNameToPath(String className) {
return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
}
}

ElasticSearch Scroll

ElasticSearch Scroll

##深分页

比如pageIndex=10,pageSize=20,{“start”: 180, “size”: 20},相当于每个分片召回了200条数据,排序之后截断前20条返回。但是如果pageIndex=1000,每个分片需要召回20000条数据,排序,最后仍然是取前20条,扔掉19980条。深分页的实现方式代价是很大的。

Scroll

为了解决以上深分页,在页码太大情况下性能的问题,ES提供了Scroll方式。

scroll 查询 可以用来对 Elasticsearch 有效地执行大批量的文档查询,而又不用付出深度分页那种代价。

游标查询允许我们 先做查询初始化,然后再批量地拉取结果。 这有点儿像传统数据库中的 cursor 。

游标查询会取某个时间点的快照数据。 查询初始化之后索引上的任何变化会被它忽略。 它通过保存旧的数据文件来实现这个特性,结果就像保留初始化时的索引 视图 一样。

深度分页的代价根源是结果集全局排序,如果去掉全局排序的特性的话查询结果的成本就会很低。 游标查询用字段 _doc 来排序。 这个指令让 Elasticsearch 仅仅从还有结果的分片返回下一批结果。

启用游标查询可以通过在查询的时候设置参数 scroll 的值为我们期望的游标查询的过期时间。 游标查询的过期时间会在每次做查询的时候刷新,所以这个时间只需要足够处理当前批的结果就可以了,而不是处理查询结果的所有文档的所需时间。 这个过期时间的参数很重要,因为保持这个游标查询窗口需要消耗资源,所以我们期望如果不再需要维护这种资源就该早点儿释放掉。 设置这个超时能够让 Elasticsearch 在稍后空闲的时候自动释放这部分资源。

例如:依据查询条件开启滚动,镜像有效期为5分钟。这个查询结果中包含scroll_id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
curl -XPOST 'http://localhost:9200/es_cluster/goods_index/goods/_search?scroll=5m' -d '{
"size": 20,
"query": {
"bool": {
"filter": [
{
"exists": {
"field": "skuUpTime"
}
}
]
}
}
}'

下次查询的时候,就可以直接用scroll_id来查询了

1
2
3
4
curl -XPOST 'http://localhost:9200/es_cluster/scroll' -d '{
"scroll": "5m",
"scroll_id": "DnF1ZXJ5VGhlbkZldGNoJgAAAAA8lsqeFjRwN19JcDBKVFd1cy04dkZucUZ4dHcAAAAAPlQH_BZIMUU1VFA2dVIxMk10M0xoWW1sSEtRAAAAADtYOdcWUVBlYXZCNmRTSEtXV2llZTE4dkEwZwAAAAA6dfbHFkdEMS1VY21CVHFhSFpFS1ZrVnBwSVEAAAAAPaL6vBZsZmRFeHRBeFFJaWlVOXNWV2xnZlB3AAAAADy6UDUWRU9NVVRadG5STnV4cThrb2VMdHhqUQAAAAA9V-f6FnNYSV83SHZVVHpDTHVLZWFiNmVwcVEAAAAAPMlOFhY5Zk5ESlBZdFI5bVVEclI4b2paTld3AAAAADn6zjwWZEJ5RU9VOThTUW1HUnB5UzlOT18xUQAAAAA9EBaGFmM4Ykh6SHkxU3JPYkFFRFZMWVRiQUEAAAAAPSKSJxZVUlRzY0luTFJTQzQ3WTZydUxoQVN3AAAAADyw6E4WS21DZHdWX21TRXlkdzVvYUpRcVBKUQAAAAA7sX3BFnh4UC1rMGhDU1FlZFFSdjVDVVc0d3cAAAAAPMIY9RZla0lLajhFQlMtYUVOZDE1U0tXekRnAAAAADtYOdgWUVBlYXZCNmRTSEtXV2llZTE4dkEwZwAAAAA5-s49FmRCeUVPVTk4U1FtR1JweVM5Tk9fMVEAAAAAPMlOFxY5Zk5ESlBZdFI5bVVEclI4b2paTld3AAAAADzjiUkWMll6VHRaUDRUV2VjNXA4LUxfMlFxdwAAAAA8cKq6FndTQ2JfOWlWUXlHNHdNMUZ3ejZVZUEAAAAAPHCquRZ3U0NiXzlpVlF5RzR3TTFGd3o2VWVBAAAAADzx948Wdlk0dzdJWnBRWHEyRWFsVEhsVUdiQQAAAAA9cxFHFnpwdy1EaXFXUmR5REM5ZW5CYTB0ZEEAAAAAPXMRSBZ6cHctRGlxV1JkeURDOWVuQmEwdGRBAAAAAD1DIxMWM0E3dW1PbkVUQzZMSUJXV21LaHNiZwAAAAA9QyMUFjNBN3VtT25FVEM2TElCV1dtS2hzYmcAAAAAPPeZ9hZyY0luUVRFb1RsNmF1YWhZaVEwWUFBAAAAADz3mfcWcmNJblFURW9UbDZhdWFoWWlRMFlBQQAAAAA6pq_PFjRDZjN6XzBSU25DRE5PWlA2RFRUR2cAAAAAOqavzhY0Q2Yzel8wUlNuQ0ROT1pQNkRUVEdnAAAAADzcYCcWQ2xwZk15U01RNk9oelRQR2w2bTRmdwAAAAA84x3vFk8wYjdmdV9aU2E2QzREc045bUx4UUEAAAAAPPTj5RZoYlctclkyblM3cXpDeEZTa05FNlNnAAAAADzrVQEWLU9xeUFOVHRUQTZtd2R2eG5MTF96UQAAAAA861UAFi1PcXlBTlR0VEE2bXdkdnhuTExfelEAAAAAO1D_vBZheEpkUFp4UFNXS19BWDdvMTNkS1BnAAAAADuxfcIWeHhQLWswaENTUWVkUVJ2NUNVVzR3dwAAAAA7ungNFlpqWVViMnZsVDU2LXg4VzBlTlZ0cWcAAAAAPaL6vRZsZmRFeHRBeFFJaWlVOXNWV2xnZlB3"
}'

Spring初始化源码分析

spring配置文件,需要关注如下两个类。

1
2
org.springframework.beans.factory.config.PropertiesFactoryBean
org.springframework.context.support.PropertySourcesPlaceholderConfigurer

img

​ PropertiesFactoryBean类图

img

​ PropertySourcesPlaceholderConfigurer

如下是配置方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--使用PropertySourcesPlaceholderConfigurer的配置方式-->
<context:property-placeholder location="classpath*:config/*.properties"/>
<!--使用PropertiesFactoryBean的配置方式-->
<util:properties id="appConfig1" location="classpath*:config/*.properties"/>
<!--使用PropertySourcesPlaceholderConfigurer的配置方式-->
<bean id="spc" class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:config/data.properties</value>
<value>classpath:config/data1.properties</value>
</list>
</property>
<property name="ignoreResourceNotFound" value="false"/>
</bean>
<!--使用PropertiesFactoryBean的配置方式-->
<bean id="appConfig" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="locations">
<list>
<value>classpath:config/data.properties</value>
<value>classpath:config/data1.properties</value>
</list>
</property>
</bean>

​ 使用方式上,用@Value注解

PropertiesFactoryBean用#{}的方式

PropertySourcesPlaceholderConfigurer用${}的方式

1
2
3
4
5
6
7
8
9
10
@Value(value = "${driveClass}")
private String driveClass;
@Value("${url}")
private String url;

@Value("#{appConfig['userName']}")
private String userName;

@Value("#{appConfig['config1']}")
private String password;

Mac环境下搭建大数据开发环境

Mac下搭建大数据开发环境

环境比较浪费时间,结合自己搭建环境的经验,整理成文档。

安装JDK 和 SCALA

所有的前置条件是安装JDK,我用的是JDK8.x

java version “1.8.0_162”

Java(TM) SE Runtime Environment (build 1.8.0_162-b12)

Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode)

Scala code runner version 2.12.4 – Copyright 2002-2017, LAMP/EPFL and Lightbend, Inc.

Spark V2.3.0

img

Spark安装比较简单,也没什么坑。网上一大堆,随意搞。

vmvare虚拟机

安装vmware fusion

虚拟下安装Hadoop & hive

1、hadoop伪分布式部署,版本 2.7.1, sbin/start-all.sh , sbin/stop-all.sh。jps验证其服务是否启动完全

2、hive用mysql(我用的是mariaDB,比安装Mysql省事)做元数据库,版本2.1.1 hive。hive-env.sh,hive-site.xml hdfs dfs -ls /user/hive/warehouse hive表都存到这个目录下

4、关闭防火墙 永久关闭: chkconfig iptables off 及时生效service iptables stop

5、mac 下 虚拟机vmware Fution

6、ssh -p 22 root@172.16.192.130 ssh链接虚拟机

core-site.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<configuration>
<property>
<name>hadoop.tmp.dir</name>
<value>/soft/data/hadoop/tmp</value>
<description>Abase for other temporary directories.</description>
</property>
<property>
<name>fs.defaultFS</name>
<value>hdfs://172.16.192.130:9000</value>
</property>
<property>
<name>hadoop.native.lib</name>
<value>false</value>
<description>Should native hadoop libraries, if present, be used. </description>
</property>
</configuration>

hdfs-site.xml

1
2
3
4
5
6
7
8
9
10
<configuration>
<property>
<name>dfs.replication</name>
<value>1</value>
</property>
<property>
<name>dfs.permissions</name>
<value>false</value>
</property>
</configuration>

启动方式:

$HADOOP_HOME/sbin/start-all

3825 Jps

3089 DataNode

3252 SecondaryNameNode

2903 NameNode

3405 ResourceManager

3695 NodeManager

虚拟机下安装Mysql,当做Hive的元数据库

service mysql status 查看mysql是否启动

service mysql start 启动mysql服务

mysql用的mariaDB,yum安装,搜索“centOS 6 yum安装 mariaDB”。mySQL 启动:service mysql start,service mysql status

查看HIVE元数据:

mysql -uroot -p 访问mysql

use hive;

show tables;

虚拟机下安装Hbase

1、 进入hbase官网,下载hbase1.2.1,因为本地的hadoop版本是2.7.1,要注意hbase和hadoop的兼容性。

2、 下载之后,tar -zxvf hbase-1.2.1-bin.tar.gz

3、 修改hbase conf目录下hbase-site.xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<configuration>
<property>
<name>hbase.rootdir</name>
<value>hdfs://192.168.2.100:9000/hbase</value>
注意别忘了端口号,URL要和hadoop core-site.xml保持一致
</property>
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<property>
<name>hbase.zookeeper.property.dataDir</name>
<value>/soft/zkdata</value>
</property>注意/soft/zkdata要有读写权限
</configuration>

4、 修改conf 下的 hbase-env.sh

export JAVA_HOME=/soft/jdk1.8.0_171

export HBASE_CLASSPATH=$HADOOP_HOME/conf

export HBASE_MANAGES_ZK=true 这个值默认是true的,作用是让HBase启动的时候同时也启动zookeeper.

5、 启动hbase ./start-hbase.sh,执行JPS会看到如下进程。

[root@localhost bin]# jps

20979 DataNode

21315 ResourceManager

22180 HRegionServer

21605 NodeManager

21158 SecondaryNameNode

21961 HQuorumPeer

23116 Jps

22060 HMaster

20845 NameNode

6、 启动Hbase shell, bin/hbase shell,执行命令测试下Hbase shell能否正常工作。

hbase(main):001:0> list “test”

TABLE

test

1 row(s) in 0.3870 seconds

=> [“test”]

遇到的雷

本地SPARK连HIVE,需要把虚拟机中 hive_home/conf/hive-site.xml 复制到spark_home/conf下一份,并且需要把mysql驱动放到$spark_home/jars中

需要把虚拟机中的MYSQL (hive元数据库)权限调成任何IP都能访问。GRANT ALL PRIVILEGES ON . TO ‘root‘@’%’ IDENTIFIED BY ‘123456’; 执行flush privileges;刷新权限。

如果遇到这个错误。 Hive Schema version 1.2.0 does not match metastore’s schema version 2.1.0 Metastore is not upgraded or corrupt。 需要把hive-site.xml设置成hive.metastore.schema.verification = false

hive.exec.local.scratchdir,hive.querylog.location,hive.server2.logging.operation.log.location 都配置成 /tmp/root。之前放到apache-hive目录下,会报错。

ElasticSearch-hadoop 6.2.3 匹配es 6.x ;ElasticSearch-hadoop 5.2.3 匹配 es 5.x

mac 改成静态IP https://blog.csdn.net/zhishengqianjun/article/details/77046796

本地调用Hbase API的时候,需要执行以下三个步骤,不然就会找不到regionServer反复重试,照成阻塞。

1、 配置Linux的hostname :vim /etc/sysconfig/network

NETWORKING=yes

HOSTNAME=master

2、 配置Linux的hosts,映射ip的hostname的关系

127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4

::1 localhost localhost.localdomain localhost6 localhost6.localdomain6

193.168.2.100 master

3、配置访问windows的hosts,192.168.2.100 master

mac下安装ElasticSearch

特别简单,自行google

brew services restart elasticsearch

brew services restart kibana

elasticsearch –version 观察es版本

安装目录: /usr/local/Cellar/elasticsearch/6.2.4

brew info elasticsearch 可以查看elasticsearch的详情。很实用的命令。

Mac下vmware 静态IP

虚拟机默认是动态分配IP,一旦IP变更,大数据的环境会有问题。静态IP是必须要解决的问题。

https://blog.csdn.net/zhishengqianjun/article/details/77046796

照着这个帖子,一步一步来。稳稳的。

Idea中执行spark sql

img

需要把spark_home/conf下的hive-site.xml复制到resource目录下。

另外,如果pom中没有mysql jar包的话,需要加入。

将hdfs 文件关联到hive上

1、 首先在虚拟机hive上,创建好表结构

2、 登录线上的hive,找到hive对应的HDFS文件,copy一个part下来。

3、 传到虚拟机的hdfs上

4、执行ALTER TABLE xxx ADD PARTITION (dt=’2013-02-28’)
LOCATION ‘/wsy’; (如果是lzo压缩过的文件,执行lzop -dv 001008_0.lzo 解压缩,不然hive读的时候会乱码)

5、 大功告成。

Spring初始化源码分析

本文主要介绍下Spring的初始化过程。

img

​ XML方式Spring应用入口

首先,在web容器启动之后,会加载org.springframework.web.servlet.DispatcherServlet,作为容器的一个Servlet实例,该Servlet也是Spring WEB应用的唯一一个Servlet实例。Servlet初始化,会调用org.springframework.web.servlet.HttpServletBean#init方法,初始化Servlet,见下面源码。

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
/**
* Map config parameters onto bean properties of this servlet, and
* invoke subclass initialization.
* @throws ServletException if bean properties are invalid (or required
* properties are missing), or if subclass initialization fails.
*/
@Override
public final void init() throws ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet '" + getServletName() + "'");
}
// Set bean properties from init parameters.
try {
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
throw ex;
}
// Let subclasses do whatever initialization they like.
initServletBean();
if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}

注意代码initServletBean,该方法被FrameworkServlet重写。如下代码。

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
/**

- Overridden method of {@link HttpServletBean}, invoked after any bean properties
- have been set. Creates this servlet's WebApplicationContext.
*/
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
}
long startTime = System.currentTimeMillis();

try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
catch (RuntimeException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}

if (this.logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
elapsedTime + " ms");
}
}​

initWebApplicationContext()方法,最终会调用createWebApplicationContext()方法,来初始化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
protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
Class<?> contextClass = getContextClass();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Servlet with name '" + getServletName() +
"' will try to create custom WebApplicationContext context of class '" +
contextClass.getName() + "'" + ", using parent context [" + parent + "]");
}
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(
"Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

wac.setEnvironment(getEnvironment());
wac.setParent(parent);
wac.setConfigLocation(getContextConfigLocation());

configureAndRefreshWebApplicationContext(wac);

return wac;
}

img

​ WEB容器的方式Spring应用入口

img

​ WEB容器的方式Spring Context初始化
最后引出最重要的代码,org.springframework.context.support.AbstractApplicationContext#refresh

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 void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
logger.warn("Exception encountered during context initialization - cancelling refresh attempt", ex);
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
}
}

这个代码的作用,是初始化整个容器,创建Bean.