#注解、反射及动态代理

一、注解(Annotation)

1.什么是注解:

  注解可以说是注释的更高级的一种,相当于标记,注解同样不影响代码的执行,但是注解能够用来创建文档跟踪代码中的依耐性执行基本编译时的检查
  同时注解是一个接口,程序可以通过反射来获取指定程序中元素的Annotation对象,然后通过该对象获取注解中元数据信息信息。

即:==Annotation(注解)是Java提供的一种对元程序中元素关联信息和元数据(metadata)的一种途径和方法。==

[引入时间:jdk1.5之后]

标记可以用在包、类、方法、字段、方法参数及局部变量上来关联信息。这些信息被储存在Annotation的“name = value”结构对中。

注意:这些标记和关联的信息并不会影响程序中代码的任何执行,而至于可以用在反射的时候在运行期间被访问,那是只有通过特定的工具才能对Anntation类型中的数据访问和处理。

2.注解分类

按照注解参数个数分类:

  1. 标记注解:一个没有成员定义的Annotation类型被称为标记注解。
  2. 单值注解。
  3. 完整注解

根据注解的使用方法和用途分类:

  1. JDK内置系统注解。
  2. 元注解。
  3. 自定义注解。

系统内置标准注解:
3个标准内置注解,定义在java.lang中:

  • @Override:用于修饰此方法重写父类的方法。
  • @Deprecated:用于修饰已经过时的方法。
  • @SuppressWarning:用于禁止特定的编译警告。

==@Override:限定重写父类方法==

使用这个注解来标记我们重写的方法,能够帮我们判断是否是重写,即方法名写错的时候,编译报错。

==@Deprecated:标记已过时==

使用这个注解标记某个方法,让这个方法称为过时的方法,当使用时候,能够提示使用者这个方法已经过时了,不推荐使用。

==@SuppressWarning:抑制编译器警告==

使用这个注解的时候,可以去除我们不想看到或者出现的警告。(目前不知道这个有什么用。)

抑制编译器警告参数:

  1. deprecation:使用了不赞成的类或方法时的警告。
  2. unchecked: 使用了未检查的转换时的警告,例如使用集合的时候没有用泛型(Generics)来指定集合保存的类型。
  3. fallthrough:当Switch程序块直接通往下一种情况而没有使用break时的警告。
  4. path:当类路径、源文件中有不存在的路径时的警告。
  5. serial:在可序列化的类上缺少serialVersionUID定义时的警告。
  6. finally:任何finally句子不能正常完成时的警告。
  7. all:以上所有的警告。

3.自定义注解

注解的深入学习,自定义注解。想要自定义注解,首先我们要了解元注解及相关自定义注解的语法。

①元注解(meta-annotation):
元注解的作用是注解其他注解(也就是我们自定义的注解包括系统内置标准注解)。

  • @Target
  • @Retention
  • @Documented
  • @Inherited

==@Target==

这个元注解说明了Annotation所修饰的范围:即注解可被用于package、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数、本地变量(循环变量、catch参数等)。
用于描述注解的使用范围,这个范围的取值(ElementType)有:

  • CONSTRUCTOR:用于描述构造器
  • FIELD:用于描述属性
  • LOCAL_VARIABLE:用于描述局部变量
  • METHOD:用于描述方法
  • PACKAGE:用于描述包
  • PARAMETER:用于描述参数
  • TYPE:用于描述类、接口(包括注解类型)和enum申明、

使用实例:

/**
 * 注解Table可用于注解类、接口(包括Annotation类型)或enum申明
 */
Target(ElementType.TYPE)
public @interface Table {
    /**
     * 数据表名称注解,默认值为类名称
     */
    public abstract String tableName() default "className";
}

/**
 * 注解NoDBColumn仅可用于注解类的成员变量
 */
Target(ElementType.FIELD)
public @interface NoDBColumn {
}

==@Retention==

