设计模式之代理模式

news/2025/2/7 7:24:16/

文章目录

    • 1、代理模式基本介绍
    • 2、Jdk中的动态代理
      • 2.1、场景推导
      • 2.2、Jdk动态代理
    • 3、静态代理
    • 4、代理模式的关键点
    • 5、代理模式和适配器模式的比较
    • 6、代理模式UML图

1、代理模式基本介绍

代理模式的定义:

  • 为其他对象提供一种代理以控制对这个对象的访问
  • 在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

简单来说

  • 代理模式就是代理对象具备真实对象的功能,并代替真实对象完成相应操作,并能够在操作执行的前后,对操作进行增强处理

  • 为真实对象提供代理,然后供其他对象通过代理访问真实对象

2、Jdk中的动态代理

2.1、场景推导

实现一个简单的加减乘除运算功能

interface ICalc{int add(int a,int b);int sub(int a,int b);int mul(int a,int b);int div(int a,int b);
}
class  CalcImpl implements ICalc{@Overridepublic int add(int a, int b) {return a+b;}@Overridepublic int sub(int a, int b) {return a-b;}@Overridepublic int mul(int a, int b) {return a*b;}@Overridepublic int div(int a, int b) {return a/b;}
}
class AppTest{public static void main(String[] args) {CalcImpl c = new CalcImpl();System.out.println(c.add(4,2));System.out.println(c.sub(4,2));System.out.println(c.mul(4,2));System.out.println(c.div(4,2));}
}

现在变化来了,客户要求为每个方法添加日志,记录方法开始和结束的时机

package com.hh.demo.designpattern;interface ICalc{int add(int a,int b);int sub(int a,int b);int mul(int a,int b);int div(int a,int b);
} 
class  CalcImpl implements ICalc{@Overridepublic int add(int a, int b) {System.out.println("add方法开始!" +"a="+a+"b="+b);int r = a+b;System.out.println("add方法结束!" +"r="+r);return r;}@Overridepublic int sub(int a, int b) {System.out.println("sub方法开始!" +"a="+a+"b="+b);int r = a-b;System.out.println("sub方法结束!" +"r="+r);return r;}@Overridepublic int mul(int a, int b) {System.out.println("mul方法开始!" +"a="+a+"b="+b);int r = a*b;System.out.println("mul方法结束!" +"r="+r);return r;}@Overridepublic int div(int a, int b) {System.out.println("div方法开始!" +"a="+a+"b="+b);int r = a/b;System.out.println("div方法结束!" +"r="+r);return r;}
}
class AppTest{public static void main(String[] args) {CalcImpl c = new CalcImpl();System.out.println(c.add(4,2));System.out.println(c.sub(4,2));System.out.println(c.mul(4,2));System.out.println(c.div(4,2));}
}

梭哈搞定,打完收工!

我们发现,这样完成业务根本不是一个好办法

  1. 代码在重复,核心业务(加减乘除)和非核心业务(打印日志)在不断的重复。
  2. 如果Icalc和CalcImpl不是我们自己创建的,是被发现的,那我们手里是没有源代码的,不能直接修改源代码(开闭原则)
  3. 需求如果再次变化,需要加入开方,求余的过程 ;又或者客户要求 上午需要日志,下午不需要日志!!!

我们尝试使用动态代理来完成上述功能

2.2、Jdk动态代理

Jdk动态代理:在程序的执行过程中,使用jdk的反射机制,创建代理对象,并动态的指定代理的目标类

在这里插入图片描述

先来看看业务逻辑是怎么实现的

