文章目录


写在前面:Java反射是我们做项目中必备的技能,本篇文章将重新学习反射的基本用法、反射的应用场景等。

Java反射学习和反射的应用场景干货都在这里-LMLPHP

一、Java反射定义

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的以及动态调用对象的方法的功能称为Java的反射机制

二、Java反射机制实现

1、Class对象获取

在一个JVM中,一种类,只会有一个类对象存在。

public class Person {
    private String name;
    private int age;
    private String address;
}
// 省略get、set方法
private void setCardId(String id) {
        System.out.println(id);
    }

获取类对象有3种方式

  • Class.forName()
  • Person.class
  • new Person().getClass()
//第一种方式 通过对象getClass方法
Person person = new Person();
Class<?> class1 = person.getClass();
//第二种方式 通过类的class属性
class1 = Person.class;
try {
    //第三种方式 通过Class类的静态方法——forName()来实现
    class1 = Class.forName("com.sl.Person");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

第三种方式这里也通过捕获异常,因为我们传的这个字符串可能不合法,字符串合法命名是类的命名空间和类的名称组成。

2、获取class对象的摘要信息

以下例子将判断Class对象是否是基础类型、是否是集合类、是否是注解类、是否是接口类、是否是枚举类、是否是匿名内部类、是否被某个注解类修饰、获取class的包信息、获取class类名、获取class访问权限、内部类、外部类。

获取Person的Class对象

Class class1 = Person.class;

判断是否是基础类型

 boolean isPrimitive = class1.isPrimitive();

判断是否是集合类

  boolean isArray = class1.isArray();

判断是否是注解类

boolean isAnnotation = class1.isAnnotation();

判断是否是接口类

boolean isInterface = class1.isInterface();

判断是否是枚举类

boolean isEnum = class1.isEnum();

判断是否是匿名内部类

boolean isAnonymousClass = class1.isAnonymousClass();

判断是否被某个注解类修饰

   boolean isAnnotationPresent = class1.isAnnotationPresent(Deprecated.class);

获取class名字 包含包名路径

 String className = class1.getName();

获取class的包信息

  Package aPackage = class1.getPackage();

获取class类名

String simpleName = class1.getSimpleName();

获取class访问权限

 int modifiers = class1.getModifiers();

内部类

 Class<?>[] declaredClasses = class1.getDeclaredClasses();

外部类

   Class<?> declaringClass = class1.getDeclaringClass();

3、获取class对象的属性、方法、构造函数等

  • 获取class对象的属性
Field[] fields = class1.getFields();
Field[] declaredFields = class1.getDeclaredFields();

getFields和getDeclaredFields的区别:
getFields 只能获取public的,包括从父类继承来的字段。
getDeclaredField 可以获取本类所有的字段,包括private的,但是不能获取继承来的字段。 (注: 这里只能获取到private的字段,但并不能访问该private字段的值,除非加上setAccessible(true))。

Field常用方法field.set()举例:
field.set()方法获取属性并修改。

 Person person = new Person();
 person.setName("张三");
 Field field = person.getClass().getDeclaredField("name");
 field.setAccessible(true);
 field.set(person,"李四");
 System.out.println(person.getName());
  • 获取class对象的方法

获取class对象的所有声明方法

Method[] methods = class1.getDeclaredMethods();

获取class对象的所有方法 包括父类的方法

Method[] allMethods = class1.getMethods();

如何调用类的私有方法

先在测试类中编写一个测试的私有方法

private void setCardId(String id) {
        System.out.println(id);
    }

正常的调用类的方法都是通过类.方法调用,所以我们调用私有方法也需要得到类的实例。
首先通过 getDeclaredMethod方法获取到这个私有方法,第一个参数是方法名,第二个参数是参数类型。
然后通过invoke方法执行,invoke需要两个参数一个是类的实例,一个是方法参数。

 Class[] str = {String.class};
 Method method= class1.getDeclaredMethod("setCardId", str);
 method.setAccessible(true);
 setName.invoke(person,"123333333");
  • 获取class对象的父类

获取class对象的父类

Class parentClass = class1.getSuperclass();

获取class对象的所有接口

Class<?>[] interfaceClasses = class1.getInterfaces();
  • 获取class对象构造函数

获取class对象的所有声明构造函数,但是不能获取父类的构造函数

Constructor<?>[] allConstructors = class1.getDeclaredConstructors();

获取class对象public构造函数,但是不能获取私有的构造函数

Constructor[] constructors = class1.getConstructors();

获取类中特定的构造方法并创建对象

step1:可以通过getDeclaredConstructor()方法传参获取特定参数类型的构造方法,这里要进行异常捕获,因为可能不存在对应的构造方法。

Class[] clz = {String.class,int.class};
Constructor declaredConstructor = class1.getDeclaredConstructor(clz);

step2:调用newInstance()方法创建对象

 Person person1 = (Person) declaredConstructor.newInstance("赵云", 25);
  • 获取类的泛型类型
    在平常写代码时会遇到这样的写法 parameterizedType.getActualTypeArguments()[0],这样写是什么意思呢。

getClass().getGenericSuperclass()返回表示此 Class 所表示的实体(类、接口、基本类型或 void)的直接超类的 Type,然后将其转ParameterizedType。
getActualTypeArguments()返回表示此类型实际类型参数的 Type 对象的数组。
[0]就是这个数组中第一个了,简而言之就是获得超类的泛型参数的实际类型。

		Type type = class1.getGenericSuperclass();
        Type[] genericInterfaces = class1.getGenericInterfaces();
        Class<?> componentType = null;
        String typeName = type.getTypeName();
        if (type instanceof ParameterizedType) {
            Type[] actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments();
            if (actualTypeArguments != null && actualTypeArguments.length > 0) {
                componentType = (Class<?>) actualTypeArguments[0];
            }
        }  else if (type instanceof GenericArrayType) {
            // 表示一种元素类型是参数化类型或者类型变量的数组类型
            componentType = (Class<?>) ((GenericArrayType) type).getGenericComponentType();
        }else {
            componentType = (Class<?>) type;
        }

这篇文章讲的很详细Java反射获取实际泛型类型参数

三、反射的应用场景

1、动态代理

利用反射机制在运行时创建代理类(method.invoke)。
接口、被代理类不变,我们构建一个handler类来实现InvocationHandler接口。

下面我们看具体事例。创建接口、实现类、代理类

//接口
public interface Anmail {

    void name();
}
//实现类
public class Cat implements Anmail {
    @Override
    public void name() {
        System.out.println("小猫咪");
    }
}

//代理类
public class ProxyHandler implements InvocationHandler {

    private Object target;

    public ProxyHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object object, Method method, Object[] args) throws Throwable {
        System.out.println("Before invoke "  + method.getName());
        Object invoke = method.invoke(target, args);
        System.out.println("After invoke " + method.getName());
        return invoke;
    }

    public static void main(String[] args) {
        Cat cat = new Cat();
        ProxyHandler proxyHandler = new ProxyHandler(cat);
        Anmail proxy = (Anmail)(Proxy.newProxyInstance(cat.getClass().getClassLoader(), cat.getClass().getInterfaces(), proxyHandler));
        proxy.name();
    }
}

