java高级

类的加载

当程序需要使用某个类时,如果该类还未被加载到内存,则系统会通过加载,连接,初始化三个步骤类对这个类进行初始化

  • 加载:就是指将class文件读入内存,并为之创建一个Class对象,任何类被使用时系统都会建立一个对应的Class对象
  • 连接:
    • 验证:是否有正确的内部结构,并和其他类协调一致
    • 准备:负责为类的静态成员分配内存,并设置默认初始化值
    • 解析:将类的二进制数据中的符号引用转化为直接引用
  • 初始化:开辟空间等初始化动作

类的加载时机

  • 创建类的实例
  • 访问类的静态变量,或者为静态变量赋值
  • 调用类的静态方法
  • 使用反射来强制创建某个类或接口对应的java.lang.Class对象
  • 初始化某个类的子类(会先初始化父类)
  • 直接使用java.exe命令来运行某个主类

类加载器

负责将class文件加载到内存中,并为之生成对应的Class对象

反射

  • 反射是框架的灵魂,java的反射机制支持我们在运行时可以访问到任意一个类的成员方法(Method),成员变量(Field)和构造函数(Constructor)

  • 获取类的Class对象的3种方式

    1
    2
    3
    4
    5
    6
    Class c1 = Demo.class;
    这说明任何一个类都有一个隐含的静态成员变量class,这种方式是通过获取类的静态成员变量得到的
    Class c2 = new Demo().getClass();
    这种方式是通过一个类的对象的getClass()方法获得的
    Class c3 = Class.forName("com.ym.Demo");
    这种方法是Class类调用forName方法,通过一个类的全量限定名获得
  • 通过Class对象获取所有public方法,两个参数分别是方法名和方法参数类的类类型列表

    1
    2
    public Method getDeclaredMethod(String name, Class<?>... parameterTypes) 
    public Method getMethod(String name, Class<?>... parameterTypes) // 得到该类所有的public方法,包括父类的
    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
    public class Person {
    private String name;
    private int age;
    private String msg="hello wrold";
    public String getName() {
    return name;
    }
    public void setName(String name) {
    this.name = name;
    }
    public int getAge() {
    return age;
    }
    public void setAge(int age) {
    this.age = age;
    }
    public Person() {
    }
    private Person(String name) {
    this.name = name;
    System.out.println(name);
    }
    //重载
    public void fun() {
    System.out.println("fun");
    }
    public void fun(String name,int age) {
    System.out.println("我叫"+name+",今年"+age+"岁");
    }
    }

    public class ReflectDemo {
    public static void main(String[] args){
    try {
    Class c = Class.forName("com.tengj.reflect.Person");
    Object o = c.newInstance();
    //string的类类型,int的类类型
    Method method = c.getMethod("fun", String.class, int.class);
    method.invoke(o, "tengj", 10);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }
  • 获取变量,参数是成员变量的名字。

    1
    2
    public Field getDeclaredField(String name) // 获得该类自身声明的所有变量,不包括其父类的变量
    public Field getField(String name) // 获得该类自所有的public成员变量,包括其父类变量
    1
    2
    3
    4
    5
    Field field = c.getDeclaredField("msg");
    // 私有变量,需手动设置可访问,公有变量不需要
    field.setAccessible(true);
    Object msg = field.get(o);
    System.out.println(msg);
  • 获取构造函数,这个参数为构造函数参数类的类类型列表

    1
    2
    public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) //  获得该类所有的构造器,不包括其父类的构造器
    public Constructor<T> getConstructor(Class<?>... parameterTypes) // 获得该类所以public构造器,包括父类
1
2
3
4
	Constructor constructor = c.getDeclaredConstructor(String.class);
// 私有的需要手动设置可访问
constructor.setAccessible(true);
constructor.newInstance("hahah");
  • 注意:Class的newInstance方法,只能创建只包含无参数的构造函数的类,如果某类只有带参数的构造函数,那么就要使用另外一种方式:

    1
    fromClass.getDeclaredConstructor(String.class).newInstance(“hahah”);

通过反射越过泛型检查

  • Java中集合的泛型,是防止错误输入的,只在编译阶段有效,绕过编译到了运行期就无效了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("hello");
    // list.add(2);报错,因为设置了泛型只能添加String
    System.out.println(list.size());

    Class c = list.getClass();

    try {
    Object o = c.newInstance();
    Method method = c.getDeclaredMethod("add", Object.class);
    // 不会报错了
    // method.invoke(o, 2);//这里是新创建一个对象操作
    method.invoke(list, 2);//这里是接着原来的对象操作
    System.out.println(list.size());

    } catch (Exception e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }

    }