package com.hh.demo.designpattern;interface ICalc {int add(int a, int b);int sub(int a, int b);int mul(int a, int b);int div(int a, int b);
}
class CalcImpl implements ICalc {@Overridepublic int add(int a, int b) {int r = a + b;return r;}@Overridepublic int sub(int a, int b) {int r = a - b;return r;}@Overridepublic int mul(int a, int b) {int r = a * b;return r;}@Overridepublic int div(int a, int b) {int r = a / b;return r;}
}
//调用处理器
class MyHandler implements InvocationHandler {//关联private ICalc calculator;public MyHandler(ICalc calculator) {this.calculator = calculator;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println(method.getName()+"开始,参数:"+ Arrays.toString(args));//利用反射机制,调用方法//把method所代表的方法,当作calculator对象的调用,参数是args//Object是为了通用Object res = method.invoke(calculator, args);System.out.println(method.getName()+"结束,结果是:"+ res);return res;//这个返回值会返回到代理对象的方法调用处}
}
public class AppTest {public static void main(String[] args) {ICalc calculator = new CalcImpl();//当前类的字节码获得当前类的类加载器ClassLoader classLoader = AppTest.class.getClassLoader();//创建代理对象,需要传入三个参数ICalc proxy = (ICalc) Proxy.newProxyInstance(classLoader, new Class[]{ICalc.class}, new MyHandler(calculator));//总之,对代理对象的方法的调用,都统统会进入调用处理器中proxy.add(3, 2);proxy.sub(3, 2);proxy.mul(3, 2);proxy.div(3, 2);}/*** add开始,参数:[3, 2]* add结束,结果是:5* sub开始,参数:[3, 2]* sub结束,结果是:1* mul开始,参数:[3, 2]* mul结束,结果是:6* div开始,参数:[3, 2]* div结束,结果是:1** Process finished with exit code 0*/
}

我们先来看看动态代理api

Proxy.newProxyInstance();

里面传三个参数,分别是:

image-20221005161215388

  • 第一个参数:

    • 实例化一个对象,必然会调用类的构造器。在调用构造器之前,Jvm会加载该类的字节码,而Jvm就是使用类加载器来加载类的字节码,这一步是Jvm自动完成的。

    • 简单来说:只要实例化对象,一定要加载类的字节码,加载字节码就一定要类的加载器。

    • 使用动态代理的api实例化对象是一种不常用的创建对象的方式,但这也是一种实例化,需要我们手动把类的加载器传入

    • 使用构造器实例化对象时Jvm会自动找到类加载器。

  • 第二个参数:

    • 第一个参数传入的类加载器,加载的是哪个类的字节码?加载的字节码就是在运行期动态生成的字节码,这个动态生成的字节码是不需要源代码的。

    • 字节码确实可以自动生成,那么动态代理api成成的字节码的内容,是根据什么生成的呢?恰恰是根据第二个参数生成的。动态生成代理,会生成一个实现了目标接口的类的字节码,在上面的栗子中就是生成了一个ICalc接口的类的字节码!

  • 第三个参数:调用处理器 InvocationHandler