方法newProxyInstance就会动态产生代理类,并且返回给我们一个实例,实现了Anmail 接口。这个方法需要三个参数,第一个ClassLoader并不重要;第二个是接口列表,即这个代理类需要实现那些接口,因为JDK的Proxy是完全基于接口的,它封装的是接口的方法而不是实体类;第三个参数就是InvocationHandler的实例,它会被放置在最终的代理类中,作为方法拦截和代理的桥梁。注意到这里的handler包含了一个Person实例。

总结一下JDK Proxy的原理,首先它是完全面向接口的,其实这才是符合代理模式的标准定义的。我们有两个类,被代理类Person和需要动态生成的代理类ProxyClass,都实现了接口Anmail。类ProxyClass需要拦截接口Anmail上所有方法的调用,并且最终转发到实体类Person上,这两者之间的桥梁就是方法拦截器InvocatioHandler的invoke方法。

2、自定义注解实现日志管理

AOP概念在这里不在介绍详情请看这两篇文章:aop概念和7个专业术语
AOP基本概念和使用

切入点语法有两种:

  • @Pointcut(“包名…”)
//任何类的任何返回值的任何方法
execution(public * *(..))

//任何类的set开头的方法
execution(* set*(..))

//任何返回值的规定类里面的方法
execution(* com.example.demo.AccountService.*(..))

