代理模式
一、简介
1、什么是代理
代理模式:为其他对象提供一种代理以控制对这个对象的访问,也就是创建一个代理对象作为客户端和目标对象之间的中介,主要目的就是保护目标对象或增强目标对象
通过使用代理模式,通常有以下两个优点:
-
可以隐藏被代理类的实现
-
可以实现客户与被代理类间的解耦,在不修改被代理类代码的情况下能够做一些额外的处理
2、分类
Proxy( /ˈprɑːksi/)模式在实际中经常应用,比如Windows系统提供的快捷方式
按照代理的创建时期,代理类可以分为两种。
-
静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了
-
动态代理:在程序运行时,运用反射机制动态创建而成。
3、Proxy模式结构图
代理模式结构图如下:
由三部分组成:
-
Proxy:保存一个引用使得代理可以访问实体。控制对实体的存取,并可能负责创建和删除它,其他功能依赖于代理的类型。
-
Subject:定义RealSubject和Proxy的共用接口,这样就在任何使用RealSubject的地方都可以使用Proxy。
-
RealSubject:定义Proxy所代表的实体。
二、静态代理
若代理类在程序运行前就已经存在,那么这种代理方式被成为 静态代理 ,这种情况下的代理类通常都是我们在Java代码中定义的
所谓的静态代理,就是通过声明一个明确的代理类来访问源对象,一个代理只能服务于一种产品,当有n种产品时,就需要n个代理,这样就不利于业务的发展。
举例:我们有两个接口,Computer和 Phone,每个接口都有一个实现类
1、定义接口(Subject)
代理接口
public interface Computer{ public void sell(); }
2、定义接口实现类(RealSubject)
即委托类
public class HuaWeiComputer implements Computer{ @Override public void sell() { System.out.println("出售华为电脑"); } }
3、定义代理类(Proxy)
代理类,最终还是调用委托类实现业务操作
现在我们要做的就是让代理在调用sell()
前输出一句售前了解
,调用后输出一句售后服务
public class ComputerProxy implements Computer { private Computer computer; public ComputerProxy(Computer computer) { this.computer = computer; } @Override public void sell() { System.out.println("售前了解"); computer.sell(); System.out.println("售后服务"); } }
4、测试
public class Test { public static void main(String[] args) { Computer huaWeiComputer = new HuaWeiComputer(); ComputerProxy computerProxy = new ComputerProxy(huaWeiComputer); computerProxy.sell(); } } -------------------------- 输出: 售前了解 出售华为电脑 售后服务
5、优缺点
Phone和computer一样再实现一遍
静态代理的代码非常简单易懂,这种模式虽好,但是也有明显的缺点:
-
会存在大量冗余的代理类,这里只有两个接口,如果有n个接口,那么就要定义n个代理类。
-
不易维护,一旦接口更改,代理类和被代理类都要更改。
那么这个时候就可以使用动态代理来解决了
三、动态代理
代理类在程序运行时创建的代理方式被成为 动态代理。 也就是说,这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的
指示
动态生成的 ,在程序运行时,运用反射机制动态创建而成代理对象。
代理类在程序运行时创建代理的方式叫动态代理,也就是说代理类并不是在java代码中定义的,而是在运行的时候动态生成的。
1、实现方式
Java的动态代理技术的实现主要有两种方式:
-
JDK原生动态代理:是Java原生支持的,不需要外部依赖,但是它只能基于接口进行代理(需要动态代理的对象必须实现与某个接口)
-
CGLIB动态代理:CGLIB通过继承的方式进行代理,(让需要代理的类成为Enhancer的父类),无论目标对象有没有实现接口都可以代理,但是无法处理Final的情况。
2、JDK动态代理
JDK从1.3版本就开始支持动态代理类的创建。
java.lang.reflect`类库中提供三个类直接支持代理模式:`Proxy`,`InvocationHandler`和`Method
2.1、Proxy类
Proxy
提供用于创建动态代理类和实例的静态方法,它还是由这些方法创建的所有动态代理类的超类。
核心方法newProxyInstance
:返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
参数说明:
loader - 定义代理类的类加载器 interfaces - 代理类要实现的接口列表 h - 指派方法调用的调用处理程序
Ps:类加载器 在Proxy类中的newProxyInstance()方法中需要一个ClassLoader类的实例,ClassLoader实际上对应的是类加载器
2.2、InvocationHandler接口
代理实例的调用处理程序 实现的接口
每个代码实例都具有一个关联的调用处理程序。对代理实例调用方法时,将对方法调用进行编码并将其指派到它的调用处理程序的 invoke
方法。
package java.lang.reflect; public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
参数说明:
Object proxy:在其上调用方法的代理实例。
Method method:要调用的方法
Object[] args:方法调用时所需要的参数
可以将InvocationHandler接口的子类想象成一个代理的最终操作类,替换掉ProxySubject。
2.3、Method
Method
提供关于类或接口上单独某个方法(以及如何访问该方法)的信息。所反映的方法可能是类方法或实例方法(包括抽象方法)。
主要方法invoke
:对带有指定参数的指定对象调用由此 Method 对象表示的基础方法
public Object invoke(Object obj, Object... args)
参数: obj - 从中调用基础方法的对象 args - 用于方法调用的参数
2.4、定义代理类处理程序
public class JDKProxy { public static Object getProxy(Object target) { // 返回代理接口的代理对象,并指定代理对象的调用处理程序为当前对象 return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() { @Override //proxy - 在其上调用方法的代理实例 //method - 代表被调用的方法 //args - 调用方法的参数 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("售前了解"); // 执行方法,底层还是通过委托类对象调用自己的方法执行 Object invoke = method.invoke(target, args); System.out.println("售后服务"); return invoke; } }); } }
当我们通过代理对象调用业务方法时,这个
调用
会转送到invoke方法中,代理类对象作为proxy参数传入,参数method标识了我们具体调用的是代理类的哪个方法,args为这个方法的参数。这样一来,我们对代理类中的所有方法的调用都会变为对invoke的调用,这样我们可以在invoke方法中添加统一的处理逻辑(也可以根据method参数对不同的代理类方法做不同的处理)。
因此我们可以在中介类的invoke方法中实现输出售前了解,再调用被代理类的方法,再输出售后服务。
2.5、执行代码
public class App{ public static void main( String[] args ){ //得到委托类 HuaWeiComputer huaWeiComputer = new HuaWeiComputer(); //得到代理类 Computer proxy = (Computer)JDKProxy.getProxy(huaWeiComputer); //通过代理对象调用方法 proxy.sell(); } }
输出:
售前了解 出售华为电脑 售后服务
可以看到无论多少个接口,只需要一个代理类就可以了。
3、CGLIB动态代理
CGLIB通过继承的方式进行代理,(让需要代理的类成为Enhancer(增强剂)的父类),无论目标对象有没有实现接口都可以代理,但是无法处理Final的情况。
3.1、引入依赖
导入cglib-3.1.jar
和asm-4.2.jar
包
3.2、代理类
package com.woniuxy.proxy; import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; public class CGLIBProxy { public static Object getProxy(Object target) { Enhancer enhancer = new Enhancer(); //设置需要创建子类的类 enhancer.setSuperclass(target.getClass()); //设置回调程序,调用时会执行该程序 enhancer.setCallback(new MethodInterceptor() { @Override // o: cglib 动态生成的代理类的实例 // method:实体类所调用的都被代理的方法的引用 // objects 参数列表 // methodProxy:生成的代理类对方法的代理引用 public Object intercept(Object arg0, Method method, Object[] arg2, MethodProxy arg3) throws Throwable { System.out.println("售前咨询"); Object invoke = method.invoke(target, arg2); System.out.println("售后服务"); return invoke; } }); //创建代理对象 return enhancer.create(); } }
3.2、测试
public class App{ public static void main( String[] args ){ //得到委托类 HuaWeiComputer huaWeiComputer = new HuaWeiComputer(); //得到代理类 Computer proxy = (Computer)CGLIBProxy.getProxy(huaWeiComputer); //通过代理对象调用方法 proxy.sell(); } }
4、JDK代理与CGLIB代理的区别
JDK动态代理实现接口,CGLIB动态继承思想
JDK动态代理(目标对象存在接口时)执行效率高于CIGLIB
如果对象有接口实现,选择JDK代理,如果没有接口实现选择CGILB代理
自定义注解
一、简介
注解(
Annotation
)是Java SE 5.0 版本开始引入的概念,它是对 Java 源代码的说明,是一种元数据(描述数据的数据)
1、Java注解定义
Java注解,是Java语言中引入的一种元数据,是附加在代码中的一些一些特殊标记。它提供了一种将元数据和程序代码组合在一起的方式。用于在编译、类加载、运行时进行解析和使用,并执行相应的处理,起到说明、校验、配置的功能。
元数据从 metadata 一词译来,是描述数据的数据,它不会影响程序的运行,但可以提供额外的信息,例如Java类的版本号、作者、方法的参数名称和类型等。
Java注解有助于将程序元数据与源代码分离开来,从而简化程序设计,并提供了一种在程序运行时动态处理元数据的方式。Java注解就像修饰符一样被使用,并应用于包、类型、构造方法、方法、成员变量、参数、本地变量的声明中。这些信息被存储在注解的 “name=value” 结构对中,从而为程序的设计和实现提供了更多的灵活性和可扩展性。
如果说注释是写给人看的,那么注解就是写给程序看的。它更像一个标签,贴在一个类、一个方法或者字段上。它的目的是为当前读取该注解的程序提供判断依据。
2、Java注解的作用
-
提供更多的程序元数据。Java注解提供了一种将元数据与程序代码组合在一起的方式,从而可以为程序提供更多的信息,例如Java类的版本号、作者、方法的参数名称和类型等。
-
简化程序设计。Java注解可以使程序的设计更加简单,因为它实现了将元数据和程序代码分离开来的目的,从而程序员可以专注于程序代码的设计和实现。
-
提供更好的可读性。通过使用Java注解,程序员可以更加清晰地表达程序的意图和目的,从而增强程序的可读性。
-
实现自动化的代码生成。Java注解可以为程序自动生成一些代码,从而减少程序员的工作量,提高程序的开发效率。
-
提供更加灵活的程序实现。Java注解可以应用到Java类、方法、属性、参数等元素上,从而为程序的设计和实现提供了更加灵活和可扩展的方式
3、Java注解应用
-
生成文档这是最常见的,也是java 最早提供的注解;
-
在编译时进行格式检查,如@Override放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出;
-
跟踪代码依赖性,实现替代配置文件功能,比较常见的是spring 2.5 开始的基于注解配置,作用就是减少配置;
-
在反射的 Class, Method, Field 等函数中,有许多于 Annotation 相关的接口,可以在反射中解析并使用 Annotation。
范围 | 作用 | 常见注解 |
---|---|---|
类和属性 | 描述版本号、作者、属性的名称和类型的元数据 | @param、@return、@author、@version |
方法和参数 | 方法的返回值类型、参数的名称和类型等元数据 | @Override、@Deprecated |
包和模块 | 模块的名称、版本号、依赖关系等元数据 | @Service、@Component |
框架和插件 | 框架的配置文件、插件的依赖关系等元数据 | @Configuration |
测试和文档 | 测试用例的名称、文档的标题等元数据 | @Test |
4、Java注解分类
-
Java自带的标准注解:包括@Override、@Deprecated(用来表示不赞成使用,标记已经过时)、@SuppressWarnings(抑制警告)等,使用这些注解后编译器就会进行检查。
-
元注解:元注解是用于定义注解的注解,包括@Retention、@Target、@Inherited、@Documented、@Repeatable 等。
-
自定义注解:用户可以根据自己的需求定义注解。
package com.woniuxy.annotation; import java.io.Serializable; @SuppressWarnings("serial") public class Person implements Serializable{ /*@Override public void eat() {
}*/ @Override public String toString() { return "hello"; } @Deprecated public void eat() { System.out.println(); } public static void main(String[] args) { Person person = new Person(); person.eat(); }
}
5、元注解
定义注解时使用的注解,称为元注解
@Target,@Retention,@Documented,@Inherited
5.1、@Target
@Target 表示该注解用于什么地方,可能的 ElemenetType 参数包括:
java.lang.annotation.ElementType
参数 | 说明 |
---|---|
CONSTRUCTOR | 构造器声明 |
FIELD | 域声明(包括 enum 实例) |
LOCAL_VARIABLE | 局部变量声明 |
METHOD | 方法声明 |
PACKAGE | 包声明 |
PARAMETER | 参数声明 |
TYPE | 类,接口(包括注解类型)或enum声明 |
5.2、@Retention(保留)
表示在什么级别保存该注解信息。可选的 RetentionPolicy 参数包括:
参数 | 说明 |
---|---|
SOURCE | 源码时保留,将被编译器丢弃 |
CLASS | 编译时保留,将由编译器记录在类文件中,但JVM不需要在运行时保留 |
RUNTIME | 运行时保留,运行中可以处理,因此可以反射性地读取 |
5.3、@Documented
如果用javadoc生成文档时,想把注解也生成文档,就带这个
5.4、 @Inherited
@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。
如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
二、自定义注解
1、语法
(/ˌænəˈteɪʃ(ə)n/)
Annotation是所有注解类型的公共扩展接口。
自定义注解通过使用@interface
来实现,它自动继承了java.lang.annotation.Annotation接口,语法如下:
修饰符 @interface 注解名 { type elementName(); type elementName() default value; }
-
修饰符: 访问修饰符必须为public,不写默认为pubic
-
关键字: 关键字为@interface,并且该注解不能再去继承别的类或是接口
-
注解名称: 要求合法的Java标识符,注解名称为自定义注解的名称,使用时还会用到
-
成员属性:定义为为无参方法的形式
-
成员类型:成员类型只能用基本类型 byte、short、char、int、long、float、double、boolean八种基本数据类型和String、Enum、Class、annotations等数据类型,以及这一些类型的数组,不能为void类型。
-
成员调用:必须要为给所有成员赋值,除非属性指定了默认值
-
value属性:当只有value属性时,调用时可以省略value属性,直接赋值
2、自定义案例
2.1、声明一个注解MyAnnotation
没有加Target之前 ,该注解可以放到各类的各个地方,可以单独创建一个类体现一下,如果只有一个值时{}可省略
Retention保留的生命周期:源码-》Class 文件-》运行时
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.METHOD}) public @interface MyAnnotation { String name() default "tom"; int age(); }
2.2、给Employee类添加注解
import com.woniuxy.annotation.MyAnnotation; @MyAnnotation(name="merry",age=22) public class Employee implements Serializable { /** * 0 */ private static final long serialVersionUID = 1L; private int empId; private String name; private int age; public Employee() { // TODO Auto-generated constructor stub } @MyAnnotation(age=20) public int getEmpId() { return empId; } public int setName(@MyAnnotation String name) { this.name = name; } …… }
2.3、测试
可以分别@Retention为不同值的时候的情况,通过命令
javap -v xxx.class
可以查看字节码文件的变化情况
-
SOURCE
-
CLASS
三、获取注解中内容
如何获取程序中注解中的内容
RetentionPolicy
必须为RUNTIME
1、前提
在Java程序中可以通过反射机制来获取指定程序元素的 Annotation对象,然后通过 Annotation对象 来获取注解里面的元数据。
要获得程序中注解的内容需要注意:
-
某个类标注了注解,那么这个类就是Annotation的一个对象
-
通过反射来获取注解的值时,要求该注解的
RetentionPolicy
必须为RUNTIME,因为java反射是在程序运行时获取字节码的内容。
2、AnnotatedElement
接口
表示当前在此JVM中运行的程序的注解元素, 该界面允许反射读取注解。
Java在java.lang.reflect
包下有一个接口AnnotatedElement
,该接口代表程序中可以接受注解的程序元素,该接口主要有如下几个实现类:
-
Class:类定义
-
Constructor:构造器定义
-
Field:累的成员变量定义
-
Method:类的方法定义
-
Package:类的包定义
AnnotatedElement
接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement
对象之后,程序就可以调用该对象的如下方法来访问Annotation信息:
3.2、常用方法
返回类型 | 方法 | 描述 |
---|---|---|
<T extends Annotation>T | getAnnotation(类<T> annotationClass) | 返回该元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null |
Annotation[] | getAnnotations() | 返回该程序元素上存在的所有注解 |
default <T extends Annotation>T | getDeclaredAnnotation(类<T> annotationClass) | 返回该元素上的指定类型的注解,忽略继承的注解 |
Annotation[] | getDeclaredAnnotations() | 返回直接存在于此元素上的所有注解,忽略继承的注解 |
default boolean | isAnnotationPresent(类<? extends Annotation> annotationClass) | 如果此元素上 存在指定类型的注解,则返回true,否则返回false。 |
3.3、示例
分别得到类和方法上的注解元素上的值
public class Test { public static void main(String[] args) throws ClassNotFoundException { Class cl = Class.forName("com.woniuxy.entity.Employee"); //得到类上的注解信息 //如果此元素上 存在指定类型的注释,则返回true,否则返回false if(cl.isAnnotationPresent(MyAnnotation.class)) { //返回该元素的,如果这样的注释 ,否则返回null指定类型的注释 MyAnnotation ma = (MyAnnotation)cl.getAnnotation(MyAnnotation.class); System.out.println(ma.age()); System.out.println(ma.name()); }else { System.out.println("该类上没有这种类型的注解"); } //得到方法上的注解信息 Method[] methods = cl.getMethods(); for (Method method : methods) { if(method.isAnnotationPresent(MyAnnotation.class)) { System.out.println(method.getName()); MyAnnotation annotation = method.getAnnotation(MyAnnotation.class); System.out.println(annotation.age()); System.out.println(annotation.name()); } } } }