引言
Java反射机制是一个非常强大的功能,在很多大型项目比如Spring, Mybatis中都可以看见反射的身影。通过反射机制我们可以在运行期间获取对象的类型信息,利用这一特性我们可以实现工厂模式和代理模式等设计模式,同时也可以解决Java泛型擦除等令人苦恼的问题。
本文我们就从实际应用的角度出发,来应用一下Java的反射机制。
反射基础
p.s: 本文需要读者对反射机制的API有一定程度的了解,如果之前没有接触过的话,建议先看一下官方文档的Quick Start。
在应用反射机制之前,首先我们先来看一下如何获取一个对象对应的反射类
Class
,在Java中我们有三种方法可以获取一个对象的反射类。
通过getClass方法
在Java中,每一个
Object
都有一个
getClass()
方法,通过getClass方法我们可以获取到这个对象对应的反射类:
String s = "http://www.ziwenxie.site" ;
Class> c = s.getClass();
通过forName方法
我们也可以调用
Class
类的静态方法
forName()
:
Class> c = Class.forName("java.lang.String" );
使用.class
或者我们也可以直接使用
.class
:
获取类型信息
在文章开头我们就提到反射的一大好处就是可以允许我们在运行期间获取对象的类型信息,下面我们通过一个例子来具体看一下。
首先我们在
typeinfo.interfacea
包下面新建一个接口
A
:
package typeinfo.interfacea;
public interface A { void f () ; }
接着我们在
typeinfo.packageaccess
包下面新建一个类
C
,类
C
实现了接口
A
,并且我们还另外创建了几个用于测试的方法,注意下面几个方法的权限都是不同的。
package typeinfo.packageaccess;
import typeinfo.interfacea.A;
class C implements A {
public void f () { System.out.println("public C.f()" ); }
public void g () { System.out.println("public C.g()" ); }
protected void v () { System.out.println("protected C.v()" ); }
void u () { System.out.println("package C.u()" ); }
private void w () { System.out.println("private C.w()" ); }
}
public class HiddenC {
public static A makeA () { return new C(); }
}
在
callHiddenMethod()
方法中我们用到了几个新的API,其中
getDeclaredMethod()
根据方法名用于获取Class类指代对象自己声明的某个方法,然后我们通过调用
invoke()
方法就可以触发对象的相关方法:
package typeinfo;
import typeinfo.interfacea.A;
import typeinfo.packageaccess.HiddenC;
import java.lang.reflect.Method;
public class HiddenImplementation {
public static void main (String[] args) throws Exception {
A a = HiddenC.makeA();
a.f();
System.out.println(a.getClass().getName());
callHiddenMethod(a, "g" );
callHiddenMethod(a, "u" );
callHiddenMethod(a, "v" );
callHiddenMethod(a, "w" );
}
static void callHiddenMethod (Object a, String methodName) throws Exception {
Method g = a.getClass().getDeclaredMethod(methodName);
g.setAccessible(true );
g.invoke(a);
}
}
从输出结果我们可以看出来,不管是
public
,
default
,
protect
还是
private
方法,通过反射类我们都可以自由调用。当然这里我们只是为了显示反射的强大威力,在实际开发中这种技巧还是不提倡。
public C.f()
typeinfo.packageaccess.C
public C.g()
package C.u()
protected C.v()
private C.w()
上面我们只是测试了
Method
对象,感兴趣的读者在熟悉了反射的API之后,不妨测试一下
Filed
,这里我们就不重复了。
利用动态代理实现面向切面编程
AOP是Spring提供的一个强大特性之一,AOP的意思是面向切面编程,就是说要分离和业务不相关的代码,当我们需要新增相关的事务的时候,我们不想要对业务本身做修改。面向切面编程和面向对象变成相比到底有什么好处呢,我们通过一个例子来看一下,对于新手来说,常常会写出下面这样的代码:
public class Example1 {
public void execute () {
Logger logger = Logger.getLog(...);
PerformanceUtil.startTimer(...);
if (!user.hasPrevilege()) {
}
executeTransaction();
PerformanceUtil.endTimer();
}
}
虽然我们上面真正要执行的业务只有
executeTransaction()
,但是日志,性能,权限相关的代码差不多要将真正的业务代码掩盖了。而且以后如果我们还有一个
Example2
,它同样需要实现相同的日志,性能,权限代码。这样当以后我们需要新增相关的逻辑检查的时候,我们需要所有
Example
进行重构,这显然不符合面向对象的一个基本原则-
封装变化
。
上面这个场景利用模板方法和装饰器模式都可以解决,在Spring中是通过动态代理来实现的,下面我们通过一个例子来模拟一下Spring中的AOP实现。
我们要实现的业务时,统计程序统计员工工资所执行的时间以及检查用户的权限。首先先来实现的
Salary
类,它里面包含一些实现统计员工工资的业务逻辑:
public interface SalaryInterface {
public void doSalary () ;
}
public class Salary implements SalaryInterface {
public void doSalary () {
...
}
}
通过
InvocationHandler
我们来实现动态代理,以后当我们调用
obj
的相关方法之前,都会通过
invoke
方法进行代理,而不会直接调用
obj
方法。
public class SimpleProxy implements InvocationHandler {
private Object obj;
private Object advice;
public Object bind (Object obj, Advice advice) {
this .obj = obj;
this .advice = advice;
return Proxy.newProxyInstance(obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(), this )
}
public Object invoke (Object proxy, Method method, Object[] args) throws Throwalbe {
Object result = null ;
try {
advice.before();
result = method.invoke(obj, args);
advice.after();
} catch (Exception e) {
e.printStackTrace();
}
return result
}
}
模拟
Spring
中的
Advice
接口:
public interface Advice {
public void before () ;
public void after () ;
}
实现
TimeAdvice
用于统计程序的执行时间:
public class TimeAdvice implements Advice {
long startTime;
long endTime;
@Override
public void before () {
startTime = System.nanoTime();
}
@Override
public void after () {
endTime = System.nanoTime();
}
}
客户端调用代码如下:
public
class Client {
public static void main (String[] args) {
SimpleProxy = new SimpleProxy();
SalaryInterface salaryInterface =
(SalaryInterface) simpleProxy.bind(new Salary(), new TimeAdvice());
salaryInterface.doSalary();
}
}
如果我们现在需要新增权限控制,我们来实现
ControlAdvie
类:
public class ControlAdvice implements Advice {
@Override
public void before () {
if (...) {
...
} else {
...
}
}
@Override
public void after () {
...
}
}
而我们客户端的代码只需要改成
simpleProxy.bind(new Salary(), new ControlAdvie)
就行了,而
SimpleProxy
本身不需要做任何的修改。
与注解相结合
在单元测试框架比如
Junit
中反射机制也得到了广泛的应用,即通过注解的方式。下面我们简单地来了解一下如何通过反射机制来获取相关方法的注解信息,比如说我们有下面这样一个业务场景,当用户在修改自己密码的时候,为了保证密码的安全性,我们要求用户的新密码要满足一些条件,比如说至少要包含一个非数字字符,不能与以前的密码相同之类的条件等。
import java.lang.annotation.*
@Target (ElementType.METHOD)
@Retention (RetentionPolicy.RUNTIME)
public @interface UserCase {
public int id () ;
public String description () default "no description" ;
}
下面是我们检测密码的工具类的实现:
public class PasswordUtils {
@UserCase (id=47 , description="Password must contain at least one numeric" )
public boolean validatePassword (String password) {
return (password.matches("\w*\d\w*" ));
}
@UserCase (id=48 )
public String encryptPassword (String password) {
return new StringBuilder(password).reverse().toString();
}
@UserCase (id=49 , description="New passwords can't equal previously used ones" )
public boolean checkForNewPassword (List prevPasswords, String password) {
return !prevPasswords.contains(password);
}
}
利用反射我们可以写出更加清晰的测试代码,其中
getDeclaredMethods()
方法可以获取相关对象自己声明的相关方法,而
getAnnotation()
则可以获取
Method
对象的指定注解。
public class UseCaseTracker {
public static void trackUseCases (List useCases, Class> cl) {
for (Method m : cl.getDeclaredMethods()) {
UseCase uc = m.getAnnotation(UseCase.class);
if (uc != null ) {
System.out.println("Found Use Case: " + uc.id() + " " + uc.description());
useCases.remove(new Integer(uc.id()));
}
}
for (int i : useCases) {
System.out.println("Warning: Missing use case-" + i);
}
}
public static void main (String[] args) {
List useCases = new ArrayList();
Collections.addAll(useCases, 47 , 48 , 49 , 50 );
trackUseCases(userCases, PasswordUtils.class);
}
}
解决泛型擦除
现在有下面这样一个业务场景,我们有一个泛型集合类
List
>
,我们需要统计出这个集合类中每种具体的
Pet
有多少个。由于Java的泛型擦除,注意类似
List extends Pet>
的做法肯定是不行的,因为编译器做了静态类型检查之后,到了运行期间JVM会将集合中的对象都视为
Pet
,但是并不会知道
Pet
代表的究竟是
Cat
还是
Dog
,所以到了运行期间对象的类型信息其实全部丢失了。p.s: 关于泛型擦除,我在上一篇文章里面有详细解释,感兴趣的朋友可以看一看。
为了实现我们上面的例子,我们先来定义几个类:
public class Pet extends Individual {
public Pet (String name) { super (name); }
public Pet () { super (); }
}
public class Cat extends Pet {
public Cat (String name) { super (name); }
public Cat () { super (); }
}
public class Dog extends Pet {
public Dog (String name) { super (name); }
public Dog () { super (); }
}
public class EgyptianMau extends Cat {
public EgyptianMau (String name) { super (name); }
public EgyptianMau () { super (); }
}
public class Mutt extends Dog {
public Mutt (String name) { super (name); }
public Mutt () { super (); }
}
上面的
Pet
类继承自
Individual
,
Individual
类的的实现稍微复杂一点,我们实现了
Comparable
接口,重新自定义了类的比较规则,如果不是很明白的话,也没有关系,我们已经将它抽象出来了,所以不理解实现原理也没有关系。
public class Individual implements Comparable <Individual > {
private static long counter = 0 ;
private final long id = counter++;
private String name;
public Individual (String name) { this .name = name; }
public Individual () {}
public String toString () {
return getClass().getSimpleName() + (name == null ? "" : " " + name);
}
public long id () { return id; }
public boolean equals (Object o) {
return o instanceof Individual && id == ((Individual)o).id;
}
public int hashCode ()