作用:表示在什么级别保存注解信息,用于描述注解的生命周期(即被描述的注解在什么范围内有效),取值(RetentionPoicy):

  • SOURCE:在源文件中有效
  • CLASS:在class文件中有效
  • RUNTIME:在运行时有效

具体实例:

/**
 * Column注解的RetentionPolicy属性值是RUNTIME,这样注解处理器就可以通过
 * 反射,获取到该注解的属性值,从而做一些运行时的逻辑处理
 */
@(Java高级)[感谢网上各个大神文章, 让我们明悟!!!]
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    public abstract String name() default "fileName";
    public abstract String setFuncName() default "setField";
    public abstract String getFuncName() default "getField";
    public abstract boolean defaultDBValue() default false;
}

==@Documented==

是一个标记注解,没有成员。用于描述其他类型的annotation应该作为被标注的程序成员的公共API,即可以被javadoc这样的工具文档化。

具体实例:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Column {
    public abstract String name() default "fileName";
    public abstract String setFuncName() default "setField";
    public abstract String getFuncName() default "getField";
    public abstract boolean defaultDBValue() default false;
}

==@Inherited==

  这也是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
  注意@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。
  当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。

具体实例:

@Inherited
public @interface Greeting {

    public enum FontColor {
        BULE, RED, GREEN
    };

    String name();

    FontColor fontColor() default FontColor.GREEN;
}

②自定义注解
自定义注解格式:
public @interface 注解名{定义体}

注意事项:自定义注解时,自动继承java.lang.Annotation接口 ,由编译程序自动完成其他细节。同时,不能继承其他接口或注解。

定义体中每一个方法实际上都是一个配置参数,方法名=参数名返回值类型=参数的类型(返回值类型只能是基本类型、Class、Annotation、String、enum和以上所有类型的数组),可以通过default申明参数的默认值。

参数设定:

  • 只能用public和默认(default)这两个访问权限修饰符修饰。
  • 只有一个参数成员时,最好将参数名称设置为"value"。

具体实例:

/**
 * 水果名称注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
    String value() default "";
}

    /**
     * 颜色属性
     * @return
     */
    Color fruitColor() default Color.BULE;
}
//被注解的类
public class Apple {
    @FruitName("Apple")
    private String appleName;

    @FruitColor(fruitColor = FruitColor.Color.RED)
    private String appleColor;

    public void setAppleColor(String appleColor) {
        this.appleColor = appleColor;
    }

    public String getAppleColor() {
        return appleColor;
    }

    public void setAppleName(String appleName) {
        this.appleName = appleName;
    }

    public String getAppleName() {
        return appleName;
    }

    public void displayName() {
        System.out.println("水果的名字是:苹果");
    }
}

==注意:==

注解元素必须要有确定的值,即要么在定义注解中设置默认值,要么在使用注解时给定值,一般使用空字符串或0作为默认值,但是很难看出这个元素是存在或缺失的的特征,所以我们在注解定义的时候,给元素赋上特殊的值,空字符串或-1,这个是一种习惯。


4.注解处理器

对于我们写的注解,如果不能去读取它的话,那还不如写注释呢!
所以,接下来我们要做的就是创建使用注解处理器。

①注解处理器类库(Java.lang.reflect.AnnotatedElement)
我们知道Annotation接口是所有Annotation类型的父接口,在java.lang.reflect包下有一个AnnotatedElement接口(它是所有程序元素的父接口),它有一些实现类使我们需要的:

  • Class :类的一些定义
  • Constructor :类的构造器定义
  • Field :类的成员变量定义
  • Method :类的方法定义
  • Packet :包的一些定义

在java.lang.reflect包下有很多工具,是在我们反射的时候需要用的。

程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的4个方法来访问Annotation信息:

  • T getAnnotation(Class annotationClass):返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。
  • Annotation[] getAnnotations():返回该程序元素上所有存在的注解。
  • boolean is AnnotationPresent(Class< ?extends Annotation> annotationClass):判断该程序元素上是否包含指定类型的注解,存在返回true,不存在返回false。
  • **Annotation[] getDeclaredAnnotaions(): **返回直接存在于此元素上的所有注解。(忽略继承过来的注解)如果没有直接存在于次元素上的注解,则返回一个长度为0的数组。

