Java 反射基本使用+使用案例

本文重点:

介绍反射基础且常用方法 , 和自己写一些反射的案例, 这个可是架构师本领的必须技能:

  • 案例一 : 反射实现SPI。
  • 案例二: 反射实现自动生成Controller接口文档。

反射定义

反射允许我们在运行时获取类、接口、字段和方法 , 并且可以进行修改。如果要写框架代码必须要掌握!

接下来我们先了解下怎么用反射。

定义一个java对象:

public class Apple {
	private int color ;
	public String area;
	public int getColor() {
		return color;
	}
	public void setColor(int color) {
		this.color = color;
	}
	public String getArea() {
		return area;
	}
	public void setArea(String area) {
		this.area = area;
	}
}

获取Class

反射的一些操作都是要先获取Class对象的,Class对象在j同一个classloader下面只有一个实例。 有下面几个办法可以获取Class。

Class<Apple> clazz;
// 1. 直接.class
clazz = Apple.class;
// 2. 通过 类的全路径
clazz = (Class<Apple>) Class.forName("collection.Apple");
// 3. 类加载器加载
clazz = (Class<Apple>) Thread.currentThread().getContextClassLoader().loadClass("collection.Apple");

获取字段

// 获取所有字段 getDeclaredFields
Arrays.asList(Apple.class.getDeclaredFields()).forEach(f->System.out.println(f.getName()));

// 获取单个字段
Apple.class.getDeclaredField("area").getName();

修改字段值

Apple apple = new Apple();
apple.setArea("深圳");
// 获取area字段
Field field = Apple.class.getDeclaredField("area");
// private字段,需要强制
field.setAccessible(true);
// 将值修改成长沙
field.set(apple, "长沙");
System.out.println(apple.getArea());  //长沙

获取方法

//获取所有方法
Apple.class.getDeclaredMethods();
//获取setArea 方法
Apple.class.getDeclaredMethod("setArea", String.class);

执行方法

Apple apple = new Apple();
apple.setArea("深圳");
// 获取setArea 方法
Method m = Apple.class.getDeclaredMethod("setArea", String.class);
// 调用apple对象的 setArea
m.invoke(apple, "长沙");
System.out.println(apple.getArea()); // 长沙

获取调用者的Class

// 这个在 1.8就不允许调用了
sun.reflect.Reflection.getCallerClass();

我这里有个替代方案:

public static  Class<?> getCallerClass(int index) throws ClassNotFoundException{
	StackTraceElement[] elements = Thread.currentThread().getStackTrace();
	return Class.forName(elements[1].getClassName());
}

System.out.println(getCallerClass(0)); // 0的话就是当前类

获取继承的类,接口

// 获取 继承的类
Apple.class.getSuperclass()
// 获取实现的接口
Apple.class.getInterfaces()

注意如果是几代继承就获取不到。比如: A 实现B ,B实现C 。 获取A的getInterfaces就只能返回B ,而不返回C 。

实例化对象

class.newInstance() 
class.getConstructor(Class<?>... parameterTypes).newInstance(Object ... initargs)

基础就这么多,下面我们来利用反射来搞点案例,不然知道反射也没用。


案例一. 反射实现SPI机制

场景: 通过改配置文件,来实现不同功能的切换 。 比如我们有一个缓存接口,提供redis实现和memecache实现:

//缓存接口
public interface Cache {
	// 设置缓存
	void set(String key , String value);
	//获取缓存
	String get(String key);
}
//redis 提供的缓存实现
public class RedisCache implements Cache{
	@Override
	public void set(String key, String value) {
		System.out.println("[redis缓存] set");
	}
	@Override
	public String get(String key) {
		System.out.println("[redis缓存] get");
		return "";
	}
}
//MemeCache 提供的缓存实现
public class MemeCache implements Cache{
	@Override
	public void set(String key, String value) {
		System.out.println("[MemeCache缓存] set");
	}
	@Override
	public String get(String key) {
		System.out.println("[MemeCache缓存] get");
		return "";
	}
}

通过项目/META-INF/spring.factorys 文件的配置去加载到底用哪个缓存组件?

spring.factory文件:

