代理、反射、注解、hook
- 2020 年 3 月 27 日
- 笔记
代理
通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,扩展目标对象的功能。 代理对象拦截真实对象的方法调用,在真实对象调用前/后实现自己的逻辑调用 这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法。
动态代理的用途与装饰模式很相似,就是为了对某个对象进行增强。所有使用装饰者模式的案例都可以使用动态代理来替换。
/** * subject(抽象主题角色): * 真实主题与代理主题的共同接口。 */ interface Subject { void sellBook(); } /** * ReaISubject(真实主题角色): * 定义了代理角色所代表的真实对象。 */ public class RealSubject implements Subject { @Override public void sellBook() { System.out.println("出版社卖书"); } } /** * Proxy(代理主题角色): * 含有对真实主题角色的引用,代理角色通常在将客户端调用传递给真实主题对象之前或者之后执行某些操 作,而不是单纯返回真实的对象。 */ public class ProxySubject implements Subject { private RealSubject realSubject; @Override public void sellBook() { if (realSubject == null) { realSubject = new RealSubject(); } sale(); realSubject.sellBook(); give(); } public void sale() { System.out.println("打折"); } public void give() { System.out.println("送优惠券"); } } public class Main { public static void main(String[] args) { //静态代理(我们自己静态定义的代理类) ProxySubject proxySubject = new ProxySubject(); proxySubject.sellBook(); //动态代理(通过程序动态生成代理类,该代理类不是我们自己定义的。而是由程序自动生成) RealSubject realSubject = new RealSubject(); MyHandler myHandler = new MyHandler(); myHandler.setProxySubject(realSubject); Subject subject = (Subject) Proxy.newProxyInstance(realSubject.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), myHandler); subject.sellBook(); } } public class MyHandler implements InvocationHandler { private RealSubject realSubject; public void setProxySubject(RealSubject realSubject) { this.realSubject = realSubject; } /** * @param proxy 指代我们所代理的那个真实对象 * @param method 指代的是我们所要调用真实对象的某个方法的Method对象 * @param args 指代的是调用真实对象某个方法时接受的参数 * @reurn * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { sale(); proxy = method.invoke(realSubject, args); give(); return proxy; } public void sale() { System.out.println("打折"); } public void give() { System.out.println("送优惠券"); } }
HOOK
Hook(钩子): Android 操作系统中系统维护着自己的一套事件分发机制,那么Hook就是在事件传送到终点前截获并监控事件的传输,并修改事件流程的过程。
public static void showToast(Context context, CharSequence cs, int length) { Toast toast = Toast.makeText(context,cs,length); hook(toast); toast.show(); }
Hook 的选择点: 静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位。 Hook 过程: 寻找 Hook 点,原则是静态变量或者单例对象,尽量 Hook public 的对象和方法。 选择合适的代理方式,如果是接口可以用动态代理。 偷梁换柱——用代理对象替换原始对象。 Android 的 API 版本比较多,方法和类可能不一样,所以要做好 API 的兼容工作
应用
- Hook指定应用注入广告
- 修复bug
- App登录劫持
登录界面上面的用户信息都存储在EditText控件上,然后通过用户手动点击“登录”按钮才会将上面的信息发送至服务器端去验证账号与密码是否正确。这样就很简单了,黑客们只需要找到开发者在使用EditText控件的getText方法后进行网络验证的方法,Hook该方法,就能劫持到用户的账户与密码了
AOP(AspectJ)
正如面向对象编程是对常见问题的模块化一样,面向切面编程是对横向的同一问题进行模块化,比如在某个包下的所有类中的某一类方法中都需要解决一个相似的问题,可以通过AOP的编程方式对此进行模块化封装,统一解决 关于AOP的具体解释,可以参照维基百科。而AspectJ就是面向切面编程在Java中的一种具体实现。
Join point:程序中执行代码插入的点,例如方法调用时或者方法执行时。
AOP编程的具体使用场景 日志记录 持久化 行为监测 数据验证 缓存 …
比如埋点,记录方法执行的时长。可以定义注解。aop可以过滤所有被"这个注解"标记的方法和构造器。然后可以可以根据他提供的方法(注解),讲我们想要埋点的日志插入进去。
注解和反射的区别 反射:对于任何一个对象,都能够调用它的任何一个方法和属性,包括私有的。这种动态获取的方法就叫反射。 注解:降低项目的耦合度;自动完成一些规律性的代码;自动生成java代码,减轻开发者的工作量。 而注解需要用到反射: 定义注解,使用注解,读取注解(用到反射)
Annotation Processing Tool(APT),注解处理器,javac中用于编译时扫描和解析Java注解的工具。 在编译阶段执行的,它的原理就是读入Java源代码,解析注解,然后生成新的Java代码。新生成的Java代码最后被编译成Java字节码,注解解析器不能改变读入的Java 类,比如不能加入或删除Java方法。
//绑定一个View (View不能为private 或者static)否则就会通过反射去获取 @BindView(R.id.textview)
注解和反射效率问题 hook和aop类似 反射先new类class,然后在从类里面new对象。Class.getMethod(…)还要查找所有的方法。 而注解编译期间就完成了注解的反射工作, jvm只是读取。
反射的缺点 不安全 编译器没法对反射相关的代码做优化 慢的原因还有安全检查,访问控制等。比如说这个方法能不能获得,能不能执行等,传进的参数的类型检查等。
注解的用法
获取类的注解 URLBuilder.Path path = paramEntity.getClass().getAnnotation(URLBuilder.Path.class);
Builder(Retrofit retrofit, Method method) { this.retrofit = retrofit; this.method = method; this.methodAnnotations = method.getAnnotations(); this.parameterTypes = method.getGenericParameterTypes(); this.parameterAnnotationsArray = method.getParameterAnnotations(); }
获取方法的注解 Method.getAnnotations(); 是获取方法上面对应的注解。method.getGenericParameterTypes();获取的是方法参数的类型,里面带有实际的参数类型。 method.getParameterAnnotations();获取的是方法参数上面的注解,是一个二维数组,第一个维度代表的是方法参数对应的下标,比如,一个方法有3个参数,那0代表第一个参数,1代表第二个参数,2代表第三个参数。
@Retention:注解保留的生命周期 @Target:注解对象的作用范围。 创建一个注解遵循: public @interface 注解名 {方法参数}
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface getViewTo { int value() default -1; } public class MainActivity extends AppCompatActivity { @getViewTo(R.id.textview) private TextView mTv; @getViewTo(R.id.button) private Button mBtn; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //通过注解生成View; getAllAnnotationView(); } /** * 解析注解,获取控件 */ private void getAllAnnotationView() { //获得成员变量 Field[] fields = this.getClass().getDeclaredFields(); for (Field field : fields) { try { //判断注解 if (field.getAnnotations() != null) { //确定注解类型 if (field.isAnnotationPresent(GetViewTo.class)) { //允许修改反射属性 field.setAccessible(true); GetViewTo getViewTo = field.getAnnotation(GetViewTo.class); //findViewById将注解的id,找到View注入成员变量中 field.set(this, findViewById(getViewTo.value())); } } } catch (Exception e) { } } } }
反射机制
JAVA反射机制是在运行状态中,对于任意一个类 (class文件),都能够知道这个类的所有属性和方法; 对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。 动态获取类中信息,就是java反射 。可以理解为对类的解剖。
Person
public class Person { private int age; private String name; public Person(String name,int age) { super(); this.age = age; this.name = name; System.out.println("Person param run..."+this.name+":"+this.age); } public Person() { super(); System.out.println("person run"); } public void show(){ System.out.println(name+"...show run..."+age); } private void privateMethod(){ System.out.println(" method run "); } public void paramMethod(String str,int num){ System.out.println("paramMethod run....."+str+":"+num); } public static void staticMethod(){ System.out.println(" static method run......"); } }
要想要对字节码文件进行解剖,必须要有字节码文件对象. 如何获取字节码文件对象呢? 获取Class对象的三种方式
public class ReflectDemo { /** * @param args * @throws ClassNotFoundException */ public static void main(String[] args) throws ClassNotFoundException { getClassObject_3(); } /* * 获取字节码对象的方式: * Object类中的getClass()方法的。 * 想要用这种方式,必须要明确具体的类,并创建对象。 * 麻烦 . * */ public static void getClassObject_1(){ Person p = new Person(); Class clazz = p.getClass(); Person p1 = new Person(); Class clazz1 = p1.getClass(); System.out.println(clazz==clazz1); } /* * 方式二: * 任何数据类型都具备一个静态的属性.class来获取其对应的Class对象。 * 相对简单,但是还是要明确用到类中的静态成员。 * 还是不够扩展。 * */ public static void getClassObject_2() { Class clazz = Person.class; Class clazz1 = Person.class; System.out.println(clazz==clazz1); } /* * 方式三: * 只要通过给定的类的 字符串名称就可以获取该类,更为扩展。 * 可是用Class类中的方法完成。 * 该方法就是forName. * 这种方式只要有名称即可,更为方便,扩展性更强。 */ public static void getClassObject_3() throws ClassNotFoundException { String className = "cn.test.bean.Person"; Class clazz = Class.forName(className); System.out.println(clazz); } }
获取Class中的构造函数
public class ReflectDemo2 { /** * @param args * @throws Exception * @throws InstantiationException * @throws ClassNotFoundException */ public static void main(String[] args) throws ClassNotFoundException, InstantiationException, Exception { createNewObject_2(); } public static void createNewObject_2() throws Exception { // cn.test.bean.Person p = new cn.test.bean.Person("小强",39); /* * 当获取指定名称对应类中的所体现的对象时, * 而该对象初始化不使用空参数构造该怎么办呢? * 既然是通过指定的构造 函数进行对象的初始化, * 所以应该先获取到该构造函数。 通过字节码文件对象即可完成。 * 该方法是:getConstructor(paramterTypes); * */ String name = "cn.test.bean.Person"; //找寻该名称类文件,并加载进内存,并产生Class对象。 Class clazz = Class.forName(name); //获取到了指定的构造函数对 象。 Constructor constructor = clazz.getConstructor(String.class,int.class); //通过该构造器对象的newInstance方法进行对象的初始化。 Object obj = constructor.newInstance("小明",38); } public static void createNewObject() throws ClassNotFoundException, InstantiationException, IllegalAccessException{ //早期:new时候,先根据被new的类的名称找寻该类的字节码文件,并加载进内存, // 并创建该字节码文件对象,并接着创建该字节文件的对应的Person对象. // cn.test.bean.Person p = new cn.test.bean.Person(); //现在: String name = "cn.test.bean.Person"; //找寻该名称类文件,并加载进内存,并产生Class对象。 Class clazz = Class.forName(name); //如何产生该类的对象呢? Object obj = clazz.newInstance(); } }
获取Class中的字段
/* * 获取字节码文件中的字段。 */ public static void getFieldDemo() throws Exception { Class clazz = Class.forName("cn.test.bean.Person"); Field field = null;//clazz.getField("age");//只能获取公有的, field = clazz.getDeclaredField("age");//只获取本类,但包含私有。 //对私有字段的访问取消权限检查。暴力访问。 field.setAccessible(true); Object obj = clazz.newInstance(); field.set(obj, 89); Object o = field.get(obj); System.out.println(o); // cn.test.bean.Person p = new cn.test.bean.Person(); // p.age = 30; }
获取Class中的方法
public static void getMethodDemo_3() throws Exception { Class clazz = Class.forName("cn.test.bean.Person"); Method method = clazz.getMethod("paramMethod", String.class,int.class); Object obj = clazz.newInstance(); method.invoke(obj, "小强",89); } public static void getMethodDemo_2() throws Exception { Class clazz = Class.forName("cn.test.bean.Person"); Method method = clazz.getMethod("show", null);//获取空参数一般方法。 // Object obj = clazz.newInstance(); Constructor constructor = clazz.getConstructor(String.class,int.class); Object obj = constructor.newInstance("小明",37); method.invoke(obj, null); } /* * 获取指定Class中的所有公共函数。 */ public static void getMethodDemo() throws Exception { Class clazz = Class.forName("cn.test.bean.Person"); Method[] methods = clazz.getMethods();//获取的都是公有的方法。 methods = clazz.getDeclaredMethods();//只获取本类中所有方法,包含私有。 for(Method method : methods){ System.out.println(method); } }
如何获得泛型类的真实类型 通过Class类上的 getGenericSuperclass() 或者 getGenericInterfaces() 获取父类或者接口的类型,然后通过ParameterizedType.getActualTypeArguments()
public class RealType<T>{ private Class<T> clazz; // 使用反射技术得到T的真实类型 public Class getRealType(){ // 获取当前new的对象的泛型的父类类型 ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass(); // 获取第一个类型参数的真实类型 this.clazz = (Class<T>) pt.getActualTypeArguments()[0]; return clazz; } }