日期

Date

构造方法 :

1
2
3
4
5
6
7
public Date() {
this(System.currentTimeMillis());
}
//表示毫秒数的long
public Date(long date) {
fastTime = date;
}

Date(long date),Date()

由于SimpleDateFormat是线程不安全的类,在并发时容易出问题,所以可以用ThreadLocal,ThreadLocal可以确保每个线程都可以得到单独的一个SimpleDateFormat的对象,那么自然也就不存在竞争问题了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {

protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};

public static Date parse(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}

public static String format(Date date) {
return threadLocal.get().format(date);
}

或者每次使用时再分别创建SimpleDateFormat类(不推荐)

LocalDate

不包含小时,分,秒,只有日期

1
2
3
4
//这样就会报错
System.out.println(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
//这样不会报错
System.out.println(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));

LocalDateTime

包含小时,分,秒

1
2
//这样不会报错,能将时间格式化到秒       
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

反射和代理

泛型

Lambda

函数式接口

有且只有一个抽象方法的接口,在java8中,可以在接口中定义static方法并实现,而且其实现类不会继承这个方法,也可以在接口中定义default方法,为了在现有的类库中中新增功能而不影响他们的实现类,实现类直接调用即可,而且还可以重写,重写时不需要加default关键字,可以有多个static方法和default方法

Lambda表达式

(参数列表)->(lambda体)

  • 其中参数列表为可选项,即没有参数时不需要传,而且就算有参数也不需要传值类型,编译器会自动识别推断类型

    1
    () -> System.out.println("Thread run()");//不需要参数
  • 可选的参数圆括号一个参数无需定义圆括号,但多个参数需要定义圆括号。例如:

    1
    2
    s -> System.out.println(s) ; //一个参数不需要添加圆括号。
    (x, y) -> Integer.compare(y, x) ;//两个参数添加了圆括号,否则编译器报错。
  • 可选的大括号: 如果主体包含了一个语句,就不需要使用大括号。

    1
    2
    s -> System.out.println(s) ;//不需要大括号.
    (s) -> { if (s.equals("s")){ System.out.println(s); } };// 需要大括号
  • 可选的返回关键字: 如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

Lambda体不加{ }就不用写return:

1
Comparator<Integer> com = (x, y) -> Integer.compare(y, x);

Lambda体加上{ }就需要添加return:

1
2
3
4
Comparator<Integer> com = (x, y) -> {
int compare = Integer.compare(y, x);
return compare;
};

方法引用::

简单来讲,就是构造一个该方法的闭包。类名::方法名

这种式子一般是用作Lambda表达式,Lambda有所谓懒加载嘛,不要括号就是说,看情况调用方法。

这种[方法引用]或者说[双冒号运算]对应的参数类型是Function<T,R> T表示传入类型,R表示返回类型。比如:

1
2
3
4
//lambda表达式
person -> person.getName(); () -> new HashMap<>();
//可以替换为
Person::getName HashMap::new
1
2
3
4
5
6
7
8
9
10
11
public class test {
public static void main(String[] args) {
List<String> collected = new ArrayList<>();
collected.add("alpha");
collected.add("beta");
//collected = collected.stream().map(string -> string.toUpperCase()).collect(Collectors.toList());
//map()需要接受一个Function类型
collected = collected.stream().map(String::toUpperCase).collect(Collectors.toList());
System.out.println(collected);
}
}

