动态代理在Java中有着广泛的应用,比如Spring AOP、Hibernate数据查询、测试框架的后端mock、RPC远程调用、Java注解对象获取、日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等。

本文主要介绍Java中两种常见的动态代理方式:JDK原生动态代理和CGLIB动态代理。

由于Java动态代理与java反射机制关系紧密,请读者确保已经了解了Java反射机制,可参考上一篇文章《Java反射机制详解》

代理模式

本文将介绍的Java动态代理与设计模式中的代理模式有关,什么是代理模式呢?

代理模式:给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。

代理模式角色分为 3 种:

Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;

RealSubject(真实主题角色):真正实现业务逻辑的类;

Proxy(代理主题角色):用来代理和封装真实主题;

代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层

代理模式按照职责(使用场景)来分类,至少可以分为以下几类:1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理等等。

如果根据字节码的创建时机来分类,可以分为静态代理和动态代理:

  • 所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。
  • 而动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件

静态代理

我们先通过实例来学习静态代理,然后理解静态代理的缺点,再来学习本文的主角:动态代理

编写一个接口 UserService ,以及该接口的一个实现类 UserServiceImpl

public interface UserService {public void select();   public void update();
}public class UserServiceImpl implements UserService {  public void select() {  System.out.println("查询 selectById");}public void update() {System.out.println("更新 update");}
}

我们将通过静态代理对 UserServiceImpl 进行功能增强,在调用 select 和 update 之前记录一些日志。写一个代理类 UserServiceProxy,代理类需要实现 UserService

public class UserServiceProxy implements UserService {private UserService target; // 被代理的对象public UserServiceProxy(UserService target) {this.target = target;}public void select() {before();target.select();    // 这里才实际调用真实主题角色的方法after();}public void update() {before();target.update();    // 这里才实际调用真实主题角色的方法after();}private void before() {     // 在执行方法之前执行System.out.println(String.format("log start time [%s] ", new Date()));}private void after() {      // 在执行方法之后执行System.out.println(String.format("log end time [%s] ", new Date()));}
}

客户端测试

public class Client1 {public static void main(String[] args) {UserService userServiceImpl = new UserServiceImpl();UserService proxy = new UserServiceProxy(userServiceImpl);proxy.select();proxy.update();}
}

输出

通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。

静态代理的缺点

虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。

1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:

  • 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
  • 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类

2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护

如何改进?

当然是让代理类动态的生成啦,也就是动态代理。

为什么类可以动态的生成?

这就涉及到Java虚拟机的类加载机制了,推荐翻看《深入理解Java虚拟机》7.3节 类加载的过程。

Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口

常见的字节码操作类库

这里有一些介绍:https://java-source.net/open-source/bytecode-libraries

  • Apache BCEL (Byte Code Engineering Library):是Java classworking广泛使用的一种框架,它可以深入到JVM汇编语言进行类操作的细节。
  • ObjectWeb ASM:是一个Java字节码操作框架。它可以用于直接以二进制形式动态生成stub根类或其他代理类,或者在加载时动态修改类。
  • CGLIB(Code Generation Library):是一个功能强大,高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。
  • Javassist:是Java的加载时反射系统,它是一个用于在Java中编辑字节码的类库; 它使Java程序能够在运行时定义新类,并在JVM加载之前修改类文件。
  • ...

实现动态代理的思考方向

为了让生成的代理类与目标对象(真实主题角色)保持一致性,从现在开始将介绍以下两种最常见的方式:

  1. 通过实现接口的方式 -> JDK动态代理
  2. 通过继承类的方式 -> CGLIB动态代理

注:使用ASM对使用者要求比较高,使用Javassist会比较麻烦

JDK动态代理

JDK动态代理主要涉及两个类:java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler,我们仍然通过案例来学习

编写一个调用逻辑处理器 LogHandler 类,提供日志增强功能,并实现 InvocationHandler 接口;在 LogHandler 中维护一个目标对象,这个对象是被代理的对象(真实主题角色);在 invoke 方法中编写方法调用的逻辑处理

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;public class LogHandler implements InvocationHandler {Object target;  // 被代理的对象,实际的方法执行者public LogHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {before();Object result = method.invoke(target, args);  // 调用 target 的 method 方法after();return result;  // 返回方法的执行结果}// 调用invoke方法之前执行private void before() {System.out.println(String.format("log start time [%s] ", new Date()));}// 调用invoke方法之后执行private void after() {System.out.println(String.format("log end time [%s] ", new Date()));}
}