下面是一个简单的例子:

==自定义注解==

/**
 * 水果名称注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
    String value() default "";
}

/**
*   水果颜色注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitColor {
    /**
     * 颜色枚举
     */
    public enum Color{
      BULE , RED , GREEN
    };

    /**
     * 颜色属性
     * @return
     */
    Color fruitColor() default Color.BULE;
}

/**
 * 水果供应者注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
    /**
     * 供应商编号
     */
    public int id() default -1;

    /**
     *
     * 供应商名称
     */
    public String name() default "";

    /**
     *
     * 供应商地址
     */
    public String address() default "";
}

==自定义注解处理器==

/**
 * 注解处理器
 */
public class FruitInfoUtil {
    public static void getFruitInfo(Class<?> clazz){
        String strFruitName = "水果名称:";
        String strFruitColor = "水果颜色:";
        String strFruitProvicer = "供应商信息:";

        //返回直接存在于clazz上的所有注释
        Field[] fields = clazz.getDeclaredFields();

        for(Field field : fields){
            if(field.isAnnotationPresent(FruitName.class)){
                FruitName fruitName = (FruitName) field.
                        getAnnotation(FruitName.class);
                strFruitName = strFruitName + fruitName.value();
                System.out.println(strFruitName);
            }else if(field.isAnnotationPresent(FruitColor.class)){
                FruitColor fruitColor = (FruitColor) field.
                        getAnnotation(FruitColor.class);
                strFruitColor = strFruitColor + fruitColor.fruitColor();
                System.out.println(strFruitColor);
            }else if(field.isAnnotationPresent(FruitProvider.class)){
                FruitProvider fruitProvider = (FruitProvider) field.
                        getAnnotation(FruitProvider.class);
                strFruitProvicer = "供应商编号:" + fruitProvider.id()
                        + " 供应商名称:" + fruitProvider.name()
                        + " 供应商地址:" + fruitProvider.address();
                System.out.println(strFruitProvicer);
            }


        }

    }
}

==测试类==

public class FruitRun {
    public static void main(String[] args) {
        FruitInfoUtil.getFruitInfo(Apple.class);
    }
}

运行结果
水果名称:Apple
水果颜色:RED
供应商编号:9527 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延安路89号红富士大厦

到此,我们注解就讲完了,下面附上一张图,便于我们整理知识点和巩固知识点。
[图片上传失败...(image-e731e1-1540044443552)]


二、反射

反射是Java动态性之一。
何为动态性,即动态性语言:
  程序在运行时可以改变其结构,新的函数可以引进,已有的函数可以被删除等结构上的变化。(如JavaScript、Ruby、Python等属于动态语言)

本质上Java和C/C++一样不属于动态语言的,但是因为反射机制,可以在程序运行期间加载、探知和使用编译期间完全未知的类,并可以生成相关类对象的实例,从而调用某个方法和改变某个属性值。因此,Java属于半个动态性语言。


1.反射机制

概念
Java中的反射机制是指在运行状态中,对于任何一个类都能知道这个类的所有属性和方法;并且对于任意一个对象,都能调用它的方法;这种动态获取信息和调用对象方法的功能即为Java语言的反射机制。

使用场合(为什么要使用反射)
我们学习过多态,知道编译类型和运行类型不一致的时候,当我们想调用运行类型特中的特有方法的时候需要强制转换,因为我们可能知道这个对象属于那些类。
但是当我们在实际开发过程中,可能会有某个外部对象传入过来,我们需要使用这个对象独有的方法,那在编译的过程中而我们根本无法知道这个对象属于哪些类,只有运行的时候我们才能知道传入的对象和类的信息,那么这个时候就要用到反射了!

2.反射使用