# collection.RedisCache 是RedisCache类的全路径代表项目要用redis缓存
collection.RedisCache

新建一个ServiceLoader来实现这个功能:

public class ServiceLoader {
	// 读取spring.factorys文件, 把行内容变成 Map key: 接口 value:对应的实现类
	private static Map<Class<?>, Class<?>> doFind() throws IOException {
		Path f = Paths.get(ServiceLoader.class.getResource("/").getPath().substring(1), "META-INF/spring.factorys");
		List<String> strs = Files.readAllLines(f, Charset.forName("UTF-8"));
		final Map<Class<?>, Class<?>> map = new HashMap<>();
		for (String line : strs) {
			try {
				Class<?> impl = Class.forName(line);
				map.put(Class.forName(line).getInterfaces()[0], impl);
			} catch (Exception e) {
			}
		}
		return map;
	}
	
	@SuppressWarnings("unchecked")
	public static <T> T load(Class<T> inter) throws Exception {
		// 加载 spring.factorys文件
		Map<Class<?>, Class<?>> mapper = doFind();
		// 查找 对应的实现类
		Class<T> impl = (Class<T>) mapper.get(inter);
		if (impl == null) {
			return null;
		}
		// 否则 利用反射生成 实现
		return doCreate(impl);
	}
	// 反射实例化对象
	private static <T> T doCreate(Class<T> impl) throws InstantiationException, IllegalAccessException {
		return impl.newInstance();
	}
}

代码逻辑:

  • 利用Paths加载${项目}/META-INF/spring.factorys文件,找到具体用哪个实现类。
  • 将上述实现类forClass得到Class对象,并通过getInterfaces获取其实现的接口,封装成Map<接口Class,实现类Class>。
  • 匹配load方法用户传过来的接口,通过上面map得到实现类,通过newInstance方法构造实例返回。


我们来测试一下:

public static void main(String args[]) throws Exception {
	new Thread() {
		public void run() {
			while (true) {
				try {
					ServiceLoader.load(Cache.class).get("");
					Thread.sleep(2000);
				} catch (Exception e) {
				}
			}
		};
	}.start();
}

我们用 线程循环 调用加载,得到缓存实现,然后调用其get方法。由于配置文件spring.factorys当前是:collection.RedisCache ,所以load的是RedisCache ,打印结果:

https://pic1.zhimg.com/v2-0bc43e895bbaab08e4b7a49a84344dcc_b.jpg

我们不重启JVM ,然后修改spring.factorys为:collection.MemeCache , 程序立马打印:

https://pic4.zhimg.com/v2-6f9edf7f313498dd8a3b8a6a8a77e1eb_b.jpg

到此我们实现了不重启JVM就改变业务实现的功能,而且这样也可以使模块之间不对实现类进行硬编码, 实现框架扩展和替换模块,充分实现实现解耦 这都归功于强大的反射!!!!!

SPI 在jdk,dubbo,springboot 中的应用,在下面文章讲了,请观看:

罗政:Java SPI+Dubbo SPI+SpringBoot SPI 原理解析


案例二. 反射实现自动生成接口文档

我们对 spingmvc 不陌生把,看下面一个获取用户信息接口:

@RestController
public class UserController {
	public static class UserDTO {
		@ApiModelProperty("用户id")
		Integer userId;
		@ApiModelProperty("用户名称")
		String name ;
	}
	public static class UserVO {
		@ApiModelProperty("用户id")
		Integer userId;
		@ApiModelProperty("用户名称")
		String name;
		@ApiModelProperty("手机号")
		String phone;
	}
	@PostMapping("/get_user")
	public UserVO conf(@RequestBody UserDTO dto) {
		return new UserVO();
	}
}

@Target(ElementType.FIELD) 
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiModelProperty {
	// 字段的注释
	public String value() default "";
}

ApiModelProperty注解代表配置的接口文档字段的注释信息。

如果叫你生成一个接口文档:

地址: http://localhost:8080/get_user
方法: post
请求参数
     字段名    类型        注释
     userId    integer     用户id
     name      string      用户名称

返回参数
    字段名    类型          注释
    userId   integer       用户id
    name     string        用户名称
    phone    string        用户名称