    • 我们已经知道,动态代理会加载自己动态生成的字节码,且这个字节码是根据某个接口生成的,在上面的例子中就是根据ICalc接口生成的实现了ICalc接口的类的字节码
    • 实现一个接口,就要实现其中的抽象方法,那麽动态代理生成的字节码,实现了ICalc接口,必然就要实现其中的add、sub等方法
    • 这些方法被实现的方法体是什么内容呢?这恰恰是由第三个参数决定的,MyHandler类的 invoke方法,就是方法体的内容!!
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {第一个参数:动态代理的对象第二个参数:调用的接口方法第三个参数:调用的接口方法的参数}

但是目前这个写法还是有缺点的,太复杂了,对于新手不是很友好,我们来封装一下

package com.hh.demo.designpattern;interface ICalc {int add(int a, int b);int sub(int a, int b);int mul(int a, int b);int div(int a, int b);
}
class CalcImpl implements ICalc {@Overridepublic int add(int a, int b) {int r = a + b;return r;}@Overridepublic int sub(int a, int b) {int r = a - b;return r;}@Overridepublic int mul(int a, int b) {int r = a * b;return r;}@Overridepublic int div(int a, int b) {int r = a / b;return r;}
}
class MyHandler implements InvocationHandler {//关联private Object target;public MyHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println(method.getName()+"开始,参数:"+ Arrays.toString(args));//利用反射机制,调用方法//把method所代表的方法,当作calculator对象的调用,参数是args//Object是为了通用Object res = method.invoke(target, args);System.out.println(method.getName()+"结束,结果是:"+ res);return res;//这个返回值会返回到代理对象的方法调用处}
}
//----------------------------------------------------------------------------------------------------
class MyProxy{//封装:对外隐藏复杂的实现细节,暴露出简单的使用方法public Object getProxy(Object target){//当前类的字节码获得当前类的类加载器ClassLoader classLoader = MyProxy.class.getClassLoader();//获取target所属的类,所实现的接口Class<?>[] interfaces = target.getClass().getInterfaces();//创建代理对象,需要传入三个参数Object proxy =  Proxy.newProxyInstance(classLoader,interfaces, new MyHandler(target));return proxy;}
}
public class AppTest {public static void main(String[] args) {ICalc calculator = new CalcImpl();ICalc proxy = (ICalc) new MyProxy().getProxy(calculator);proxy.add(3, 2);proxy.sub(3, 2);proxy.mul(3, 2);proxy.div(3, 2);}/*** add开始,参数:[3, 2]* add结束,结果是:5* sub开始,参数:[3, 2]* sub结束,结果是:1* mul开始,参数:[3, 2]* mul结束,结果是:6* div开始,参数:[3, 2]* div结束,结果是:1** Process finished with exit code 0*/
}

MyProxy类对外隐藏复杂的实现细节,暴露出简单的使用方法。似乎有点代理的意思了

目前看起来似乎挺好的,但是仍然有问题:

目前我们创建的代理对象,只能在真实对象的真实方法调用前后加上日志,无法扩展其他功能,比如,用户不想加日志功能,而是想加缓存功能,或者权限控制…

再次封装代码,我们定义一个接口,用来描述代理类对应方法执行前后需要拓展执行的方法。

称这个接口为Interceptor拦截器,也可以理解为切面

package com.hh.demo.designpattern;interface ICalc {int add(int a, int b);int sub(int a, int b);int mul(int a, int b);int div(int a, int b);
}
class CalcImpl implements ICalc {@Overridepublic int add(int a, int b) {int r = a + b;return r;}@Overridepublic int sub(int a, int b) {int r = a - b;return r;}@Overridepublic int mul(int a, int b) {int r = a * b;return r;}@Overridepublic int div(int a, int b) {int r = a / b;return r;}
}
class MyHandler implements InvocationHandler {//关联private Object target;private Interceptor interceptor;public MyHandler(Object target,Interceptor interceptor) {this.target = target;this.interceptor = interceptor;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//方法执行前的钩子函数interceptor.before(target, method, args);Object res = method.invoke(target, args);//方法执行后的钩子函数interceptor.after(target, method, args, res);// 返回到代理对象的方法调用处return res;}
}
class MyProxy{public Object getProxy(Object target, Interceptor interceptor){//当前类的字节码获得当前类的类加载器ClassLoader classLoader = MyProxy.class.getClassLoader();//获取target所属的类,所实现的接口Class<?>[] interfaces = target.getClass().getInterfaces();//创建代理对象,需要传入三个参数Object proxy =  Proxy.newProxyInstance(classLoader,interfaces, new MyHandler(target,interceptor));return proxy;}
}
interface Interceptor {//前置通知void before(Object target, Method method, Object[] args);//后置通知void after(Object target, Method method, Object[] args, Object returnVal);
}
//----------------------------------------------------------------------------------------------------
//用户制作拦截器的实现类
class LogInterceptor implements Interceptor{@Overridepublic void before(Object target, Method method, Object[] args) {System.out.println(String.format("方法名为:%s,参数为:%s", method.getName(), Arrays.toString(args)));}@Overridepublic void after(Object target, Method method, Object[] args, Object returnVal) {System.out.println(String.format("返回结果为:%s", returnVal.toString()));}
}
public class AppTest {public static void main(String[] args) {ICalc calculator = new CalcImpl();ICalc proxy = (ICalc) new MyProxy().getProxy(calculator,new LogInterceptor());proxy.add(3, 2);proxy.sub(3, 2);proxy.mul(3, 2);proxy.div(3, 2);}/*** 方法名为:add,参数为:[3, 2]* 返回结果为:5* 方法名为:sub,参数为:[3, 2]* 返回结果为:1* 方法名为:mul,参数为:[3, 2]* 返回结果为:6* 方法名为:div,参数为:[3, 2]* 返回结果为:1** Process finished with exit code 0*/
}

这样就简单了很多,应对不同的需求我们就去定制不同的代理类和拦截器,实现不同的需求

可是现在变化又来了,客户有了新需求:

针对ICalc接口的日志功能,add方法使用中文日志,sub方法使用英文日志,mul方法和div方法不要日志。

这时只能用判断来解决了,针对不同方法有不同的日志。我们来实现一下

//用户制作拦截器的实现类
class LogInterceptor implements Interceptor{@Overridepublic void before(Object target, Method method, Object[] args) {if("add".equals(method.getName())){System.out.println(String.format("方法名为:%s,参数为:%s", method.getName(), Arrays.toString(args)));}else if("sub".equals(method.getName())){System.out.println(String.format("methodName is:%s,parameter is:%s", method.getName(), Arrays.toString(args)));}else{System.out.println(method.getName()+Arrays.toString(args));}}@Overridepublic void after(Object target, Method method, Object[] args, Object returnVal) {if("add".equals(method.getName())){System.out.println(String.format("返回结果为:%s", returnVal.toString()));}else if("sub".equals(method.getName())){System.out.println(String.format("result is:%s", returnVal.toString()));}else{System.out.println(returnVal.toString());}}
}

运行结果:

方法名为:add,参数为:[3, 2]
返回结果为:5
methodName is:sub,parameter is:[3, 2]
result is:1
mul[3, 2]
6
div[3, 2]
1Process finished with exit code 0

可以看到,虽然做虽然满足了客户的需求,但是有很多的 if else ,感觉好像怪怪的!!!

仔细想想,这设计违反了什么设计原则呢?单一职责设计原则

那就拆分呗,设计原则不就是讲究一个字吗? 我们针对于四个方法,做四个拦截器,这里写两个作为演示

//用户制作拦截器的实现类
class  addInterceptor implements Interceptor{@Overridepublic void before(Object target, Method method, Object[] args) {if("add".equals(method.getName())){System.out.println(String.format("方法名为:%s,参数为:%s", method.getName(), Arrays.toString(args)));}}@Overridepublic void after(Object target, Method method, Object[] args, Object returnVal) {if("add".equals(method.getName())) {System.out.println(String.format("返回结果为:%s", returnVal.toString()));}}
}
class  subInterceptor implements Interceptor{@Overridepublic void before(Object target, Method method, Object[] args) {if("sub".equals(method.getName())){System.out.println(String.format("methodName is:%s,parameter is:%s", method.getName(), Arrays.toString(args)));}}@Overridepublic void after(Object target, Method method, Object[] args, Object returnVal) {if("sub".equals(method.getName())){System.out.println(String.format("result is:%s", returnVal.toString()));}}
}

客户端:

public class AppTest {public static void main(String[] args) {//calculator是目标对象ICalc calculator = new CalcImpl();//根据目标对象calculator,动态生成一个代理对象ICalc proxy = (ICalc) new MyProxy().getProxy(calculator,new addInterceptor());proxy.add(3, 2);proxy.sub(3, 6);}/*** 方法名为:add,参数为:[3, 2]* 返回结果为:5** Process finished with exit code 0*/
}

但是问题又来了

现在,getProxy方法只能传addInterceptor 或者subInterceptor,不能同时用啊

有同学可能想到用可变参数解决这个问题,没错,是可以的;

但是换一种思路,既然能根据目标对象动态代理生成一个代理对象,那我是不是可以将这个代理对象再当成一个新的目标对象动态代理一下?等等,感觉cpu要烧了!!!!

public class AppTest {public static void main(String[] args) {//calculator是目标对象ICalc calculator = new CalcImpl();//根据目标对象calculator,动态生成一个代理对象ICalc proxy = (ICalc) new MyProxy().getProxy(calculator,new addInterceptor());//我们把proxy这个代理对象,再当成一个新的目标对象ICalc proxy2 = (ICalc) new MyProxy().getProxy(proxy,new subInterceptor());//我们发现add方法和sub方法都能拦截proxy2.add(3, 2);proxy2.sub(3, 6);}/*** 方法名为:add,参数为:[3, 2]* 返回结果为:5* methodName is:sub,parameter is:[3, 6]* result is:-3** Process finished with exit code 0*/
}

简单理解就是套娃,贴一张图帮助理解

在这里插入图片描述

现在代码总没有问题了吧?是吗?那我说目前代码还有问题呢?

问题是:添加拦截器的顺序是逆向的,对用户不友好

解决方法:

public class AppTest {public static void main(String[] args) {//calculator是目标对象ICalc calculator = new CalcImpl();List<Interceptor> interceptors = new ArrayList<>();interceptors.add(new addInterceptor());interceptors.add(new subInterceptor());for (int i =interceptors.size() - 1; i >=0; i--) {Interceptor interceptor = interceptors.get(i);calculator = (ICalc) MyProxy.getProxy(calculator, interceptor);}calculator.add(1,2);calculator.sub(3,2);}/***     方法名为:add,参数为:[1, 2]*     返回结果为:3*     methodName is:sub,parameter is:[3, 2]*     result is:1**     Process finished with exit code 0*/
}

但是现在客户端代码很复杂了,对用户不友好呀

我们封装一下倒叙添加拦截器的逻辑

class MyProxy{public static Object getProxy(Object target, Interceptor interceptor){//当前类的字节码获得当前类的类加载器ClassLoader classLoader = MyProxy.class.getClassLoader();//获取target所属的类,所实现的接口Class<?>[] interfaces = target.getClass().getInterfaces();//创建代理对象,需要传入三个参数Object proxy =  Proxy.newProxyInstance(classLoader,interfaces, new MyHandler(target,interceptor));return proxy;}//封装倒叙添加拦截器public static Object getProxy2(Object target, List<Interceptor> interceptors){for (int i =interceptors.size() - 1; i >=0; i--) {Interceptor interceptor = interceptors.get(i);target = (ICalc) MyProxy.getProxy(target, interceptor);}return  target;}
}

客户端代码:

public class AppTest {public static void main(String[] args) {//calculator是目标对象ICalc calculator = new CalcImpl();List<Interceptor> interceptors = new ArrayList<>();interceptors.add(new addInterceptor());interceptors.add(new subInterceptor());ICalc proxy2 = (ICalc) MyProxy.getProxy2(new CalcImpl(), interceptors);proxy2.add(1,2);proxy2.sub(3,2);}/***     方法名为:add,参数为:[1, 2]*     返回结果为:3*     methodName is:sub,parameter is:[3, 2]*     result is:1**     Process finished with exit code 0*/
}

现在还有问题,以后,用户要添加拦截器,删除拦截器,必然要修改应用程序代码,要修改 List,这不合理鸭

用户应该是改配置而不是改代码,所以这个interceptors根本不用传

//封装倒叙添加拦截器
public static Object getProxy2(Object target) throws Exception{//拦截器集合不是用户传进来的,是读取配置文件得到的,配置文件放在同一个包下Properties prop = new Properties();InputStream in = MyProxy.class.getResourceAsStream("myconfig.properties");prop.load(in);String str = prop.getProperty("interceptors");String[] split = str.split(",");List<Interceptor> interceptors = new ArrayList<>();for (String hh : split) {interceptors.add((Interceptor)Class.forName(hh).newInstance());}for (int i =interceptors.size() - 1; i >=0; i--) {Interceptor interceptor = interceptors.get(i);target = (ICalc) MyProxy.getProxy(target, interceptor);}return  target;
}

客户端:

public static void main(String[] args) throws Exception {ICalc proxy2 = (ICalc) MyProxy.getProxy2(new CalcImpl());proxy2.add(1,2);proxy2.sub(3,2);}

客户端代码变得非常简洁

3、静态代理

业务场景:现在需要做一个图书解析器,解析一本书里面有多少个句子,多少个副词

package com.hh.demo.designpattern;
//图书解析器
class BookParser{//接收一本书的内容,字符串的值,是很大的private String content = "天下大事,分久必合,合久必分...!!";public Integer numberOfSentence(){//每次解析,都有很高的执行代价return content.split("[.!?]").length;}public Integer numberOfVerb() throws InterruptedException {//假设执行了很多逻辑;Thread.sleep(1000);return 80;}public Integer numberOfAdverb() throws InterruptedException {//假设执行了很多逻辑;Thread.sleep(1000);return 220;}
}public class AppTest {public static void main(String[] args) throws InterruptedException {BookParser bp = new BookParser();Integer a = bp.numberOfAdverb();System.out.println("有"+ a + "个副词");Integer a2 = bp.numberOfAdverb();System.out.println("有"+ a2 + "个副词");Integer a3 = bp.numberOfAdverb();System.out.println("有"+ a3 + "个副词");}/*** //每隔一秒出现一个结果* 有220个副词* 有220个副词* 有220个副词** Process finished with exit code 0*/}

现在有个问题,每解析一次就要花费1s, 这是极其不合理的;

我们可以做一个代理,每次调方法时进入代理,代理判断一下这个数字有没有统计过,如果统计过了,直接返回这个值,就不用去调用真实对象,如果没有统计过,就去调真实对象,然后返回值,并将这个值存到缓存中;

//图书解析器
class BookParser{//接收一本书的内容,字符串的值,是很大的private String content = "天下大事,分久必合,合久必分...!!";public Integer numberOfSentence(){//每次解析,都有很高的执行代价return content.split("[.!?]").length;}public Integer numberOfVerb() throws InterruptedException {//假设执行了很多逻辑;Thread.sleep(1000);return 80;}public Integer numberOfAdverb() throws InterruptedException {//假设执行了很多逻辑;Thread.sleep(1000);return 220;}
}
class BookParserProxy extends BookParser{//因为没有定义接口,所以为了与真实对象有相同的方法,继承一下BookParserprivate Integer numberOfSentence;private Integer numberOfVerb;private Integer numberOfAdverb;@Overridepublic Integer numberOfSentence() {if(numberOfSentence == null){numberOfSentence = super.numberOfSentence();}return numberOfSentence;}@Overridepublic Integer numberOfVerb() throws InterruptedException {if(numberOfVerb == null){numberOfVerb = super.numberOfVerb();}return numberOfVerb;}@Overridepublic Integer numberOfAdverb() throws InterruptedException {if(numberOfAdverb == null){numberOfAdverb = super.numberOfAdverb();}return numberOfAdverb;}
}
public class AppTest {public static void main(String[] args) throws InterruptedException {BookParser bp = new BookParserProxy();Integer a = bp.numberOfAdverb();System.out.println("有"+ a + "个副词");Integer a2 = bp.numberOfAdverb();System.out.println("有"+ a2 + "个副词");}/*** //等待一秒后两个结果同时出现,说明什么,说明第二次没有调用真实方法* 有220个副词* 有220个副词** Process finished with exit code 0*/}

这个就叫做静态代理,自己手写的代码,写死的代理类

而动态代理是运行时动态的生成字节码

4、代理模式的关键点