①反射API

  • Class类(java.lang.Class):反射的核心类,可以获取类的属性、方法等信息。
  • Field类(java.lang.reflec):表示类中的成员变量,可以获取和设置类中的属性值。
  • Method类(Java.lang.reflec):表示类的方法,可以获取方法中的信息或执行方法。
  • Constructor类(Java.lang.reflec):表示类的构造方法。

②获取方法和属性等信息
大致步骤:获取想要的类的Class对象—>调用Class类中的方法—>
使用反射API来操作这些信息。

获取Class对象的几个方法

  • Class clazz = 对象名(想要的类的对象).getClass();
  • Class clazz = 类名.getClass();
  • 使用Class类中的forName()静态方法(最安全、性能最好)
    ==Class clazz = Class.forName("类的安全路径");==(通常使用这个)

简单案例:

package com.xsl.reflectTest;

/**
 * 测试类Person,通过反射获取这个类的方法和属性
 */
public class Person {
    private String name;
    private String gender;
    private int age;

    public Person() {}
    public Person(String name, String gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
    }
    //getter和setter方法
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getGender() {
        return gender;
    }
    public void setGender(String gender) {
        this.gender = gender;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }

    public String toString(){
        return "姓名:"+name+"  性别:"+gender+"  年龄:"+age;
    }
}

接下来我们通过反射来获取Person类中的方法、属性等信息

package com.xsl.reflectTest;

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