Stream

Stream它并不是一个容器,它只是对容器的功能进行了增强,添加了很多便利的操作,例如查找、过滤、分组、排序等一系列的操作。并且有串行、并行两种执行模式,并行模式充分的利用了多核处理器的优势,使用fork/join框架进行了任务拆分,同时提高了执行速度。简而言之,Stream就是提供了一种高效且易于使用的处理数据的方式。

  • Stream自己不会存储元素。
  • Stream的操作不会改变源对象。相反,他们会返回一个持有结果的新Stream。
  • Stream 操作是延迟执行的。它会等到需要结果的时候才执行。也就是执行终端操作的时候。

使用

分为三个步骤,第一步是创建Stream,从集合、数组中获取一个流,第二步是中间操作链,对数据进行处理。第三步是终端操作,用来执行中间操作链,返回结果。

创建stream

  • 使用集合:Java8 中的 Collection 接口被扩展,提供了两个获取流的方法,这两个方法是default方法,也就是说所有实现Collection接口的接口都不需要实现就可以直接使用:

    1. default Stream stream() : 返回一个顺序流。
    2. default Stream parallelStream() : 返回一个并行流。
    1
    2
    3
    4
    5
    List<Integer> integerList = new ArrayList<>();
    integerList.add(1);
    integerList.add(2);
    Stream<Integer> stream = integerList.stream();
    Stream<Integer> stream1 = integerList.parallelStream();
  • 由数组创建: Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:

    • static Stream stream(T[] array): 返回一个流
    • 重载形式,能够处理对应基本类型的数组:
    1
    2
    3
    4
    5
    6
    public static IntStream stream(int[] array)
    public static LongStream stream(long[] array)
    public static DoubleStream stream(double[] array)

    int[] intArray = {1,2,3};
    IntStream stream = Arrays.stream(intArray);

stream中间操作

如果Stream只有中间操作是不会执行的,当执行终端操作的时候才会执行中间操作,这种方式称为延迟加载或惰性求值。多个中间操作组成一个中间操作链,只有当执行终端操作的时候才会执行一遍中间操作链。每一个中间操作的结果都是stream

  • map(Function<? super T, ? extends R> mapper): 接收一个Function函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。也就是转换操作,map还有三个应用于具体类型方法,分别是:mapToInt,mapToLong和mapToDouble。这三个方法也比较好理解,比如mapToInt就是把原始Stream转换成一个新的Stream,这个新生成的Stream中的元素都是int类型。这三个方法可以免除自动装箱/拆箱的额外消耗。
  • distinct(): 去重,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
  • filter(Predicate<? super T> predicate): Predicate函数在上一篇当中我们已经讲过,它是断言型接口,所以filter方法中是接收一个和Predicate函数对应Lambda表达式,返回一个布尔值,从流中过滤某些元素。
  • sorted(Comparator<? super T> comparator): 指定比较规则进行排序。倒排需要用到reversed()方法

stream终端操作

  • void forEach(Consumer<? super T> action): 内部迭

    1
    users.stream().forEach(user -> System.out.println(user.getName()));
  • <R, A> R collect(Collector<? super T, A, R> collector): 收集、将流转换为其他形式,比如转换成List、Set、Map。collect方法是用Collector作为参数,Collector接口中方法的实现决定了如何对流执行收集操作(如收集到 List、Set、Map)。但是 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例。

从List获取某元素转为新的list

1
2
3
4
5
/1.提取出list对象中的一个属性
List<String> stIdList1 = stuList.stream().map(Person::getId).collect(Collectors.toList());

//2.提取出list对象中的一个属性并去重
List<String> stIdList2 = stuList.stream().map(Person::getId).distinct().collect(Collectors.toList());

将list按照元素中的某个属性转为不同的map