编写客户端,获取动态生成的代理类的对象须借助 Proxy 类的 newProxyInstance 方法,具体步骤可见代码和注释


import com.suiyun.proxy.staticstate.UserService;
import com.suiyun.proxy.staticstate.UserServiceImpl;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;/*** @author ww* @date 2021/5/24 下午2:58*/
public class Client2 {public static void main(String[] args) throws IllegalAccessException, InstantiationException {// 设置变量可以保存动态代理类,默认名称以 $Proxy0 格式命名// System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");// 1. 创建被代理的对象,UserService接口的实现类UserServiceImpl userServiceImpl = new UserServiceImpl();// 2. 获取对应的 ClassLoaderClassLoader classLoader = userServiceImpl.getClass().getClassLoader();// 3. 获取所有接口的Class,这里的UserServiceImpl只实现了一个接口UserService,Class[] interfaces = userServiceImpl.getClass().getInterfaces();// 4. 创建一个将传给代理类的调用请求处理器,处理所有的代理对象上的方法调用//     这里创建的是一个自定义的日志处理器,须传入实际的执行对象 userServiceImplInvocationHandler logHandler = new LogHandler(userServiceImpl);/*5.根据上面提供的信息,创建代理对象 在这个过程中,a.JDK会通过根据传入的参数信息动态地在内存中创建和.class 文件等同的字节码b.然后根据相应的字节码转换成对应的class,c.然后调用newInstance()创建代理实例*/UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);// 调用代理的方法proxy.select();proxy.update();// 保存JDK动态代理生成的代理类,类名保存为 UserServiceProxy// ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy");}
}

运行结果

InvocationHandler 和 Proxy 的主要方法介绍如下:

java.lang.reflect.InvocationHandler

Object invoke(Object proxy, Method method, Object[] args) 定义了代理对象调用方法时希望执行的动作,用于集中处理在动态代理类对象上的方法调用

java.lang.reflect.Proxy

static InvocationHandler getInvocationHandler(Object proxy) 用于获取指定代理对象所关联的调用处理器

static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) 返回指定接口的代理类

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 构造实现指定接口的代理类的一个新实例,所有方法会调用给定处理器对象的 invoke 方法

static boolean isProxyClass(Class<?> cl) 返回 cl 是否为一个代理类

代理类的调用过程

生成的代理类到底长什么样子呢?借助下面的工具类,把代理类保存下来再探个究竟
(通过设置环境变量sun.misc.ProxyGenerator.saveGeneratedFiles=true也可以保存代理类)

