java反射与注解详解,共同实现动态代理模式
个人主页:https://blog.csdn.net/hello_list
id:
学习日记
不知不觉一年过去了,整整一年,这一年写了60多篇博客,其实具体22年四月份才开始有认真写,之前就随便发了几篇,博主内容持续输出,看到这里就点个关注吧,点关注不迷路
今天我们来学习下反射和注解
思考:反射是什么?别的语言有没有反射,为什么会有反射,反射的作用有哪些?
注解又是什么?注解的作用是什么?反射与注解是什么关系,怎么样产生关系相互使用?
带着思考,我们开始学习java中的反射和注解
回到问题,什么是反射?
首先说明,反射是java特有的,jReflection(反射) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序对自身进行检查。被private封装的资源只能类内部访问,外部是不行的,但反射能直接操作类私有属性。反射可以在运行时获取一个类的所有信息,(包括成员变量,成员方法,构造器等),并且可以操纵类的字段、方法、构造器等部分。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法。所以先要获取到每一个字节码文件对应的Class类型的对象。
反射就是把java类中的各种成分映射成一个个的Java对象。
例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把一个个组成部分映射成一个个对象。(其实:一个类中这些成员方法、构造方法、在加入类中都有一个类来描述)
加载的时候:Class对象的由来是将 .class 文件读入内存,并为之创建一个Class对象。
不要想的太难,其实很简单,看完这一篇就理解了
与反射机制相关的包就都在java.lang.reflect.*;这个里面了
反射获取class的三种方式
我们想要获取一个类,就要获取它的.class文件,在获取类中的信息,那下面就是三种获取方式
方式 | 备注 |
---|---|
Class.forName(“完整类名带包名”) | 静态方法 |
对象.getClass() | |
任何类型.class |
比方说我这里有一个Student类
package com.xuexi.springboottest.pojo;
public class Student {
private int sid;
private String sname;
private int sage;
private String ssex;
public int getSid() {
return sid;
}
public void setSid(int sid) {
this.sid = sid;
}
public String getSname() {
return sname;
}
public void setSname(String sname) {
this.sname = sname;
}
public int getSage() {
return sage;
}
public void setSage(int sage) {
this.sage = sage;
}
public String getSsex() {
return ssex;
}
public void setSsex(String ssex) {
this.ssex = ssex;
}
public Student(int sid, String sname, int sage, String ssex) {
this.sid = sid;
this.sname = sname;
this.sage = sage;
this.ssex = ssex;
}
@Override
public String toString() {
return "Student{" +
"sid=" + sid +
", sname='" + sname + '\'' +
", sage=" + sage +
", ssex='" + ssex + '\'' +
'}';
}
}
被jvm编译之后成为class文件,我想要再获取这个类,怎么获取呢,三种方式
public class ReflectTest {
public static void main(String[] args) throws ClassNotFoundException {
// 方式一:通过包路径
Class<?> aClass = Class.forName("com.xuexi.springboottest.pojo.Student");
// 方式二:直接通过Student.class获取
Class<Student> studentClass = Student.class;
// 方式三:通过对象获取
Student student = new Student();
Class<? extends Student> aClass1 = student.getClass();
}
}
不管通过哪种方式,我们总能够可以获取到类的字节码文件,也就是类,那我们有了这个类,我们可以做什么呢?我们既然能够拿到类的字节码文件,那这个类中的所有都是我们可以获取并且操作的了,干什么不可以呢?
我们看下都有哪些操作方法
我们可以看jdk的帮助文档,里面的所有方法属性信息都有
但是很多,我们这里挑几个主要的看看
//获取包名、类名
clazz.getPackage().getName()//包名
clazz.getSimpleName()//类名
clazz.getName()//完整类名
//获取成员变量定义信息
getFields()//获取所有公开的成员变量,包括继承变量
getDeclaredFields()//获取本类定义的成员变量,包括私有,但不包括继承的变量
getField(变量名)
getDeclaredField(变量名)
//获取构造方法定义信息
getConstructor(参数类型列表)//获取公开的构造方法
getConstructors()//获取所有的公开的构造方法
getDeclaredConstructors()//获取所有的构造方法,包括私有
getDeclaredConstructor(int.class,String.class)
//获取方法定义信息
getMethods()//获取所有可见的方法,包括继承的方法
getMethod(方法名,参数类型列表)
getDeclaredMethods()//获取本类定义的的方法,包括私有,不包括继承的方法
getDeclaredMethod(方法名,int.class,String.class)
//反射新建实例
clazz.newInstance();//执行无参构造创建对象
clazz.newInstance(222,"韦小宝");//执行有参构造创建对象
clazz.getConstructor(int.class,String.class)//获取构造方法
//反射调用成员变量
clazz.getDeclaredField(变量名);//获取变量
clazz.setAccessible(true);//使私有成员允许访问
f.set(实例,值);//为指定实例的变量赋值,静态变量,第一参数给null
f.get(实例);//访问指定实例变量的值,静态变量,第一参数给null
//反射调用成员方法
Method m = Clazz.getDeclaredMethod(方法名,参数类型列表);
m.setAccessible(true);//使私有方法允许被调用
m.invoke(实例,参数数据);//让指定实例来执行该方法
测试
public class ReflectTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
// 方式一:通过包路径
Class<?> aClass = Class.forName("com.xuexi.springboottest.pojo.Student");
// 方式二:直接通过Student.class获取
Class<Student> studentClass = Student.class;
// 方式三:通过对象获取
Student student = new Student();
Class<? extends Student> aClass1 = student.getClass();
// 获取包名
System.out.println(aClass.getPackage().getName());
System.out.println(aClass.getSimpleName());
System.out.println(aClass.getName());
// 获取成员变量信息
Field[] field = aClass.getFields();
for (Field field1 : field) {
System.out.println(field1.getName());
}
// 获取构造方法
Constructor<?>[] constructors = aClass.getConstructors();
System.out.println(constructors);
// 获取方法
Method[] methods = aClass.getMethods();
for (Method method : methods) {
System.out.println(method.getName());
}
// 我们可以创建一个student对象
Student o = (Student) aClass.newInstance();
System.out.println(student);
}
}
结果
大家到这里可以尽情的去点方法,看看如何使用,不过一句话,只要能够拿到这个class类,我什么都可以获取到,包括调用它的方法,改造方法等等等等,都可以做到,当然这也是反射的缺点,因为有了反射,对于底层代码逻辑,就没有了安全。
反射就是这样,没什么可多说的了,我们再来学习一个东西,就是注解。
注解
什么又是注解呢?注解本省没什么作用,就是用来标注的一种元数据,可以把注解理解成一种配置文件,这么说可能有人要反驳了,我们用到的注解都是有功能的,加上什么注解就可以做什么什么,确实是这样,但注解本身就是一种标记类型的元数据,那具体怎么产生意义性的实质行为呢,那配合着反射使用就再好不过了
其实我们平常有会用到很多注解,比如
- @Override – 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
- @Deprecated – 标记过时方法。如果使用该方法,会报编译警告。
- @SuppressWarnings – 指示编译器去忽略注解中声明的警告。
作用在其他注解的注解(或者说 元注解)是:
- @Retention – 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
- @Documented – 标记这些注解是否包含在用户文档中。
- @Target – 标记这个注解应该是哪种 Java 成员。
- @Inherited – 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
从 Java 7 开始,额外添加了 3 个注解:
- @SafeVarargs – Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
- @FunctionalInterface – Java 8 开始支持,标识一个匿名函数或函数式接口。
- @Repeatable – Java 8 开始支持,标识某注解可以在同一个声明上使用多次
那我们先来了解如何自己去定义一个注解
比如我这里就定义了一个注解,就这样就可以定义一个注解,我们来了解下这个注解的组成
@Documented
@Inherited
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
String value() default "xuexi";
}
首先是上面几个元注解,JDK5.0提供了四种元注解:Retention, Target, Documented, Inherited
1、@Retention:用于指定修饰的注解的生命周期,@Rentention包含一个RetentionPolicy枚举类型的成员变量,使用@Rentention时必须为该value成员变量指定值:
SOURCE:只在源文件中有效,编译器直接丢弃这种策略的注释,在.class文件中不会保留注解信息。反编译查看字节码文件:发现字节码文件中没有MyAnnotation这个注解
CLASS: 在class文件中有效,保留在.class文件中,但是当运行Java程序时,不会继续加载了,不会保留在内存中,JVM不会保留注解。
如果注解没有加Retention元注解,那么相当于默认的注解就是这种状态。
RUNTIME:在运行时有效(即运行时保留),当运行 Java程序时,JVM会保留注释,加载在内存中了,那么程序可以通过反射获取该注释。
2、@Target:用于修饰注解的注解,用于指定被修饰的注解能用于修饰哪些程序元素,即修饰位置。@Target也包含一个名为value的成员变量。
3、@Documented:用于指定被该元注解修饰的注解类将被javadoc工具提取成文档。默认情况下,javadoc是 不包括注解的,但是加上了这个注解生成的文档中就会带着注解了
4、@Inherited: 被它修饰的Annotation将具有继承性。如果某个类使用了被 @Inherited修饰的Annotation,则其子类将自动具有该注解。
其实两个没用,我们这要来了解这两个
@Retention(RetentionPolicy.CLASS) // 这个就是我们自定义的注解的生命周期,就像上面说的一样,一般我设置成RetentionPolicy.CLASS就可以了
生命周期类型 描述
RetentionPolicy.SOURCE 编译时被丢弃,不包含在类文件中
RetentionPolicy.CLASS JVM加载时被丢弃,包含在类文件中,默认值
RetentionPolicy.RUNTIME 由JVM 加载,包含在类文件中,在运行时可以被获取到
@Target(ElementType.METHOD) // 表示注解可以使用在什么上面,我这里就是ElementType.METHOD方法上,或者还可以这些:
Target类型 描述
ElementType.TYPE 应用于类、接口(包括注解类型)、枚举
ElementType.FIELD 应用于属性(包括枚举中的常量)
ElementType.METHOD 应用于方法
ElementType.PARAMETER 应用于方法的形参
ElementType.CONSTRUCTOR 应用于构造函数
ElementType.LOCAL_VARIABLE 应用于局部变量
ElementType.ANNOTATION_TYPE 应用于注解类型
ElementType.PACKAGE 应用于包
ElementType.TYPE_PARAMETER 应用于类型变量
ElementType.TYPE_USE 应用于任何使用类型的语句中(例如声明语句、泛型和强制转换语句中的类型)
就这样,就可以了,你已经学会注解了,注解就是一种标记,用来表示一个信息,数据,本身没有上面作用,那我们通过一个小例子就可以明白了,注解+反射的使用
注解+反射的使用
我们来做一个小例子,来学习注解+反射的使用,简单的模拟一下我们平时使用的注解都是怎么工作的
首先我们定义一个注解
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
String value() default "这里已经帮你写好了学习方法";
}
光有一个注解没什么用,注解用来的是标记,标记上的数据有用啊,我们再配合反射的使用,
原理:就是注解标记在类上一个信息,我们在类运行的使用,去扫描有没有,并且注解上的信息是什么,再通过注解上的信息用反射去动态实现需要帮这个类做些什么
这也是代理模式的应用,大家可以去了解一下什么是代理,代理的实现方式有很多种,我们先用一种简单是实现下
我们再创建一个对象,都没有去具体实现这个方法去做什么,但是我都用自己定义的注解,标记了这个方法应该做什么;
public class User {
@MyAnnotation("打游戏")
public void play(){
}
@MyAnnotation("吃饭")
public void eat(){
}
@MyAnnotation
public void learning(){
}
}
怎么样让我们的注解发挥作用呢,怎么样用到反射呢,来一个代理
public class ProxyUser {
public Object proXY(User user,String methodName,@Nullable Object... param) throws InvocationTargetException, IllegalAccessException {
// 通过反射拿到类信息
Class<? extends User> aClass = user.getClass();
// 看看类中的方法有没有被我们自定义注解标记的,并拿到注解信息
Method[] methods = aClass.getMethods();
for (Method method : methods) {
if (method.getName().equals(methodName)) {
if (method.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
String value = annotation.value();
// 在这里我们就想当与可以把原本的方法覆盖了,
// 也可以拿到放的参数,以及参数类型,包括返回值,这里可以通过param接收参数
System.out.println("这里是代理对象帮你实现的方法,帮你实现了"+value+"方法");
return null;
}
// 如果方法本身就没有被我们的注解标记,我们就直接执行它自己的方法
return method.invoke(aClass, param);
}
}
// 包括没有进for循环啊,这些都可以抛出异常什么的。进行下处理
return null;
}
}
测试
public class Test01 {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
// 创建一个user对象
User user = new User();
// 我们可以先执行下本身的方法
user.play(); // 当然就是什么都没有
// 通过代理帮我们实现方法
new ProxyUser().proXY(user,"play");
new ProxyUser().proXY(user,"eat");
new ProxyUser().proXY(user,"learning");
}
}
结果
这样是不是就实现了,这就是代理的好处,有人可能认为,这样多写多少代码,这么多,但是这只是开始,往后只要是用到我这个注解的都可以通过这种方式,去实现一件事情;这就是代理,我不想做的就交给你,你帮我代理,做完就行
这只是一种方式,通过自己去实现的代理,我说过代理有n种方式,这里再给大家演示一种,通过spring中的aop,来实现对一个方法执行的扫描,aop本质上横切就是代理
是不是感觉这种方式比上面那个更好理解,其实都是一样的只是实现的步骤不一样吧,用spring有一个缺点,就是你这么做,你这个类必须是从spring容器中取出来的,为什么刚开始没有用aop,aop这个是spring的aop,我们要想用就必须用到spring,总所周知spring給java带来了春天,但是也有一个这样的说法,java现在已经严重被spring绑架了,所以说我们思考之前不要先想着具体架构实现,框架上的实现,在框架上实现,就通过最基本的代码来理解这个思想,总之一句话,要想自己轻松就要找到代理。
@Component
@Aspect
public class UserAspect {
@Pointcut("execution(* com.xuexi.springboottest.pojo..*.*(..))")
private void myPointcut() {
}
/**
* 环绕通知
*/
@Around("myPointcut()")
public void advice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around begin...");
// 本身aop实现就是通过反射,这里的代理实现了更多的增强
// 有before 环绕通知,方法执行后After
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
if (method.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
String value = annotation.value();
// 在这里我们就想当与可以把原本的方法覆盖了,
// 也可以拿到放的参数,以及参数类型,包括返回值,这里可以通过param接收参数
System.out.println("这里是代理对象帮你实现的方法,帮你实现了"+value+"方法");
}
System.out.println("around after....");
}
@Before("myPointcut()")
public void record(JoinPoint joinPoint) {
System.out.println("Before");
}
@After("myPointcut()")
public void after() {
System.out.println("After");
}
}
结果:
使用aop常用方法
@Before("customerJoinPointerExpression()")
public void beforeMethod(JoinPoint joinPoint){
joinPoint.getSignature().getName(); // 获取目标方法名
joinPoint.getSignature().getDeclaringType().getSimpleName(); // 获取目标方法所属类的简单类名
joinPoint.getSignature().getDeclaringTypeName(); // 获取目标方法所属类的类名
joinPoint.getSignature().getModifiers(); // 获取目标方法声明类型(public、private、protected)
Object[] args = joinPoint.getArgs(); // 获取传入目标方法的参数,返回一个数组
joinPoint.getTarget(); // 获取被代理的对象
joinPoint.getThis(); // 获取代理对象自己
}
// 获取目标方法上的注解
private <T extends Annotation> T getMethodAnnotation(ProceedingJoinPoint joinPoint, Class<T> clazz) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
return method.getAnnotation(clazz);
}
当然了,还有一种实现方式,基于java的原生jdk的动态代理,这里就留给大家实现以下了,可以参考我上面的代理,实现一样,这是一种思想,大家一定要理解。
JDk动态代理
本来想留下让大家自己去试着实现下,看到这里你可以不看,自己试着去实现下,然后回来看看是不是你的思路更好,如果更好可以把你的答案留在评论区,我们再来理解下,我觉得这几个理解特别好,摆出来就懂了。
-
什么是动态代理
1、使用 jdk 的反射机制,创建对象的能力, 创建的是代理类的对象。 而不用你创建类文件。不用写 java 文件。
2、动态:在程序执行时,调用 jdk 提供的方法才能创建代理类的对象。
3、jdk 动态代理,必须有接口,目标类必须实现接口,没有接口时,需要使用 cglib 动态代理。 -
动态代理能做什么
1、可以在不改变原来目标方法功能的前提下,可以在代理中增强自己的功能代码。
2、背景示例。
比如:你所在的项目中,有一个功能是其他人(公司的其它部门,其它小组的人)写好的,你可以使用(但是只有 Class 文件):Demo.class。
Demo dm = new Demo();
dm.print();
当这个功能缺少时, 不能完全满足我项目的需要。我需要在 dm.print() 执行后,需要自己在增加代码。
这时就会用代理实现 dm.print() 调用时, 增加自己代码, 而不用去改原来的 Demo 文件。
(在 mybatis,spring 中也有应用)
2、就是当你写了一个接口,里面有方法,然后写了实现类实现方法,完成方法逻辑,然后有一天,想对这个方法进行修改,但是不想改源码,所以有两种方式可以实现。第一种是静态代理,创建一个类继承实现类,然后对方法进行修改,这样太局限,因为只能针对特定的类增强方法,有100个实现类就要创建100个子类去实现。第二种方式是动态代理,可以动态地生成代理类,这是可以接受的。
实现动态代理,我们需要学会什么
InvocationHandler 接口(调用处理器)
就一个方法 invoke()。
invoke():表示代理对象要执行的功能代码。你的代理类要完成的功能就写在 invoke() 方法中。
-
代理类完成的功能:
1、调用目标方法,执行目标方法的功能
2、功能增强,在目标方法调用时,增加功能。 - 方法原型:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
参数:
Object proxy:jdk创建的代理对象,无需赋值。
Method method:目标类中的方法,jdk提供method对象的
Object[] args:目标类中方法的参数, jdk提供的。
Proxy类:核心的对象,创建代理对象。之前创建对象都是 new 类的构造方法()
现在我们是使用Proxy类的方法,代替new的使用。
方法: 静态方法 newProxyInstance()
作用是: 创建代理对象, 等同于静态代理中的TaoBao taoBao = new TaoBao();
参数:
1、ClassLoader loader 类加载器,负责向内存中加载对象的。 使用反射获取对象的ClassLoader类a,
a.getCalss().getClassLoader(),目标对象的类加载器。
2、Class[] interfaces:接口,目标对象实现的接口,也是反射获取的。 3、InvocationHandler h:我们自己写的,代理类要完成的功能。 返回值:就是代理对象 public static Object newProxyInstance(ClassLoader loader, Class[] interfaces,
InvocationHandler h)
还是之前的功能,但是我们需要添加一个东西,其实之前我们就应该遵守面向接口原则,但是之前的user类,确实没有遵守,不过刚好这里也做了强调了,使用JDK动态代理,被代理的对象必须要有接口
添加接口
public interface UserInter {
public void play();
public void eat();
public void learning();
}
然后我们写jdk的动态代理类,看到没spring中的aop简单实现
// 必须实现实现InvocationHandler进行方法增强
public class JDKProXYUser implements InvocationHandler {
// 被代理对象
private Object target;
// 通过构造方法,传入被代理对象
public JDKProXYUser(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Object proxy, Method method, Object[] args
// 被代理对象,被代理对象方法,被代理对象执行方法的参数
// System.out.println(proxy);
// System.out.println(method);
// System.out.println(args);
/**
* 方法中的proxy,method都是拿的接口类中的
* 比如这样:都是接口中的方法
* method.isAnnotationPresent(MyAnnotation.class)
* || method.getDeclaringClass().isAnnotationPresent(MyAnnotation.class)
*/
Method targetMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());
System.out.println("aop====方法前置增强====");
// 看看类中的方法有没有被我们自定义注解标记的,并拿到注解信息
if (target.getClass().isAnnotationPresent(MyAnnotation.class)
|| targetMethod.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = targetMethod.getAnnotation(MyAnnotation.class);
String value = annotation.value();
// 在这里我们就想当与可以把原本的方法覆盖了,
// 也可以拿到放的参数,以及参数类型,包括返回值,这里可以通过param接收参数
System.out.println("这里是代理对象帮你实现的方法,帮你实现了" + value + "方法");
}
System.out.println("aop====方法后置增强====");
return null;
}
}
测试
public class Test02 {
public static void main(String[] args) {
// 面向接口创建一个user类
User user = new User();
// 创建代理类进行代理
JDKProXYUser jdkProXYUser = new JDKProXYUser(user);
// 这里需要这个帮我们实现代理,并且返回一个全新的代理对象,三个参数
UserInter newUser = (UserInter) Proxy.newProxyInstance(
// 反射获取的被代理对向的类加载器
user.getClass().getClassLoader(),
// 目标对象实现的接口,所以这里为什么要实现接口
user.getClass().getInterfaces(),
// 实现代理的对象
jdkProXYUser
);
newUser.eat();
}
}
结果:
到这里就完事了,看下你的思路是什么样子的,最后这里再提一下,jdk的静态代理是什么,继承重写父类方法,这是静态代理。
小结
那都看到这里了,还不三连,点赞收藏关注吗,持续输出更好内容,bye~