1
2
3
4
5
6
List<PayCenterBankBusiness> payCenterBankBusinesses = businessMapper.queryList(orderNo);
List<PayCenterBusinessModel> models = new ArrayList<>();

//按照业务type分组
Map<Integer, List<PayCenterBankBusiness>> businessMap =
payCenterBankBusinesses.stream().collect(Collectors.groupingBy(PayCenterBankBusiness::getBusinessType));

自定义注解@interface

元注解

所谓元注解其实就是可以注解到别的注解上的注解,被注解的注解称之为组合注解,组合注解具备其上元注解的功能.

@Target

描述注解的使用范围(即被修饰的注解可以用在什么地方),它的取值范围定义在ElementType 枚举中

1
2
3
4
5
6
7
8
9
10
11
12
13
public enum ElementType {

TYPE, // 类、接口、枚举类
FIELD, // 成员变量(包括:枚举常量)
METHOD, // 成员方法
PARAMETER, // 方法参数
CONSTRUCTOR, // 构造方法
LOCAL_VARIABLE, // 局部变量
ANNOTATION_TYPE, // 注解类
PACKAGE, // 可用于修饰:包
TYPE_PARAMETER, // 类型参数,JDK 1.8 新增
TYPE_USE // 使用类型的任何地方,JDK 1.8 新增
}

@Retention

描述注解保留的时间范围(即:被描述的注解在它所修饰的类中可以被保留到何时),定义在RetentionPolicy枚举中

1
2
3
4
5
6
public enum RetentionPolicy {

SOURCE, // 源文件保留
CLASS, // 编译期保留,默认值
RUNTIME // 运行期保留,可通过反射去获取注解信息
}

生命周期长度 SOURCE < CLASS < RUNTIME ,前者能作用的地方后者一定也能作用。如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。

@Inherited

使被它修饰的注解具有继承性(如果某个类使用了被@Inherited修饰的注解,则其子类将自动具有该注解)。

@Document

通过反射能获取到注解内容 ,比如能获取到RequestMapping注解,然后再取出属性值

1
RequestMapping requestMapping = this.getClass().getDeclaredAnnotation(RequestMapping.class);

JVM

设计模式

单例模式

一般使用双检索或静态内部类的方法

工厂模式

观察者模式

定义了对象之间的一对多的依赖,这样一来,当一个对象改变时,它的所有的依赖者都会收到通知并自动更新。

装饰器模式

策略模式

适配器模式

并发

  1. 每一个运行在Java虚拟机里的线程都拥有自己的线程栈,一个线程创建的本地变量对其它线程不可见,仅自己可见
  2. Runnable只是一个等待被调度的任务,每一个对象都有一个属于自己的监视器(monitor),但是它只在多线程环境下才发挥作用,比如在synchronized块内,wait/notify必须存在于synchronized块中。

sleep和wait

线程的阻塞状态可以分为三类:但都是因为某些原因使线程放弃当前cpu的使用权进入等待池,等待获取下一次调度机会

  • 等待阻塞:运行的线程执行wait()方法,释放锁,进入等待池
  • 同步阻塞:线程获取锁的时候,锁被别的线程抢占,进入等待池
  • 其他阻塞:运行的线程执行sleep()或者join()方法,或者发出io请求,进入等待池,sleep()不会释放锁

ThreadLocal

Lock

ReentrantLock

ConcurrentHashMap

信号量

AQS

