什么是代理?
代理思想就是被代理者没有能力,或者不愿意去完成某件事情,需要找个人(代理)代替自己去完成这件事。
别想的太复杂,就是找一个黄牛。举个最近的例子,周杰伦要开演唱会了,大家需要在大麦上买票,规定买票的规则 是这样的,只能用自己实名制的账号去买票。票和自己的身份证绑定,不能转票。
分析一下: 我们也可以买票,但是我们未必可以抢到,我们能做的仅仅是(1)登录账号,(2)抢票(未必能抢到),(3)退出账号。但是黄牛就比较牛了,别管人家什么手段,后台有人也好,开外挂也好,人家就是可以给你抢到周董的票。而你要做的就是登录和退出账号。我们只需要黄牛把核心的抢票的功能实现即可。这样想不就是很明白了。这就是动态代理的思想。我有这样的功能,但是我做的不好,但是找个代理可以做,我只负责把其他我能做的完成,我完不成的功能交给代理。
如何创建代理对象?
Java中代理的代表类是:java.lang.reflect.Proxy,它提供了一个静态方法,用于为被代理对象,产生一个代理对象返回。
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
为被代理对象返回一个代理对象。
参数一:类加载器 加载代理类,产生代理对象。
参数二:真实业务对象的接口。(被代理的方法交给代理对象)
参数三:代理的核心处理程序。
好我们知道了有这样一个类,这个类中有个一静态方法,供我们去传入参数,然后就可以返回给我们一个黄牛。这就是我们要的结果,那么其中的参数都是什么呢?我们一个一个的介绍
我们知道我们编写的是java文件,通过编译生成.class文件,但是他们都在硬盘中,因为我们都可以看到,那么他是怎么进入到内存中的呢?没错,就是类加载器,通过类加载器把字节码文件加载到内存中。产生字节码文件。奥~这样,那么是所有的类都是一个类加载器吗?不是。
首先介绍在java中给我们提供了三层的类加载器:分别是BootStrap类加载器,PlatformClassLoader类加载器(jdk1.8之前为ExtClassLoader扩展类加载器,下面作介绍),AppClassLoader类加载器。
下面一一的做一个介绍:
BootstrapClassLoader(启动类加载器):主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
(JDK1.9之前)ExtClassLoader(扩展类加载器):主要负责加载Java_Home/lib/ext目录下的一些扩展的jar。
(JDK1.9之后)PlatFormClassLoader(平台类加载器):下面解释
AppClassLoader(应用类加载器):它负责将系统类路径(CLASSPATH)中指定的类库(自定义类)加载到内存中。开发者可以直接使用系统类加载器。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般称为系统(System)加载器。
从JDK1.9+版本提供有一个“PlatformClassLoader”类加载器,而在JDK1.8及以前的版本中提供的加载器为“ ExtClassLoader”,因为在JDK的安装目录中提供了一个ext的目录,开发者可以将*.jar文件拷贝到此目录中,这样就可以直接执行了,但是这样的处理开发并不安全,最初时也是不提倡使用的,所以从JDK1.9开始将其彻底废除,同时为了与系统类加载器和应用类加载器之间保持设计的平衡,提供有平台类加载器。
具体的改变可以参考下面的文章:聊聊java9的classloader - 简书 (jianshu.com)
那如果有一个我们写的Hello.java编译成的Hello.class文件,它是如何被加载到JVM中的呢?看下源码:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// -----??-----
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 首先,检查是否已经被类加载器加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 存在父加载器,递归的交由父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 直到最上面的Bootstrap类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
参考文章以及图片来源:通俗易懂的双亲委派机制_IT烂笔头的博客-CSDN博客
图片是先走蓝色然后再走红色
从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。那么有人就有下面这种疑问了?
双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。
总结一下:双亲委派从下到上的其中的两个原因
- 从下往上首先是为了防止加载过得类再次加载
- 防止用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,导致程序混乱,而由下往上层层的判断,是否加载过,等到了BootStrap类加载器,就可以放心的属于自己的类,这样就不会出现混乱
public class Student {
public static void main(String[] args) {
Class<Student> studentClass = Student.class;
// 获取AppClassLoader
ClassLoader classLoader = studentClass.getClassLoader();
System.out.println(classLoader);
// 获取'父'加载器 PlatFormClassLoader
ClassLoader platForm = classLoader.getParent();
System.out.println(platForm);
// 获取'父'加载器 BootStrapClassLoader
ClassLoader bootStrap = platForm.getParent();
System.out.println(bootStrap);
}
}
jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
jdk.internal.loader.ClassLoaders$PlatformClassLoader@404b9385
null
最后因为BootStrapClassLoader是归jvm调用C语言编写,所以就返回null。
Class>[] interfaces
直接传入代理类实现接口,而且必须有接口
也可以通过字节码文件中的方法返回值得到,当前类的所有的实现接口的字节码文件的集合
studentClass.getInterfaces()
用来指定生成的代理对象要干什么事情
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null
}
});
怎么解释呢,通过案例来理解了就十分的好理解了,上面已经有两个参数的基础了,第三个就好解释了。我们暂且先告别黄牛的案例,先完成一个下面的案例:
有一个Animal接口,猫实现了这个接口,但是我们发现这个猫实现类,功能不是太好,他尽然在吃东西前不洗手,开玩笑这怎么可以,咱们就通过反射 帮它在吃饭之前洗个手。(你可能会说,我使用继承也能解决呀。没错是可以,但是这里是一个方法,那么如果是100个呢?累死了吧,再说继承还是操作原有的类吗,显然不是,操作是自己创建的新的类 ),来吧,看代码
Animal接口
public interface Animal {
// 吃饭的方法
void eat();
// 叫的方法
void cry();
}
Cat类:
public class Cat implements Animal{
@Override
public void eat() {
System.out.println("余之辈正在吃鱼~~"); // 没错,我的猫叫余之辈
}
@Override
public void cry() {
System.out.println("余之辈正在嗷嗷的叫~~");
}
}
创建一个类,使用动态代理帮 薛定谔的猫 吃饭之前洗个手
public class CatProxyClass {
public static void main(String[] args) {
// 帮你增强我肯定要获得你的对象
Cat cat = new Cat();
// 使用JDK提供的类获取到代理对象
Animal animal = (Animal)Proxy.newProxyInstance(
// 这个上面说过,这里不作赘述,但是如果大家思考,
// 这里是不是只要是获得一个APPClassLoader就可以,那么通过Student的对象获取理论上来说也是可以
cat.getClass().getClassLoader(),
// 两种方式一种是 cat.getClass().getInterfaces(),另一种是下面的
new Class[]{Animal.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 判断如果这个方法的名字叫做eat,那么就在执行执行给他洗个手
if ("eat".equals(method.getName())) {
System.out.println("先给余之辈洗个爪爪");
return method.invoke(cat, args);
}
return null;
}
}
);
// 调用方法
animal.eat();
animal.cry();
}
}
控制台结果:首先调用的时候是多态,大家应该可以看懂吧,编译看父类运行的时候看子类。这里就是代理对象的方法,但是因为我们没有调用所以就没有输出。
先给余之辈洗个爪爪 // 增强
余之辈正在吃鱼~~ // 原方法
可能大家还是疑惑,那么下面就开始做一个解释:
这里面有亿点绕,我们在学习反射的时候可以感受,都是反着来的,之前是对象调用方法,现在是方法对象调用invoke()传入对象。
另外可能也会发现,这里的第三个参数的第一个参数没有使用 Object Proxy。在这里说明一下,这个参数代表着当前代理类的对象,具体的我看网上使用的用处是作为链式编程。如果感兴趣可以去搜一下。这里也不在赘述了,因为目前用不到,脑子转不过来了
那么如果方法有返回值,直接return即可,比如说对cry做一个修改
控制台结果如下:
先给余之辈洗个爪爪
余之辈正在吃鱼~~
jack正在嗷嗷的叫~~
刚才cat实现了Animal接口,但是现在有一个需求,我想让我的猫余之辈,学习一下鱼会的游泳。并且拥有游泳的功能。代码如下:
两个接口:Animal接口 Fish接口
public interface Animal {
// 吃饭的方法
void eat();
// 叫的方法
void cry();
}
public interface Fish {
// 游泳
void swimming();
}
然后我让Cat实现这两个接口
public class Cat implements Animal,Fish{
@Override
public void eat() {
System.out.println("余之辈正在吃鱼~~"); // 没错,我的猫叫余之辈
}
@Override
public void cry() {
System.out.println("余之辈正在嗷嗷的叫~~");
}
@Override
public void swimming() {
System.out.println("余之辈通过后天的学习,终于学会了游泳");
}
}
(CatProxyClass)代理类要做的还是在吃饭之前洗手的功能: 注解就删了,上面有详细的注解
public class CatProxyClass {
public static void main(String[] args) {
Cat cat = new Cat();
Object object = Proxy.newProxyInstance(
cat.getClass().getClassLoader(),
new Class[]{Animal.class,Fish.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("eat".equals(method.getName())) {
System.out.println("先给余之辈洗个爪爪");
return method.invoke(cat, args);
}
return method.invoke(cat,args);
}
}
);
// 强制类型转换,再调用自己独有的方法,这就体现出了多态的好处
Fish cat1 = (Fish)object;
Animal cat2 = (Animal) object;
cat1.swimming();
cat2.eat();
cat2.cry();
}
}
控制台结果:
余之辈通过后天的学习,终于学会了游泳
先给余之辈洗个爪爪
余之辈正在吃鱼~~
余之辈正在嗷嗷的叫~~
另外如果还有另一个需求,在所有的Animal方法执行之前,先进行一个特殊的操作<给余之辈洗手的就不加了>,那么该怎么办呢?总不能再去一个一个的通过方法名去判断吧?一两个还可以,万一Animal有一百个方法,直接废了,其实我们想想,代理类的第三个参数是按照接口中的方法,挨个执行,那么我们只需要判断这个方法是不是属于这个接口不就ok了吗?实现代码如下
改造后的CatProxyClass 代码:
public class CatProxyClass {
public static void main(String[] args) {
Cat cat = new Cat();
Object object = Proxy.newProxyInstance(
cat.getClass().getClassLoader(),
new Class[]{Animal.class,Fish.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取这个方法所实现的接口的字节码,会调用本身的toString方法
if (method.getDeclaringClass().equals(Animal.class)){
// 说明是Animal的方法,这样就可以在方法执行之前进行处理
System.out.println("Animal方法执行前的一个特殊的操作");
return method.invoke(cat,args);
}
return method.invoke(cat,args);
}
}
);
Fish cat1 = (Fish)object;
Animal cat2 = (Animal) object;
cat1.swimming();
cat2.eat();
cat2.cry();
}
}
控制台输出: 在Animal的方法之前就都加了这个特殊的操作
余之辈通过后天的学习,终于学会了游泳
Animal方法执行前的一个特殊的操作
余之辈正在吃鱼~~
Animal方法执行前的一个特殊的操作
余之辈正在嗷嗷的叫~~
另外除了method.getDeclaringClass().equals(Animal.class)
判断是否为这个类的方法,也可以通过比较Method对象toString之后的字符串是否包含对应的接口,如下:
if (method.toString().contains("Animal")){
// 说明是Animal的方法,这样就可以在方法执行之前进行处理
System.out.println("Animal方法执行前的一个特殊的操作");
return method.invoke(cat,args);
}
可以打印一下method对象:所以可以通过字符串操作
public abstract void com.yfs1024.test02.Fish.swimming()
public abstract void com.yfs1024.test02.Animal.eat()
public abstract void com.yfs1024.test02.Animal.cry()
定义一个people接口,拥有登录,买票和退出功能
public interface People {
// 登录
void login();
// 买票
void buyTicket();
// 退出
void loginOut();
}
定义My类,实现People接口
public class My implements People{
@Override
public void login() {
System.out.println("输入我的密码");
}
@Override
public void buyTicket() {
System.out.println("抢票~~~~~");
}
@Override
public void loginOut() {
System.out.println("退出我的账号");
}
}
定义代理类,实现功能的增强
public class ProxyClass {
public static void main(String[] args) {
My my = new My();
People yellowOx = (People)Proxy.newProxyInstance(
my.getClass().getClassLoader(),
my.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 仅仅对买票的功能进行处理,不使用自己的功能,完全交给黄牛
if ("buyTicket".equals(method.getName())){
System.out.println("在黄牛的一通暗箱操作操作下,抢到周杰伦演唱会的门票~");
return null; // 返回null 让功能完全有黄牛完成
}
// 其他方法不作操作
return method.invoke(my,args);
}
}
);
yellowOx.login(); // 这里看似是用的代理的对象的调用的login方法,但是实际上还是自己的方法
yellowOx.buyTicket();
yellowOx.loginOut();
}
}
控制台结果:
输入我的密码
在黄牛的一通暗箱操作操作下,抢到周杰伦演唱会的门票~
退出我的账号
在上面我们的几个简单的案例可能已经使大家很好的理解了动态代理,那么下面就通过我们使用的日志,我们再来理解和学习一下动态代理。下面就模拟 **Logback ** <是在log4j的基础上开发,因为不满log4j的功能>,来处理日志的控制台打印,及存储日志信息到指定的本地路径。
好下面作一个简单的分析,首先我们要知道日志分为五个级别(由低到高):分别是trace,debug,info,warn,error。具体的作用在下面的我的笔记里面有描述,如果需要可以参考学习;
java中的特殊文件、日志技术、多线程入门_yfs1024的博客-CSDN博客
首先我们需要两个最基本的数据:一个是日志的级别,另一个是把日志存储的位置
定义一个配置文件:logback.properties
# 配置存储的日志的最低级别
level=debug
# 配置存储的位置
path=D:\\log\\my_log.log
定义一个Logger接口:trace级别太细了,基本不用。所以这里就演示四个
public interface Logger {
void debug(String debug);
void info(String info);
void warn(String warn);
void error(String error);
}
定义一个实现类LoggerImpl:分析每个方法当执行的时候应该做什么
- 记载配置文件
- 判断当前级别是比配置文件中配置的级别大,还是小
- 根据判断的结果,将日志的信息,保存到指定的log文件中
那么我们会发现每个方法都要这样做,假如我们一个一个的做了,还是和上面说的一样,一个两个还好,一下子那么多,每个都要重复做,那么就别做了,找个代理让代理来做。
public class LoggerImpl implements Logger {
@Override
public void debug(String debug) {
}
@Override
public void info(String info) {
}
@Override
public void warn(String warn) {
}
@Override
public void error(String error) {
}
}
我们在用Logback时是下面的方法获得一个LOGGER对象,那么我们也模仿他这么做
public static final Logger LOGGER = LoggerFactory.getLogger("类名");
自己也写一个LoggerFactory,思路可以按照代码中的顺序思考。
public class LoggerFactory {
// 在这个类中要做的就是通过级别,进行对日志信息的处理
private static Map<String, Integer> map = new HashMap<>();
private static Properties properties = new Properties();
// (3)在静态代理块中对基本的信息进行初始化
static {
//放到外面扩大范围
map.put("debug", 1);
map.put("info", 2);
map.put("warn", 3);
map.put("error", 4);
// 加载配置文件到内存中
try {
properties.load(new FileInputStream("javaEE-day14/logback.properties"));
} catch (IOException e) {
e.printStackTrace();
}
}
// (1)按照Logback的调用,我们也创建一个静态方法,在方法中进行处理
public static Logger getLogger() {
Logger Logger = (Logger) Proxy.newProxyInstance(
LoggerFactory.class.getClassLoader(),
new Class[]{Logger.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// (2)对每个调用的方法进行处理,到这里就发现问题了,我们怎么比较级别呢?只有方法名,那么怎么获取级别呢?
// 哎,用map是不是就可以,我们把方法名设置为key,其对应的大小就是他们的等级。这样解决了,因为是静态方法,所以我们把
// 对map的创建放入到一个静态的方法中,那么静态代码块是不是最合适
// (4) 有了map就可以写了, 问题又来了,我给谁比较呢?在配置文件中呀,所以我们还用通过操作IO来加载配置文件中的信息
if (map.get(method.getName()) >= map.get(properties.getProperty("level"))) {
// 说符合打印控制台和写入log文件中的级别,信息呢就是这里的args,只有一个参数所以获取第一个
// 我们也可以对日志信息做一些补充,这里就打印日志时间,级别,和记录的信息了
StringBuilder logMessage =
new StringBuilder(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).append("-").append(method.getName()).append("-").append(args[0]);
//打印到控制台
System.out.println(logMessage);
// 写入日志
BufferedWriter bw = new BufferedWriter(new FileWriter(properties.getProperty("path"), true));
bw.write(logMessage + "");
bw.newLine();
bw.close();
}
return null;
}
}
);
return Logger;
}
}
最后定义一个方法,测试一下代码:
public class TestMyselfLogback {
private static Logger logger = LoggerFactory.getLogger();
public static void main(String[] args) {
logger.debug("编码时候的错误");
logger.info("一般信息");
logger.warn("警告信息");
logger.error("错误信息");
}
}
控制台结果:
2023-03-16 21:54:46-debug-编码时候的错误
2023-03-16 21:54:46-info-一般信息
2023-03-16 21:54:46-warn-警告信息
2023-03-16 21:54:46-error-错误信息
配置的路径中也会生成对应的信息;
到此这个笔记也就结束了,在写笔记的时候,自己也查阅了很多资料,当然中间也是问题不断,但在笔记中基本把我学习过程中产生的疑问都解决了,重新的认识了动态代理真的很香,很强大。但可能有的地方描述的还不是很正确和完善,甚至可能和java的原本设计有出入,也会有很多手误打错的地方,如果您在后续的学习中发现了,还望纠正
路虽远,行则将至