你是想用手打呢,还是用反射自动生成????

下面我来告诉你怎么实现,首先来定义接口的数据建模:

public class ApiEntry {
	// post get 请求
	String method;
	// 请求url
	String url;
	// 请求字段集合
	List<ApiField> reqs;
	// 返回字段集合
	List<ApiField> res;

	static class ApiField {
		String name; // 字段名称
		String type; // 类型
		String notice; // 注释
	}
}

定义解析方法:

public class ApiGenerator {
	public static void doParse(Class<?> controller) {
		String url = "http://localhost:8080";
		List<ApiEntry> apis = new ArrayList<>();
		// 获取所有方法
		for (Method method : controller.getDeclaredMethods()) {
			if (!method.isAnnotationPresent(PostMapping.class) && !method.isAnnotationPresent(GetMapping.class)) {
				// 没有配置 PostMapping 且 GetMapping 说明不是controller方法
				continue;
			}
			ApiEntry apiEntry = new ApiEntry();
			if (method.isAnnotationPresent(PostMapping.class)) {
				apiEntry.method = "POST";
				apiEntry.url = url + method.getAnnotation(PostMapping.class).value()[0];
			}
			if (method.isAnnotationPresent(GetMapping.class)) {
				apiEntry.method = "GET";
				apiEntry.url = url + method.getAnnotation(GetMapping.class).value()[0];
			}
			// 解析 请求参数 的字段
			apiEntry.reqs = parseApiFields(method.getParameterTypes()[0]);
			// 解析 返回数据 的字段
			apiEntry.res = parseApiFields(method.getReturnType());
			apis.add(apiEntry);
		}
		// 打印接口文档
		apis.forEach(api -> {
			System.out.println("地址: " + api.url);
			System.out.println("方法: " + api.method);
			System.out.println("请求参数:");
			System.out.println("\t字段名\t类型\t注释");
			for (ApiField f : api.reqs) {
				System.out.println("\t" + f.name + "\t" + f.type + "\t" + f.notice);
			}
			System.out.println("返回参数:");
			System.out.println("\t字段名\t类型\t注释");
			for (ApiField f : api.res) {
				System.out.println("\t" + f.name + "\t" + f.type + "\t" + f.notice);
			}

		});
	}

	private static List<ApiField> parseApiFields(Class<?> clazz) {
		List<ApiField> apiFields = new ArrayList<>();
		for (Field field : clazz.getDeclaredFields()) {
			// 获取字段
			ApiField apiField = new ApiField();
			apiField.name = field.getName();
			// 获取 类型的 简单名称 如:java.lang.Integer 简单名称就是 Integer
			apiField.type = field.getType().getSimpleName();
			// 获取注释
			apiField.notice = field.getAnnotation(ApiModelProperty.class).value();
			apiFields.add(apiField);
		}
		return apiFields;
	}
	
	public static void main(String[] args) {
		ApiGenerator.doParse(UserController.class);
	}
}

代码注释已经很明确了,核心思想步骤:

  • 通过controller的class ,调用其getDeclaredMethods 获取全部方法,一个方法代表一个接口(前提是配置了PostMapping 或 GetMapping 注解)。
  • 通过isAnnotationPresent方法判断是否配置了指定注解,如果是PostMapping就是POST请求,否则就是GET请求。
  • 通过getAnnotation方法获取注解,得到value值,也就是url地址。
  • 解析请求字段。通过method.getParameterTypes 方法得到所有方法参数,这里我们取第一个,得到参数class,然后通过getDeclaredFields方法获取所有字段,从而得到字段名,类型,配置的ApiModelProperty注解的注释信息。
  • 同理解析返回字段。打印生成文档。

程序效果:

这个例子中,我们巧妙用到了反射,注解来完成我们的功能。

今天的两个例子只是反射的冰山一角,反射要配合动态代理,javaassit(asm) , classloader远程加载,agent等技术使用才能感觉到它的强大,后面我们会一一详解。


强力推荐一个Java架构师修炼博客,全是干货

JAVA架构师修炼

支付宝打赏 微信打赏

如果文章对您有帮助,您可以鼓励一下作者