线程池

  • ExcutorService的实现类ThreadPoolExecutor,不推荐使用Excutors的静态方法直接创建,参见阿里巴巴开发手册

  • 自己使用ThreadPoolExecutor的构造方法创建线程池

  • 线程池是如何接受请求的
    1、线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务(需要获取全局锁)。如果核心线程池里的线程都在执行任务,则执行第二步。
    2、线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里进行等待。如果工作队列满了,则执行第三步
    3、线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务(需要获取全局锁)。如果已经满了(已经达到最大线程数量了),则交给饱和策略来处理这个任务

    在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

  • 饱和策略

    1. 自定义策略,实现RejectedExecutionHandler,并自己定义策略模式
    2. 直接丢弃(DiscardPolicy)
    3. 丢弃队列中最老的任务(DiscardOldestPolicy)。
    4. 抛异常(AbortPolicy)
    5. 将任务分给调用线程来执行(CallerRunsPolicy)。
  • 向线程池提交任务

    可以使用两个方法向线程池提交任务,分别为execute()submit()方法。execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

    • execute()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    int c = ctl.get();
    // 工作线程数小于核心线程数
    if (workerCountOf(c) < corePoolSize) {
    // 直接启动新线程,true表示会再次检查workerCount是否小于corePoolSize
    if (addWorker(command, true))
    return;
    c = ctl.get();
    }
    // 如果工作线程数大于等于核心线程数
    // 线程的的状态未RUNNING并且队列notfull
    if (isRunning(c) && workQueue.offer(command)) {
    // 再次检查线程的运行状态,如果不是RUNNING直接从队列中移除
    int recheck = ctl.get();
    if (! isRunning(recheck) && remove(command))
    // 移除成功,拒绝该非运行的任务
    reject(command);
    else if (workerCountOf(recheck) == 0)
    // 防止了SHUTDOWN状态下没有活动线程了,但是队列里还有任务没执行这种特殊情况。
    // 添加一个null任务是因为SHUTDOWN状态下,线程池不再接受新任务
    addWorker(null, false);
    }
    // 如果队列满了或者是非运行的任务都拒绝执行
    else if (!addWorker(command, false))
    reject(command);

共享对象

通过在某些共享的对象变量中设置一个信号值。

等待通知机制

CountDownLatch

CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行
与CountDownLatch的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用CountDownLatch.await()方法(立刻阻塞)。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。

其他N 个线程必须引用闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()(接解除阻塞)方法来完成的;每调用一次这个方法,在构造函数中初始化的count值就减1。所以当N个线程都调 用了这个方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。
join()

线程间发送信号的一个简单方式是在共享对象的变量里设置信号值。

反射

Java 反射机制在程序运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性(包括private)。

反射的功能

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的方法

反射的作用

  • 操作因访问权限限制的属性和方法
  • 实现自定义注解
  • 按需加载类,节省编译和初始化APK的时间

反射涉及到的四个核心类

  • java.lang.Class.java: 类对象
  • Java.lang.reflect.Constructor.java: 类的构造器对象
  • java.lang.reflect.Method.java: 类的方法对象
1
2
3
4
5
int getModifiers();返回一个表示java变量访问控制符的整数
Modifier.toString(int a);返回整数a对应的访问控制符
Class getReturnType();返回返回值类型
Parameter[] getParameters();获取方法的所有参数
Class[] getExceptionTypes(); 获取并输出方法抛出的异常
  • Java.lang.reflect.Field.java: 类的成员对象
1
2
3
4
int getModifiers();返回一个表示java变量访问控制符的整数
Modifier.toString(int a);返回整数a对应的访问控制符
getType().getName(); 返回变量类型
getName(); 返回变量名称

Class对象的三种获取方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws ClassNotFoundException {
//第一种
Class person1 = Person.class;
//第二种
Person person = new Person();
Class person2 = person.getClass();
//第三种
Class person3 = Class.forName("reflect.Person");

//通过newInstance()反向创建实体,结果是Object类型,需要向下转型
Person person1 = (Person)class1.newInstance();
Person person2 = (Person)class2.newInstance();
Person person3 = (Person)class3.newInstance();
//获取类的名称(全限定类名)
System.out.println(class1.getName());
}

