文章目录
写在前面:Java反射是我们做项目中必备的技能,本篇文章将重新学习反射的基本用法、反射的应用场景等。
一、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源码