import sun.misc.ProxyGenerator;
import java.io.FileOutputStream;
import java.io.IOException;public class ProxyUtils {/*** 将根据类信息动态生成的二进制字节码保存到硬盘中,默认的是clazz目录下* params: clazz 需要生成动态代理类的类* proxyName: 为动态生成的代理类的名称*/public static void generateClassFile(Class clazz, String proxyName) {// 根据类信息和提供的代理类名称,生成字节码byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());String paths = clazz.getResource(".").getPath();System.out.println(paths);FileOutputStream out = null;try {//保留到硬盘中out = new FileOutputStream(paths + proxyName + ".class");out.write(classFile);out.flush();} catch (Exception e) {e.printStackTrace();} finally {try {out.close();} catch (IOException e) {e.printStackTrace();}}}
}

然后在 Client2 测试类的main的最后面加入一行代码

// 保存JDK动态代理生成的代理类,类名保存为 UserServiceProxy
ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy");

IDEA 再次运行之后就可以在 target 的类路径下找到 UserServiceProxy.class,双击后IDEA的反编译插件会将该二进制class文件

UserServiceProxy 的代码如下所示:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.UserService;public final class UserServiceProxy extends Proxy implements UserService {private static Method m1;private static Method m2;private static Method m4;private static Method m0;private static Method m3;public UserServiceProxy(InvocationHandler var1) throws  {super(var1);}public final boolean equals(Object var1) throws  {// 省略...}public final String toString() throws  {// 省略...}public final void select() throws  {try {super.h.invoke(this, m4, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}public final int hashCode() throws  {// 省略...}public final void update() throws  {try {super.h.invoke(this, m3, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}static {try {m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));m2 = Class.forName("java.lang.Object").getMethod("toString");m4 = Class.forName("proxy.UserService").getMethod("select");m0 = Class.forName("java.lang.Object").getMethod("hashCode");m3 = Class.forName("proxy.UserService").getMethod("update");} catch (NoSuchMethodException var2) {throw new NoSuchMethodError(var2.getMessage());} catch (ClassNotFoundException var3) {throw new NoClassDefFoundError(var3.getMessage());}}
}

从 UserServiceProxy 的代码中我们可以发现:

  • UserServiceProxy 继承了 Proxy 类,并且实现了被代理的所有接口,以及equals、hashCode、toString等方法
  • 由于 UserServiceProxy 继承了 Proxy 类,所以每个代理类都会关联一个 InvocationHandler 方法调用处理器
  • 类和所有方法都被 public final 修饰,所以代理类只可被使用,不可以再被继承
  • 每个方法都有一个 Method 对象来描述,Method 对象在static静态代码块中创建,以 m + 数字 的格式命名
  • 调用方法的时候通过 super.h.invoke(this, m1, (Object[])null); 调用,其中的 super.h.invoke 实际上是在创建代理的时候传递给 Proxy.newProxyInstance 的 LogHandler 对象,它继承 InvocationHandler 类,负责实际的调用处理逻辑

而 LogHandler 的 invoke 方法接收到 method、args 等参数后,进行一些处理,然后通过反射让被代理的对象 target 执行方法

@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {before();Object result = method.invoke(target, args);       // 调用 target 的 method 方法after();return result;  // 返回方法的执行结果}

代理类的调用过程相信大家都明了了,而关于Proxy的源码解析,还请大家另外查阅其他文章或者直接看源码

CGLIB动态代理

maven引入CGLIB包,然后编写一个UserDao类,它没有接口,只有两个方法,select() 和 update()

public class UserDao {public void select() {System.out.println("UserDao 查询 selectById");}public void update() {System.out.println("UserDao 更新 update");}
}

编写一个 LogInterceptor ,实现了 MethodInterceptor,用于方法的拦截回调


import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.Method;
import java.util.Date;public class LogInterceptor implements MethodInterceptor {/*** @param object 表示要进行增强的对象* @param method 表示拦截的方法* @param objects 数组表示参数列表,基本数据类型需要传入其包装类型,如int-->Integer、long-Long、double-->Double* @param methodProxy 表示对方法的代理,invokeSuper方法表示对被代理对象方法的调用* @return 执行结果* @throws Throwable*/@Overridepublic Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {before();Object result = methodProxy.invokeSuper(object, objects);   // 注意这里是调用 invokeSuper 而不是 invoke,否则死循环,methodProxy.invokesuper执行的是原始类的方法,method.invoke执行的是子类的方法after();return result;}private void before() {System.out.println(String.format("log start time [%s] ", new Date()));}private void after() {System.out.println(String.format("log end time [%s] ", new Date()));}
}

测试

public class CglibTest {public static void main(String[] args) {LogInterceptor logInterceptor = new LogInterceptor();Enhancer enhancer = new Enhancer();enhancer.setSuperclass(UserDao.class);  // 设置超类,cglib是通过继承来实现的enhancer.setCallback(logInterceptor);UserDao dao = (UserDao)enhancer.create();   // 创建代理类dao.select();dao.update();}
}

运行结果

还可以进一步多个 MethodInterceptor 进行过滤筛选

public class LogInterceptor2 implements MethodInterceptor {@Overridepublic Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {before();Object result = methodProxy.invokeSuper(object, objects);after();return result;}private void before() {System.out.println(String.format("log2 start time [%s] ", new Date()));}private void after() {System.out.println(String.format("log2 end time [%s] ", new Date()));}
}

回调过滤器: 在CGLib回调时可以设置对不同方法执行不同的回调逻辑,或者根本不执行回调。

public class DaoFilter implements CallbackFilter {@Overridepublic int accept(Method method) {if ("select".equals(method.getName())) {return 0;   // Callback 列表第1个拦截器}return 1;   // Callback 列表第2个拦截器,return 2 则为第3个,以此类推}
}

再次测试

import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;public class CglibTest2 {public static void main(String[] args) {LogInterceptor logInterceptor = new LogInterceptor();LogInterceptor2 logInterceptor2 = new LogInterceptor2();Enhancer enhancer = new Enhancer();enhancer.setSuperclass(UserDao.class);   // 设置超类,cglib是通过继承来实现的enhancer.setCallbacks(new Callback[]{logInterceptor, logInterceptor2});   // 设置多个拦截器enhancer.setCallbackFilter(new DaoFilter());UserDao proxy = (UserDao) enhancer.create();   // 创建代理类proxy.select();proxy.update();}
}

运行结果

CGLIB 创建动态代理类的模式是:

  1. 查找目标类上的所有非final 的public类型的方法定义;
  2. 将这些方法的定义转换成字节码;
  3. 将组成的字节码转换成相应的代理的class对象;
  4. 实现 MethodInterceptor接口,用来处理对代理类上所有方法的请求

JDK动态代理与CGLIB动态代理对比

JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。

cglib动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。

JDK Proxy 的优势:

  • 最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。
  • 平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
  • 代码实现简单。

基于类似 cglib 框架的优势:

  • 无需实现接口,达到代理类无侵入
  • 只操作我们关心的类,而不必为其他相关类增加工作量。
  • 高性能

面试题

来源于网上,用于帮助理解和掌握,欢迎补充

描述动态代理的几种实现方式?分别说出相应的优缺点

代理可以分为 "静态代理" 和 "动态代理",动态代理又分为 "JDK动态代理" 和 "CGLIB动态代理" 实现。

静态代理:代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是 Real Object

  • 优点:可以很好的保护实际对象的业务逻辑对外暴露,从而提高安全性。
  • 缺点:不同的接口要有不同的代理类实现,会很冗余

JDK 动态代理

  • 为了解决静态代理中,生成大量的代理类造成的冗余;

  • JDK 动态代理只需要实现 InvocationHandler 接口,重写 invoke 方法便可以完成代理的实现,

  • jdk的代理是利用反射生成代理类 Proxyxx.class 代理类字节码,并生成对象

  • jdk动态代理之所以只能代理接口是因为代理类本身已经extends了Proxy,而java是不允许多重继承的,但是允许实现多个接口

  • 优点:解决了静态代理中冗余的代理实现类问题。

  • 缺点:JDK 动态代理是基于接口设计实现的,如果没有接口,会抛异常。

CGLIB 代理

  • 由于 JDK 动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK方式解决不了;

  • CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,来完成动态代理的实现。

  • 实现方式实现 MethodInterceptor 接口,重写 intercept 方法,通过 Enhancer 类的回调方法来实现。

  • 但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。

  • 同时,由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。

  • 优点:没有接口也能实现动态代理,而且采用字节码增强技术,性能也不错。

  • 缺点:技术实现相对难理解些。

CGlib 对接口实现代理?


import com.suiyun.proxy.staticstate.UserService;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.Method;/*** 创建代理类的工厂 该类要实现 MethodInterceptor 接口。* 该类中完成三样工作:* (1)声明目标类的成员变量,并创建以目标类对象为参数的构造器。用于接收目标对象* (2)定义代理的生成方法,用于创建代理对象。方法名是任意的。代理对象即目标类的子类* (3)定义回调接口方法。对目标类的增强这在这里完成*/
public class CglibFactory implements MethodInterceptor {// 声明目标类的成员变量private UserService target;public CglibFactory(UserService target) {this.target = target;}// 定义代理的生成方法,用于创建代理对象public UserService myCGLibCreator() {Enhancer enhancer = new Enhancer();// 为代理对象设置父类,即指定目标类enhancer.setSuperclass(UserService.class);/*** 设置回调接口对象 注意,只所以在setCallback()方法中可以写上this,* 是因为MethodIntecepter接口继承自Callback,是其子接口*/enhancer.setCallback(this);return (UserService) enhancer.create();// create用以生成CGLib代理对象}@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {System.out.println("start invoke " + method.getName());Object result = method.invoke(target, args);System.out.println("end invoke " + method.getName());return result;}
}

测试

public class CglibTest3 {public static void main(String[] args) {CglibFactory cglibFactory = new CglibFactory(new UserServiceImpl());UserService userService = cglibFactory.myCGLibCreator();userService.select();userService.update();}
}

运行结果

参考:

《Java核心技术》卷1
《深入理解Java虚拟机》7.3
java docs: https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Proxy.html
Java三种代理模式:静态代理、动态代理和cglib代理
Java动态代理机制详解(JDK 和CGLIB,Javassist,ASM)
静态代理和动态代理的理解

原文链接:https://www.cnblogs.com/whirly/p/10154887.html

源码

https://download.csdn.net/download/javanbme/19034253

Java 动态代理详解 ( 附示例源码,建议收藏)相关推荐

  1. JAVA动态代理详解

    JAVA动态代理详解 问题 1:什么是静态代理,动态代理? 2:动态代理的好处? 什么是静态代理 以生活中例子来看,我作为某某品牌面膜的北京区代理,我替厂家卖面膜,我属于代理,厂家属于委托方. 联系到 ...

  2. java动态代理详解及实际应用

    重点教你如何应用 先回顾一下代理模式 使用场景1:动态代理+反射 实现hook技术 AOP(面向切面编程) 源码下载 先回顾一下代理模式 代理模式分动态代理和静态代理,写起来也比较简单,先上代码: p ...

  3. Java动态代理详解(Proxy+InvocationHandler)

    文章目录 1. 什么是代理 2. 静态代理 2.1 静态代理代码示例 2.2 静态代理总结 2.3 静态代理优缺点 3. 动态代理 3.1 动态代理概念 3.2 动态代理例子 4. 原理解析 1. 什 ...

  4. java小程序详解_微信小程序登录Java后台接口(详解,附示例代码)

    首先看一下官方文档 地址:微信小程序官方文档API登录接口 我们先对官方给的时序图进行简单的分析 1.当小程序调用wx.login()时,会获得一个code(临时登录凭证),然后我们需要用wx.req ...

  5. Java 动态代理详解

    原文: https://www.cnblogs.com/whirly/p/10154887.html

  6. 原创高端影楼人像专业磨皮法教程详解 附PSD源码

    引用:http://user.qzone.qq.com/8286659/blog/1363231042#!app=2&via=QZ.HashRefresh&pos=1363231042 ...

  7. 振动传感器的c语言程序,单片机红外对射传感器实验详解 附程序源码

    #include #define                            BUF_LENTH              128                            // ...

  8. android WebView详解,常见漏洞详解和安全源码(下)

    上篇博客主要分析了 WebView 的详细使用,这篇来分析 WebView 的常见漏洞和使用的坑.  上篇:android WebView详解,常见漏洞详解和安全源码(上)  转载请注明出处:http ...

  9. android WebView详解,常见漏洞详解和安全源码(上)

    这篇博客主要来介绍 WebView 的相关使用方法,常见的几个漏洞,开发中可能遇到的坑和最后解决相应漏洞的源码,以及针对该源码的解析.  由于博客内容长度,这次将分为上下两篇,上篇详解 WebView ...

最新文章

  1. 本地两台虚拟机构建NFS服务器和客户端
  2. unet脑肿瘤分割_2D UNet3+ Pytorch实现 脑肿瘤分割
  3. 小米游戏手柄pc插件_在电视上玩电脑游戏
  4. Python可视化:Seaborn(三)
  5. 苹果 macOS Monterey 12 正式发布
  6. CentOS 6.5忘记root密码,怎么办?
  7. 在国内程序员是吃青春饭的职业吗?
  8. DIY_红外计数模块
  9. 华罗庚黄金分割优选法
  10. 哀其不幸的墨西哥人工智能
  11. 【美图秀秀】如何P图恶搞——室友的噩梦
  12. 六年一轮回:大数据改变的,不仅仅是我的专业!
  13. Html中怎么用CSS让ul中多个li标签不换行横排显示
  14. 深度学习代码学习笔记(一)——阶跃函数与激活函数的python代码实现
  15. Android APP开发框架选择——JSBridge既个人心得和技巧
  16. 计算机缺少opencl.dll,opencl.dll损坏,如何修复
  17. 数字图像处理第五次作业——频域滤波器
  18. SEdb:超级增强子数据库简介
  19. 设置顺序表L是个递增的有序表,试写一算法将number插入L中,并使L任是一个有序表。
  20. Python中的套接字编程

热门文章

  1. 清洁环保的小型风电滑环基本介绍
  2. 2018河北计算机对口高考学校6,[2018年最新整理]2014河北省对口升学计算机高考试题A.doc...
  3. vb.net 教程 4-7 文本文件编码 2
  4. 定义一个学生类。 有下面的类属性:①姓名②年龄③成绩(语文,数学,英语)[每课成绩的类型为整数] 类方法: ①获取学生的姓名:get_name() 返回类型:str ②获取学生的年龄:ge
  5. 退休时每月能领多少钱?——养老金计算程序
  6. 成都富华力鼎:抖音短视频字幕怎么制作
  7. 解决poi设置excel列宽精度丢失问题
  8. 关于DEVC++调试时将该读入的内容输入后终端自动闪退问题解决方案。
  9. arch linux配置服务器,如何在Arch Linux服务器上安装ZNC
  10. Ellisys Bluetooth Sniffer 文档 (EEN-BT08) - 将小麦与麦壳分开