得到Class对象过后,可以通过newInstance()方法反向创建实体类,不过得到的是Object,需要向下转型才能获取到类属性

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
package reflect;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
* @Author: ym
* @Description:
* @Date: 2019/12/19 10:02 下午
* @Version:
*/
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException {
// 获取私有方法
//第一个参数为要获取的私有方法的名称
//第二个为要获取方法的参数的类型,参数为 Class...,没有参数就是null
//方法参数也可这么写 :new Class[]{String.class , int.class}
Method fun1 = Class.forName("reflect.Person").getDeclaredMethod("fun1", null);
//获取私有方法的访问权
//只是获取访问权,并不是修改实际权限
fun1.setAccessible(true);
//使用 invoke 反射调用私有方法
//fun1 是获取到的私有方法
//第一个参数是要操作的对象 要操作的对象
//如果是static修饰的类方法,则传null也可,
//后面两个参数传实参,没有就不传
fun1.invoke(new Person());

Method fun2 = Class.forName("reflect.Person").getDeclaredMethod("fun2", String.class);
fun2.setAccessible(true);
fun2.invoke(new Person(),"余明");


Method fun3 = Class.forName("reflect.Person").getDeclaredMethod("fun3", int.class);
//共有方法可以不要这一步
//fun3.setAccessible(true);
fun3.invoke(new Person(),3);

//获取私有变量
Field password = Class.forName("reflect.Person").getDeclaredField("password");
Person p = new Person();
password.setAccessible(true);
password.set(p,"faefa");
System.out.println(p.getPassword());
}
}

java动态代理

动态代理即让代理类动态生成,为什么可以动态生成?这就涉及到JVM的类加载机制了,Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流

  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口

    由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:

    • 从ZIP包获取,这是JAR、EAR、WAR等格式的基础
    • 从网络中获取,典型的应用是 Applet
    • 运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 类中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为 *$Proxy 的代理类的二进制字节流
    • 由其它文件生成,典型应用是JSP,即由JSP文件生成对应的Class类
    • 从数据库中获取等等

    所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用(因为虚拟机对二进制流的来源并没有规范,所以来源途径可以多种多样)。但是这个计算过程确是很复杂的,所以我们需要借助现有的方案

常见的字节码操作类库

  • Apache BCEL (Byte Code Engineering Library):是Java classworking广泛使用的一种框架,它可以深入到JVM汇编语言进行类操作的细节。
  • ObjectWeb ASM:是一个Java字节码操作框架。它可以用于直接以二进制形式动态生成stub根类或其他代理类,或者在加载时动态修改类。
  • CGLIB(Code Generation Library):是一个功能强大,高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。
  • Javassist:是Java的加载时反射系统,它是一个用于在Java中编辑字节码的类库; 它使Java程序能够在运行时定义新类,并在JVM加载之前修改类文件。
  • Spring AOP默认是使用JDK动态代理,如果代理的类没有接口则会使用CGLib代理

最常见的两种动态代理方式

Spring AOP默认是使用JDK动态代理,如果代理的类没有接口则会使用CGLib代理

如果是单例的我们最好使用CGLib代理,如果是多例的我们最好使用JDK代理,原因:

  • JDK在创建代理对象时的性能要高于CGLib代理,而生成代理对象的运行性能却比CGLib的低。

  • 如果是单例的代理,推荐使用CGLib

    1. 通过继承类的方式。–>. CGLIB动态代理,生成的代理类其实是委托类的子类
  1. 通过实现接口的方式。–>.JDK动态代理

    主要涉及两个类

    java.lang.reflect.Proxy : Proxy提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。

    1
    2
    3
    4
    //返回指定接口的代理类的一个实例,该接口将方法分配给指定的程序处理
    Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
    new Class<?>[] { Foo.class },
    handler);

    java.lang.reflect.InvocationHandler(接口)

    1
    2
    //在代理实例上处理方法并返回结果   
    Object invoke(Object proxy,Method method,Object[] args) throws Throwable

动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理

代理模式(设计模式)

代理模式:给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。

代理模式角色分为 3 种:

Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;

RealSubject(真实主题角色):真正实现业务逻辑的类;

Proxy(代理主题角色):用来代理和封装真实主题;

如果根据字节码的创建时机来分类,可以分为静态代理和动态代理:

  • 所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。
  • 而动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件