public class ReflectTest {
    public static void main(String[] args) {
        try {
            //获取Person类的Class对象
            Class clazz = Class.forName("com.xsl.reflectTest.Person");

            //获取Person类的所有方法
            Method[] methods = clazz.getDeclaredMethods();
            for (Method m : methods) {
                System.out.println(m);
            }

            //获取Person类的所有属性
            Field[] fields = clazz.getDeclaredFields();
            for (Field  f : fields) {
                System.out.println(f);
            }

            //获取Person类所有的构造器
            Constructor[] constructors = clazz.getDeclaredConstructors();
            for(Constructor cons : constructors){
                System.out.println(cons);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
} 

输出结果:
所有方法
public java.lang.String com.xsl.reflectTest.Person.getGender()
public void com.xsl.reflectTest.Person.setGender(java.lang.String)
public int com.xsl.reflectTest.Person.getAge()
public void com.xsl.reflectTest.Person.setAge(int)
public java.lang.String com.xsl.reflectTest.Person.toString()
public java.lang.String com.xsl.reflectTest.Person.getName()
public void com.xsl.reflectTest.Person.setName(java.lang.String)

所有属性
private java.lang.String com.xsl.reflectTest.Person.name
private java.lang.String com.xsl.reflectTest.Person.gender
private int com.xsl.reflectTest.Person.age

所有构造方法
public com.xsl.reflectTest.Person()
public com.xsl.reflectTest.Person(java.lang.String,java.lang.String,int)

③来创建对象为我们使用
创建对象的两种方法:

  • 使用Class对象的==newInstance()==方法创建该Class对象对应类的实例。(要求对应的类要有空参构造器)目前提示不推荐使用,即方法过时。【2018年】
  • 使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建对应类的实例。(这个方法可以指定构造方法创建实例)

简单例子(延续上面的例子):

public class demo01 {
    public static void main(String[] args) throws Exception{
        //首先获取Class对象
        Class clazz = Class.forName("com.xsl.reflectTest.Person");

        //采用第一种方法创建对象
        Person person = (Person) clazz.newInstance();//提示了不推荐使用,我们知道就好
        //设置属性
        person.setName("刘备");
        person.setAge(20);
        person.setGender("男");
        System.out.println(person);//会默认调用toString()方法,我们重写了
        System.out.println("====================");
        Constructor constructor = clazz.getDeclaredConstructor(
                String.class,String.class,int.class);
        Person person1 = (Person) constructor.newInstance("孙尚香","女",16);
        System.out.println(person1);
    }
} 

运行结果:
姓名:刘备 性别:男 年龄:20

姓名:孙尚香 性别:女 年龄:16


3.通过反射操作泛型、注解

①反射操作泛型
Java采用泛型擦除机制来引入泛型。Java中的泛型仅仅是给编译器javac使用的,为确保数据的安全性和免去强制类型转换的麻烦。
  为了通过反射操作这些类型以迎合开发的需要,Java新增了ParameterizedTypeGenericArrayTypeTypeVariableWildcardType这几个类型代表不能被归一到Class类中的类型但是又和原始类型齐名的类型。

  • ParameterizedType:表示一种参数化的类型,比如Collection< String >
  • GenericArrayType:表示一种元素类型是参数化类型或者类型变量的数组类型。
  • TypeVariable:是各种类型变量的公共父接口。
  • WildcardType:代表一种通配符类型表达式,比如?、? extends Number、? super Integet。

简单实例:

/*
    通过反射获取泛型信息
 */
public class Demo {

    /*
        定义两个带泛型的方法
     */

    public static  void test01(Map<String,Person> map, List<Person> list){
        System.out.println("Demo01.test01()");
    }

    public Map<Integer,Person> test02(){
        System.out.println("Demo01.test02()");
        return null;
    }

    public static void main(String[] args) throws NoSuchMethodException {
        //获取指定方法参数泛型信息
        Method m = Demo.class.getMethod("test01", Map.class, List.class);
        Type[] t = m.getGenericParameterTypes();

        for(Type paramType : t){
            System.out.println("#"+paramType);
            if(paramType instanceof ParameterizedType){
                //获取泛型中的具体信息
                Type[] genericTypes = ((ParameterizedType) paramType).
                                        getActualTypeArguments();
                for (Type genericType : genericTypes){
                    System.out.println("泛型信息:"+genericType);
                }
            }
        }
        System.out.println();
        //获得指定方法返回值泛型信息
        Method m2 = Demo.class.getMethod("test02",null);
        Type returnType = m2.getGenericReturnType();
        System.out.println("#"+returnType);
        if(returnType instanceof ParameterizedType){
            Type[] genericTypes = ((ParameterizedType) returnType).
                                        getActualTypeArguments();
            for(Type genericType : genericTypes){
                System.out.println("返回值,泛型类型:"+genericType);
            }
        }
    }
}


②反射操作注解
反射操作注解,即在上面注解中的注解器便是通过反射操作的!


4.反射性能

Method/Constructor/Field/Element都继承了AccessibleObject,其中有一个方法setAccessible(boolean flag)方法,这个方法有两个作用:

  1. 启动/禁止访问安全检查开关:参数flag为true时,反射的对象在使用时,取消Java语言访问检查。默认有false,实施Java语言访问检查。
  2. 禁止安全检查,提高反射效率。

简单实例:

public class TestReflect {
    public static void testNoReflect(){
        Person person = new Person();
        long startTimes = System.currentTimeMillis();
        for(int i = 0;i < Integer.MAX_VALUE; i++){
            person.getName();
        }
        long times = System.currentTimeMillis() - startTimes;
        System.out.println("没有通过反射,调用方法消耗时间:"+times+"毫秒");
    }

    public static void testNoAccess() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Person person = new Person();
        Method method = Class.forName("com.xsl.reflectTest.Person").
                                    getMethod("getName");
        long startTimes = System.currentTimeMillis();
        for(int i =0;i < Integer.MAX_VALUE;i ++){
            method.invoke(person,null);
        }
        long times = System.currentTimeMillis() - startTimes;
        System.out.println("通过反射,没有访问权限,调用方法共消耗时间:"+
                        times+"毫秒");
    }

    public static void testUserAccess() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Person person = new Person();
        Method method = Class.forName("com.xsl.reflectTest.Person").
                getMethod("getName");
        method.setAccessible(true);
        long startTimes = System.currentTimeMillis();
        for(int i =0;i < Integer.MAX_VALUE;i ++){
            method.invoke(person,null);
        }
        long times = System.currentTimeMillis() - startTimes;
        System.out.println("通过反射,有访问权限,调用方法共消耗时间:"+
                times+"毫秒");
    }

    public static void main(String[] args) throws Exception {
        testNoReflect();
        testNoAccess();
        testUserAccess();
    }
}

运行结果:
没有通过反射,调用方法消耗时间:4毫秒
通过反射,没有访问权限,调用方法共消耗时间:5077毫秒
通过反射,有访问权限,调用方法共消耗时间:2611毫秒

虽然直接调用和反射调用消耗时间相差很大,但是这是我们将调用次数放大到了Integer.MAX_VALUE的结果,问题不大。但是禁止访问检查后,反射的效率确实提升了不少。


三、动态代理

在了解动态代理之前,我们先了解一下一种设计模式--代理模式--
同时对于代理,根据创建代理的时间点,分为静态代理和动态代理。


1.代理模式

代理模式是Java常用的设计模式,特征是代理类和委托类有==同样的接口==,代理类的工作主要是负责为委托类预处理消息、过滤消息、把消息转发给委托类以及事后处理消息等。
代理类和委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定服务。即我们访问实际对象时,是通过代理对象来访问的。代理模式就是在访问实际对象时引入一定程度的间接性,通过这种间接性,可以附加多种用途。


2.静态代理

何为静态代理?
由程序员创建或特定工具自动生成源代码,也就是在编译时就已经将接口、被代理类、代理类等确定下来。在运行之前代理类的.class文件就已经生成。
静态代理的简单实现:
首先我们定义好==公共接口==:

/*
    学生(接口)-->就是 被代理类(学生) 和代理类(班长)的公共接口
 */
public interface Person {
    //教班费的行为
    void giveMoney();
}

定义好==被代理类==:

/*
    Student类实现Person接口,并实现交班费的功能
 */
public class Student implements Person{
    private String name;
    public Student(String name){
        this.name = name;
    }
    @Override
    public void giveMoney() {
        System.out.println(name+"交了班费100块");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

然后定义==代理类==:

/*
    学生代理类,也实现了Person接口,保存一个学生实体,如此就可以代理学生产生行为
 */
public class StudentProxy implements Person{
    //被代理的学生
    Student stu;

    public StudentProxy(Student stu){
        //只代理学生
        if(stu.getClass() == Student.class){
            this.stu = stu;
        }
    }

    //代理交班费,调用被代理学生上交班费行为
    @Override
    public void giveMoney() {
        stu.giveMoney();
    }
}

最后我们来测试一下:

public class Test {
    public static void main(String[] args) {
        //被代理的学生曹操,他的班费上交由代理对象monitor班长完成
        Student caocao = new Student("曹操");

        //生成代理对象,并将曹操传给代理对象
        Person monitor = new StudentProxy(caocao);

        //班长代理上交班费
        monitor.giveMoney();
    }
}

运行结果:
曹操交了班费100块

从测试及结果上看,我们并没有直接通过Student类对象caocao来执行交班费的行为,而是通过StudentProxy类对象monitor来代理执行的,这种模式就是代理模式。
代理模式:公共接口(Person),被代理类(Student),代理类(StudentProxy),而且代理类要有被代理类的具体实例,才能通过这个间接的调用方法

那么就有疑问了?这样做有什么意义呢?
这个意义就在于代理模式的间接性,通过间接性,我们可以在代理过程中加入其它的用途或者是处理。
简单的说,在交班费之前,先来评价一下Student对象caocao,修改一下代码:

/*
    学生代理类,也实现了Person接口,保存一个学生实体,如此就可以代理学生产生行为
 */
public class StudentProxy implements Person{
    //被代理的学生
    Student stu;

    public StudentProxy(Student stu){
        //只代理学生
        if(stu.getClass() == Student.class){
            this.stu = stu;
        }
    }

    //代理交班费,调用被代理学生上交班费行为
    @Override
    public void giveMoney() {
        //在这里添加就可以了
        System.out.println(stu.getName()+"学习很好,就是疑心太大,老说有人偷他作业");
        stu.giveMoney();
    }
}

重新执行:
曹操学习很好,就是疑心太大,老说有人偷他作业
曹操交了班费100块


我们可以代理过程中切入一些其他的操作。
这个可以看Spring的面向切面编程(AOP),在一个切点前执行操作,在一个切点后执行操作。


3.动态代理

动态代理就是代理类在程序运行时创建的方式。
  对于静态代理,代理类是我们已经定义好的,在程序运行之前就编译完成的。而动态代理中,代理类不是在Java代码中定义的,而是在运行期间根据我们在Java代码中的“提示”动态生成的。

那么相比于静态代理,动态代理有什么好处呢?
动态代理的优势可以很方便的对代理类的函数进行统一处理,而不用修改每个代理类中的方法。

用上面静态代理中代理类StudentProxy中的giveMoney()方法举例子:

    @Override
    public void giveMoney() {
          //调用被代理方法前我们加入个处理方法
          beforeMethod(); 

          stu.giveMoney();//被代理方法
    }

那好,这里我们只有一个giveMoney()方法,我们也只需要写一次beforeMethod()处理方法,但是如果多了很多其他方法呢,我们是不是也要在每个方法中,都写一次处理方法呢?
所以我们通过动态代理可以解决这个问题:

动态代理的简单实现:
首先,我们需要java.lang.reflect包下的==Proxy类==和一个==InvocationHandler==接口,通过这2个工具我们可以生成JDK动态代理类和动态代理对象。

基本步骤

  • 创建一个==InvocationHandler对象==
//MyInvocationHandler<Person>(stu)这个是我们实现 
//InvocationHandler接口的类
InvocationHandler stuHandler = new MyInvocationHandler<Person>(stu);
  • 使用Proxy类的==getProxyClass静态方法==生成一个动态代理类对象StuProxyClass
Class<?> stuProxyClass = Proxy.getProxyClass(Person.class.getClassLoader(),new Class<?>[]{Person.class});
  • 获得stuProxyClass中一个带InvocationHandler参数的constructor
Constructor<?> constructor = PersonProxy.getConstructor(InvocationHandler.calss);
  • 通过构造器constructor来创建动态实例stuProxy
Person stuProxy = (Person)constructor.newInstance(stuHandler);

OK,动态代理对象创建完毕。
上面4个步骤也可以分成2个步骤完成:

//创建一个与动态代理对象相关联的InvocationHandler
InvocationHandle stuHandler = new MyInvocationHandler<Person>(stu);
//创建一个代理对象stuProxy,代理对象的每个方法执行
//都会替换执行Invocation中的invoke方法
Person stuProxy = (Person)Proxy.newProxyInstance(Person.class.getClassLoader(),new Class<?>[]{Person.class},stuHandler);

我们基本上以上面的例子来展示:

首先我们创建一个==接口==:

/*
    创建接口
 */
public interface Person {
    //交班费
    void giveMoney();
    //学习
    void study();
}

然后是需要==被代理的类==:

/*
    创建需要被代理的类
 */
public class Student implements Person {
    private String name;

    public Student(String name) {
        this.name = name;
    }


    @Override
    public void giveMoney() {
        try {
            //假设数钱用时1秒
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name+"交了班费50元");
    }

    @Override
    public void study() {
        System.out.println(name+"正在学习");
    }
}

然后我们创建一个工具类,用来打印方法执行的时间:

/**
 *  检测方法执行时间的工具类:
 *      即在方法执行前调用start方法,方法结束后调用finsh方法,即可
 *      计算出方法执行的时间
 */
public class MonitorUti {
    private static ThreadLocal<Long> t = new ThreadLocal<>();

    public static void start(){
        t.set(System.currentTimeMillis());
    }

    public static void finsh(String methodName){
        long endTimes = System.currentTimeMillis();
        System.out.println(methodName+"方法一共耗时"+(endTimes - t.get())+"毫秒");
    }

}

再实现InvocationHandler接口,并持有代理对象实例

public class stuInvocationHandler<T> implements InvocationHandler {
    //invocationHandler持有的被代理对象
    T target;

    public stuInvocationHandler(T target){
        this.target =target;
    }

    /**
     *
     * @param proxy 代表动态代理对象
     * @param method    正在执行的方法
     * @param args  调用目标方法时传入的实参
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代理执行"+method.getName()+"方法");

        //代理过程中插入检测方法,计算方法耗时
        MonitorUti.start();
        Object result = method.invoke(target,args);
        MonitorUti.finsh(method.getName());

        return result;
    }
}

最后我们直接测试:

public class ProxyTest {
    public static void main(String[] args) throws InterruptedException {
        //创建一个实例对象,这个对象是被代理的对象
        Person caocao = new Student("曹操");

        //创建一个与代理对象相关联的InvocationHandler
        InvocationHandler stuHandler = new stuInvocationHandler<Person>(caocao);

        /*
        创建一个代理对象来代理caocao,
        代理对象的每个执行方法都会替换执行Invocation中的invoke方法
        */
        Person stuProxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(),
                new Class[]{Person.class},stuHandler);
        //执行交班费的方法
        stuProxy.giveMoney();
        stuProxy.study();

    }
}

运行结果:
代理执行giveMoney方法
曹操交了班费50元
giveMoney方法一共耗时1012毫秒
代理执行study方法
曹操正在学习
study方法一共耗时0毫秒

结论:
我们发现,我们调用被代理类方法的时候,我们并没有修改其任何代码,但是执行结果却改变了了,我们只是在StuInvocationHadnler中修改了几行代码而已,我们调用被代理类的任何方法,运行时都被添加了我们自定义的工具类的方法。
这就是动态代理带来的改变,但是具体是为什么了?

4.动态代理原理

我们先来慢慢理清楚:

  1. 我们有==被代理类==
  2. 我们有==公共接口(所有的抽象方法被代理类都要实现的)==
  3. 我们有实现了InvocationHandler接口的类,我们叫它==调用处理器==(这里构造参数T,表示泛型,这样无论被代理类是什么类型,都能传入了)
  4. 最后我们通过Proxy类的静态方法创建了代理类对象

大致图我们看下:
[图片上传失败...(image-fb521e-1540044443552)]

所以,终归那么多,最主要的核心还是
Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[]{Person.class},stuHandler);
这段代码,这段代码究竟做了什么呢?
经过我们分析源码,一层一层解刨,在Java虚拟机缓存中,找到了生成的代理类:
public final class $Proxy0 extends Proxy implements Person
这个代理类的构造器:

 public $Proxy0(InvocationHandler paramInvocationHandler){
    super(paramInvocationHandler);
  }

这个父类构造器有一个参数InvocationHandler paramInvocationHandler
这不就是我们传入的==调用处理器对象==吗?
再来看父类Proxy源码:

//这里正好有个this.h就是InvocationHandler类型!!!
protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }

因此,子类$Proxy0中不正好也有了这个对象吗?
继续分析代理类源码:

//在静态代码块中有这样的一行代码
m3 = Class.forName("proxy.Person").getMethod("giveMoney", new Class[0]);

这不正是我们代理类的吗,通过反射获取方法,然后继续找:

//这里重写了接口Person的方法
  public final void giveMoney()
    throws 
  {
    try
    {
      //上面我们知道h是我们传入的参数“调用处理器”对象
      this.h.invoke(this, m3, null);
      return;
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }

终于拨开云雾了:
this.h.invoke(this, m3, null);这行代码不就是直接调用了我们“调用处理器”中实现的invoke方法吗?
到这,终于知道动态反射的一系列过程了!!!!


5.总结

从源码可以看出,生成的代理类是继承了Proxy类,所以,动态代理只能对接口来代理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容