//任何返回值的,规定包或者规定包子包的任何类任何方法
execution(* com.example.demo.service..*.*(..))
  • @Pointcut("@annotation(自定义注解)")

下面将以自定义注解举例说明

有两个类需要重点关注

  • Joinpoint
 Signature getSignature() /*获取连接点的方法签名对象*/
  • ProceedingJoinPoint
    ProceedingJoinPoint继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法
//通过反射执行目标对象的连接点处的方法
- java.lang.Object proceed() throws Throwable
//通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参。
- java.lang.Object proceed(java.lang.Object[] args) throws Throwable

自定义注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    String value() default ""; //用于描述方法作用

    /**
     * 是否忽略返回值,仅方法上有效
     * @return
     */
    boolean ignoreReturn() default false;

}

切面类LogAspect

@Aspect
@Component
public class LogAspect {
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
    private static final String dateFormat = "yyyy-MM-dd HH:mm:ss";
    private static final String STRING_START = "\n--------------------------->\n";
    private static final String STRING_END = "\n<----------------------------\n";

    @Pointcut("@annotation(Log)")
    public void logPointCut() {
    }

    public Object controllerAround(ProceedingJoinPoint joinPoint) {
        try {
            return printLog(joinPoint);
        } catch (Throwable throwable) {
            log.error(throwable.getMessage(), throwable);
            return true;
        }
    }
    //通知:拦截到连接点之后要执行的代码
    @Around("logPointCut()")
    private Object printLog(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取连接点的方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //获取方法签名里的方法:方法签名里有两个方法:getReturnType   getMethod
        Method method = signature.getMethod();
        //获取类
        Class<?> targetClass = method.getDeclaringClass();
        StringBuffer classAndMethod = new StringBuffer();

        // 获取目标方法上的Log注解
        Log methodAnnotation = method.getAnnotation(Log.class);

        // 判断是否有LOG注解以及是否带有ignore参数
        if (methodAnnotation != null) {
            classAndMethod.append(methodAnnotation.value());
        }
        //拼接目标切入的类名称和方法名称
        String target = targetClass.getName() + "#" + method.getName();
        // 请求参数转JSON,对日期进行格式转换并打印出所有为null的参数
        String params = JSONObject.toJSONStringWithDateFormat(joinPoint.getArgs(), dateFormat, SerializerFeature.WriteMapNullValue);
        //日志打印拼接的调用信息
        log.info(STRING_START + "{} 开始调用--> {} 参数:{}", classAndMethod.toString(), target, params);

        long start = System.currentTimeMillis();
        //proceed()通过反射执行目标对象的连接点处的方法;
        Object result = joinPoint.proceed();
        long timeConsuming = System.currentTimeMillis() - start;
        if (methodAnnotation != null && methodAnnotation.ignoreReturn()) {
            log.info("\n{} 调用结束<-- {} 耗时:{}ms" + STRING_END, classAndMethod.toString(), target, timeConsuming);
            return result;
        }
        // 响应参数转JSON,对日期进行格式转换并打印出所有为null的参数
        log.info("\n{} 调用结束<-- {} 返回值:{} 耗时:{}ms" + STRING_END, classAndMethod.toString(), target, JSONObject.toJSONStringWithDateFormat(result, dateFormat, SerializerFeature.WriteMapNullValue), timeConsuming);

        return result;
    }

}

运行结果:

--------------------------->
 开始调用--> com.sl.dao.UserDao#save 参数:[{"age":"20","id":null,"name":"张三"}]
2020-04-06 14:45:05.695  INFO 16864 --- [nio-8080-exec-4] com.sl.aspect.LogAspect                  :
 调用结束<-- com.sl.dao.UserDao#save 返回值:null 耗时:8ms
<----------------------------

上面代码所有源码地址为反射基础部分和自定义AOP源码


03-26 23:18