  1. 代理对象,一定与目标对象有相同的接口。这样才能做代理
  2. 代理对象中,一定有目标对象
  3. 代理对象,具有对目标对象的访问权限

其中上面的栗子中充分体现了前两点;第三点也可以体现,只需要改一下前置通知的返回值为boolean,然后做个判断就好啦

类似于现在要做一个权限验证的业务逻辑,再前置通知里面判断,返回true,才能调目标对象。

5、代理模式和适配器模式的比较

  1. 代理模式中,代理对象和它所包裹的目标对象,必须实现相同的接口;适配器模式种 ,适配器和它所包裹的对象不用实现相同的接口
  2. 代理模式中,代理对象可以控制它所包裹的目标对象的方法是否执行;适配器模式中,适配器总是调用目标对象的方法,无法控制

6、代理模式UML图

在这里插入图片描述


http://www.ppmy.cn/news/76909.html

相关文章

【计算机网络复习】第四章 网络层 3

路由器的功能和层次 o 计算机网络的核心设备 o 具有多个输入接口和多个输出接口 o 任务是转发IP包&#xff1a;将从某个输入接口收到的I包&#xff0c;按照要去的目的地&#xff08;即目的网络&#xff09;&#xff0c;从路由器的某个合适的输出接口转发给下一跳路由器 …

将小米SoundMove 无缝接入 ChatGPT

将小米SoundMove 无缝接入 ChatGPT 本教程内容参考 Github 地址(可选)部署查看小米 SoundMove 信息的环境(可选)查看小米 SoundMove 的信息以容器方式部署程序到小米万兆路由器实际效果有待改善点 本教程内容 1 是记录了将小米 SoundMove 接入 ChatGPT 的操作步骤。 2 是将小米…

04-CSS3-渐变色、2D转换、3D转换

一、渐变色 CSS渐变色&#xff08;Gradient&#xff09;是指在元素背景中使用两种或多种不同的颜色进行过渡&#xff0c;超过两个颜色可以形成更为细腻的渐变效果。常见的CSS渐变色有线性渐变和径向渐变。 1. 线性渐变&#xff1a;Linear Gradients 向下/向上/向左/向右/对角…

异常处理错误

目录 1.异常的概念 2.异常的结构 3.异常的分类 3.1 受检查异常&#xff08;Checked Exception&#xff09; 3.2 运行时异常&#xff08;RunTime Exception&#xff09; 4.捕获异常 4.1 try块 4.2 catch捕获 5. 创建自定义异常 6. finally 7. 一些注意点 1.异常的概念…

Tensorflow2基础代码实战系列之双层RNN文本分类任务

深度学习框架Tensorflow2系列 注&#xff1a;大家觉得博客好的话&#xff0c;别忘了点赞收藏呀&#xff0c;本人每周都会更新关于人工智能和大数据相关的内容&#xff0c;内容多为原创&#xff0c;Python Java Scala SQL 代码&#xff0c;CV NLP 推荐系统等&#xff0c;Spark …

安捷伦DSO80404B(Agilent)dso80404b租售回收 数字示波器

DSO80404B 是 Agilent 的 4 GHz、4 通道数字示波器。测量电子电路或组件中随时间变化的电压或电流信号&#xff0c;以显示振幅、频率和上升时间等。应用包括故障排除、生产测试和设计。 附加功能&#xff1a; 4 GHz 带宽&#xff0c;可升级至 13 GHz 4个模拟通道 高达 40 G…

Java基础 变量与数据类型(类型转换)

变量 为什么需要变量 一花一世界&#xff0c;如果把一个程序看做一个世界或一个社会的话&#xff0c;那么变量就是程 序世界的花花草草、万事万物。即&#xff0c;变量是程序中不可或缺的组成单位&#xff0c;最基 本的存储单元。 初识变量 变量的概念 内存中的一个存储区域…

宝塔面板搭建Discuz论坛并发布互联网访问【无需云服务器】

文章目录 前言1.安装基础环境2.一键部署Discuz3.安装cpolar工具4.配置域名访问Discuz5.固定域名公网地址6.配置Discuz论坛 转载自cpolar极点云的文章&#xff1a;Linux宝塔面板搭建Discuz论坛&#xff0c;并公网远程访问【内网穿透】 前言 Crossday Discuz! Board&#xff08;以…