摘要

动态代理是Java语言中非常经典的一种设计模式,也是所有设计模式中最难理解的一种。那什么是代理设计模式?代理设计的基础概念就是通过代理控制对象的访问,可以在这个对象调用方法之前、调用方法之后去处理/添加新的功能。(也就是AOP微实现)。代理在原有代码乃至原业务流程都不修改的情况下,直接在业务流程中切入新代码,增加新功能。

一、代理模式的示例

public interface Flyable {void fly();
}public class Bird implements Flyable {@Overridepublic void fly() {System.out.println("Bird is flying...");try {Thread.sleep(new Random().nextInt(1000));} catch (InterruptedException e) {e.printStackTrace();}}
}

很简单的一个例子,用一个随机睡眠时间模拟小鸟在空中的飞行时间。接下来问题来了,如果我要知道小鸟在天空中飞行了多久,怎么办?有人说,很简单,在Bird->fly()方法的开头记录起始时间,在方法结束记录完成时间,两个时间相减就得到了飞行时间。

   @Overridepublic void fly() {long start = System.currentTimeMillis();System.out.println("Bird is flying...");try {Thread.sleep(new Random().nextInt(1000));} catch (InterruptedException e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("Fly time = " + (end - start));}

的确,这个方法没有任何问题,接下来加大问题的难度。如果Bird这个类来自于某个SDK(或者说Jar包)提供,你无法改动源码,怎么办?一定会有人说,我可以在调用的地方这样写:

public static void main(String[] args) {Bird bird = new Bird();long start = System.currentTimeMillis();bird.fly();long end = System.currentTimeMillis();System.out.println("Fly time = " + (end - start));
}

这个方案看起来似乎没有问题,但其实你忽略了准备这些方法所需要的时间,执行一个方法,需要开辟栈内存、压栈、出栈等操作,这部分时间也是不可以忽略的。因此,这个解决方案不可行。那么,还有什么方法可以做到呢?使用继承

继承是最直观的解决方案,相信你已经想到了,至少我最开始想到的解决方案就是继承。 为此,我们重新创建一个类Bird2,在Bird2中我们只做一件事情,就是调用父类的fly方法,在前后记录时间,并打印时间差:

public class Bird2 extends Bird {@Overridepublic void fly() {long start = System.currentTimeMillis();super.fly();long end = System.currentTimeMillis();System.out.println("Fly time = " + (end - start));}
}

这是一种解决方案,还有一种解决方案叫做:聚合,其实也是比较容易想到的。 我们再次创建新类Bird3,在Bird3的构造方法中传入Bird实例。同时,让Bird3也实现Flyable接口,并在fly方法中调用传入的Bird实例的fly方法:

public class Bird3 implements Flyable {private Bird bird;public Bird3(Bird bird) {this.bird = bird;}@Overridepublic void fly() {long start = System.currentTimeMillis();bird.fly();long end = System.currentTimeMillis();System.out.println("Fly time = " + (end - start));}
}

为了记录Bird->fly()方法的执行时间,我们在前后添加了记录时间的代码。同样地,通过这种方法我们也可以获得小鸟的飞行时间。那么,这两种方法孰优孰劣呢?咋一看,不好评判!

继续深入思考,用问题推导来解答这个问题:

问题一:如果我还需要在fly方法前后打印日志,记录飞行开始和飞行结束,怎么办? 有人说,很简单!继承Bird2并在在前后添加打印语句即可。那么,问题来了,请看问题二。

问题二:如果我需要调换执行顺序,先打印日志,再获取飞行时间,怎么办? 有人说,再新建一个类Bird4继承Bird,打印日志。再新建一个类Bird5继承Bird4,获取方法执行时间。

问题显而易见:使用继承将导致类无限制扩展,同时灵活性也无法获得保障。那么,使用聚合是否可以避免这个问题呢? 答案是:可以!但我们的类需要稍微改造一下。修改Bird3类,将聚合对象Bird类型修改为Flyable

public class BirdTimeProxy implements Flyable {private Flyable flyable;public BirdTimeProxy(Flyable flyable) {this.flyable = flyable;}@Overridepublic void fly() {long start = System.currentTimeMillis();flyable.fly();long end = System.currentTimeMillis();System.out.println("Fly time = " + (end - start));}
}

为了让你看的更清楚,我将Bird3更名为BirdTimeProxy,即用于获取方法执行时间的代理的意思。同时我们新建BirdLogProxy代理类用于打印日志:

public class BirdLogProxy implements Flyable {private Flyable flyable;public BirdLogProxy(Flyable flyable) {this.flyable = flyable;}@Overridepublic void fly() {System.out.println("Bird fly start...");flyable.fly();System.out.println("Bird fly end...");}
}

接下来神奇的事情发生了,如果我们需要先记录日志,再获取飞行时间,可以在调用的地方这么做:

public static void main(String[] args) {Bird bird = new Bird();BirdLogProxy p1 = new BirdLogProxy(bird);BirdTimeProxy p2 = new BirdTimeProxy(p1);p2.fly();
}public static void main(String[] args) {Bird bird = new Bird();BirdTimeProxy p2 = new BirdTimeProxy(bird);BirdLogProxy p1 = new BirdLogProxy(p2);p1.fly();
}

看到这里,有同学可能会有疑问了。虽然现象看起来,聚合可以灵活调换执行顺序。可是为什么聚合可以做到,而继承不行呢。我们用一张图来解释一下:

二、静态代理

public class BirdTimeProxy implements Flyable {private Flyable flyable;public BirdTimeProxy(Flyable flyable) {this.flyable = flyable;}@Overridepublic void fly() {long start = System.currentTimeMillis();flyable.fly();long end = System.currentTimeMillis();System.out.println("Fly time = " + (end - start));}
}
public class BirdLogProxy implements Flyable {private Flyable flyable;public BirdLogProxy(Flyable flyable) {this.flyable = flyable;}@Overridepublic void fly() {System.out.println("Bird fly start...");flyable.fly();System.out.println("Bird fly end...");}
}

上面实现的就是静态代理。在它的fly方法中我们直接调用了flyable->fly()方法。换而言之,BirdTimeProxy其实代理了传入的Flyable对象,这就是典型的静态代理实现。

从表面上看,静态代理已经完美解决了我们的问题。可是,试想一下,如果我们需要计算SDK中100个方法的运行时间,同样的代码至少需要重复100次,并且创建至少100个代理类。往小了说,如果Bird类有多个方法,我们需要知道其他方法的运行时间,同样的代码也至少需要重复多次。因此,静态代理至少有以下两个局限性问题:

  • 如果同时代理多个类,依然会导致类无限制扩展
  • 如果类中有多个方法,同样的逻辑需要反复实现

三、JDK动态代理

那么,我们是否可以使用同一个代理类来代理任意对象呢?我们以获取方法运行时间为例,是否可以使用同一个类(例如:TimeProxy)来计算任意对象的任一方法的执行时间呢?甚至再大胆一点,代理的逻辑也可以自己指定。比如,获取方法的执行时间,打印日志,这类逻辑都可以自己指定。

你脑海中的第一个解决方案应该是使用反射。反射是用于获取已创建实例的方法或者属性,并对其进行调用或者赋值。很明显,在这里,反射解决不了问题。但是,再大胆一点,如果我们可以动态生成TimeProxy这个类,并且动态编译。然后,再通过反射创建对象并加载到内存中,不就实现了对任意对象进行代理了吗?为了防止你依然一头雾水,我们用一张图来描述接下来要做什么:

动态生成Java源文件并且排版是一个非常繁琐的工作,为了简化操作,我们使用JavaPoet这个第三方库帮我们生成TimeProxy的源码。(GitHub - square/javapoet: A Java API for generating .java source files.)  ,如果你的字节码的解析感兴趣,可以自行查阅相关内容。

3.1 生成TimeProxy源码

public class Proxy {public static Object newProxyInstance() throws IOException {TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TimeProxy").addSuperinterface(Flyable.class);FieldSpec fieldSpec = FieldSpec.builder(Flyable.class, "flyable", Modifier.PRIVATE).build();typeSpecBuilder.addField(fieldSpec);MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addParameter(Flyable.class, "flyable").addStatement("this.flyable = flyable").build();typeSpecBuilder.addMethod(constructorMethodSpec);Method[] methods = Flyable.class.getDeclaredMethods();for (Method method : methods) {MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName()).addModifiers(Modifier.PUBLIC).addAnnotation(Override.class).returns(method.getReturnType()).addStatement("long start = $T.currentTimeMillis()", System.class).addCode("\n").addStatement("this.flyable." + method.getName() + "()").addCode("\n").addStatement("long end = $T.currentTimeMillis()", System.class).addStatement("$T.out.println(\"Fly Time =\" + (end - start))", System.class).build();typeSpecBuilder.addMethod(methodSpec);}JavaFile javaFile = JavaFile.builder("com.youngfeng.proxy", typeSpecBuilder.build()).build();// 为了看的更清楚,我将源码文件生成到桌面javaFile.writeTo(new File("/Users/ouyangfeng/Desktop/"));return null;}}

在main方法中调用Proxy.newProxyInstance(),你将看到桌面已经生成了TimeProxy.java文件,生成的内容如下:

package com.zhaungxiaoyan.proxy;import java.lang.Override;
import java.lang.System;class TimeProxy implements Flyable {private Flyable flyable;public TimeProxy(Flyable flyable) {this.flyable = flyable;}@Overridepublic void fly() {long start = System.currentTimeMillis();this.flyable.fly();long end = System.currentTimeMillis();System.out.println("Fly Time =" + (end - start));}
}

3.2 编译TimeProxy源码

编译TimeProxy源码我们直接使用JDK提供的编译工具即可,为了使你看起来更清晰,我使用一个新的辅助类来完成编译操作:

public class JavaCompiler {public static void compile(File javaFile) throws IOException {javax.tools.JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null, null, null);Iterable iterable = fileManager.getJavaFileObjects(javaFile);javax.tools.JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, null, null, null, iterable);task.call();fileManager.close();}
}

在Proxy->newProxyInstance()方法中调用该方法,编译顺利完成:

// 为了看的更清楚,我将源码文件生成到桌面
String sourcePath = "/Users/ouyangfeng/Desktop/";
javaFile.writeTo(new File(sourcePath));// 编译
JavaCompiler.compile(new File(sourcePath + "/com/youngfeng/proxy/TimeProxy.java"));

3.3 加载到内存中并创建对象

  URL[] urls = new URL[] {new URL("file:/" + sourcePath)};URLClassLoader classLoader = new URLClassLoader(urls);Class clazz = classLoader.loadClass("com.youngfeng.proxy.TimeProxy");Constructor constructor = clazz.getConstructor(Flyable.class);Flyable flyable = (Flyable) constructor.newInstance(new Bird());flyable.fly();

通过以上三个步骤,我们至少解决了下面两个问题:

  • 不再需要手动创建TimeProxy
  • 可以代理任意实现了Flyable接口的类对象,并获取接口方法的执行时间

3.4 增加InvocationHandler接口

查看Proxy->newProxyInstance()的源码,代理类继承的接口我们是写死的,为了增加灵活性,我们将接口类型作为参数传入:

接口的灵活性问题解决了,TimeProxy的局限性依然存在,它只能用于获取方法的执行时间,而如果要在方法执行前后打印日志则需要重新创建一个代理类,显然这是不妥的!

为了增加控制的灵活性,我们考虑针将代理的处理逻辑也抽离出来(这里的处理就是打印方法的执行时间)。新增InvocationHandler接口,用于处理自定义逻辑:

public interface InvocationHandler {void invoke(Object proxy, Method method, Object[] args);
}

想象一下,如果客户程序员需要对代理类进行自定义的处理,只要实现该接口,并在invoke方法中进行相应的处理即可。这里我们在接口中设置了三个参数(其实也是为了和JDK源码保持一致):

  • proxy => 这个参数指定动态生成的代理类,这里是TimeProxy
  • method => 这个参数表示传入接口中的所有Method对象
  • args => 这个参数对应当前method方法中的参数

引入了InvocationHandler接口之后,我们的调用顺序应该变成了这样:

MyInvocationHandler handler = new MyInvocationHandler();
Flyable proxy = Proxy.newProxyInstance(Flyable.class, handler);
proxy.fly();方法执行流:proxy.fly() => handler.invoke()

为此,我们需要在Proxy.newProxyInstance()方法中做如下改动:

  • 在newProxyInstance方法中传入InvocationHandler
  • 在生成的代理类中增加成员变量handler
  • 在生成的代理类方法中,调用invoke方法
  public static Object newProxyInstance(Class inf, InvocationHandler handler) throws Exception {TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TimeProxy").addModifiers(Modifier.PUBLIC).addSuperinterface(inf);FieldSpec fieldSpec = FieldSpec.builder(InvocationHandler.class, "handler", Modifier.PRIVATE).build();typeSpecBuilder.addField(fieldSpec);MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addParameter(InvocationHandler.class, "handler").addStatement("this.handler = handler").build();typeSpecBuilder.addMethod(constructorMethodSpec);Method[] methods = inf.getDeclaredMethods();for (Method method : methods) {MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName()).addModifiers(Modifier.PUBLIC).addAnnotation(Override.class).returns(method.getReturnType()).addCode("try {\n").addStatement("\t$T method = " + inf.getName() + ".class.getMethod(\"" + method.getName() + "\")", Method.class)// 为了简单起见,这里参数直接写死为空.addStatement("\tthis.handler.invoke(this, method, null)").addCode("} catch(Exception e) {\n").addCode("\te.printStackTrace();\n").addCode("}\n").build();typeSpecBuilder.addMethod(methodSpec);}JavaFile javaFile = JavaFile.builder("com.youngfeng.proxy", typeSpecBuilder.build()).build();// 为了看的更清楚,我将源码文件生成到桌面String sourcePath = "/Users/ouyangfeng/Desktop/";javaFile.writeTo(new File(sourcePath));// 编译JavaCompiler.compile(new File(sourcePath + "/com/youngfeng/proxy/TimeProxy.java"));// 使用反射load到内存URL[] urls = new URL[] {new URL("file:" + sourcePath)};URLClassLoader classLoader = new URLClassLoader(urls);Class clazz = classLoader.loadClass("com.youngfeng.proxy.TimeProxy");Constructor constructor = clazz.getConstructor(InvocationHandler.class);Object obj = constructor.newInstance(handler);return obj;}

上面的代码你可能看起来比较吃力,我们直接调用该方法,查看最后生成的源码。在main方法中测试newProxyInstance查看生成的TimeProxy源码:

Proxy.newProxyInstance(Flyable.class, new MyInvocationHandler(new Bird()));

生成的TimeProxy.java源码

package com.youngfeng.proxy;import java.lang.Override;
import java.lang.reflect.Method;public class TimeProxy implements Flyable {private InvocationHandler handler;public TimeProxy(InvocationHandler handler) {this.handler = handler;}@Overridepublic void fly() {try {Method method = com.youngfeng.proxy.Flyable.class.getMethod("fly");this.handler.invoke(this, method, null);} catch(Exception e) {e.printStackTrace();}}
}

MyInvocationHandler.java

public class MyInvocationHandler implements InvocationHandler {private Bird bird;public MyInvocationHandler(Bird bird) {this.bird = bird;}@Overridepublic void invoke(Object proxy, Method method, Object[] args) {long start = System.currentTimeMillis();try {method.invoke(bird, new Object[] {});} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("Fly time = " + (end - start));}
}

至此,整个方法栈的调用栈变成了这样:

看到这里,估计很多同学已经晕了,在静态代理部分,我们在代理类中传入了被代理对象。可是,使用newProxyInstance生成动态代理对象的时候,我们居然不再需要传入被代理对象了。我们传入了的实际对象是InvocationHandler实现类的实例,这看起来有点像生成了InvocationHandler的代理对象,在动态生成的代理类的任意方法中都会间接调用InvocationHandler->invoke(proxy, method, args)方法。其实的确是这样。TimeProxy真正代理的对象就是InvocationHandler,不过这里设计的巧妙之处在于,InvocationHandler是一个接口,真正的实现由用户指定。另外,在每一个方法执行的时候,invoke方法都会被调用 ,这个时候如果你需要对某个方法进行自定义逻辑处理,可以根据method的特征信息进行判断分别处理。

如何使用:上面这段解释是告诉你在执行Proxy->newProxyInstance方法的时候真正发生的事情,而在实际使用过程中你完全可以忘掉上面的解释。按照设计者的初衷,我们做如下简单归纳:

  • Proxy->newProxyInstance(infs, handler) 用于生成代理对象
  • InvocationHandler:这个接口主要用于自定义代理逻辑处理
  • 为了完成对被代理对象的方法拦截,我们需要在InvocationHandler对象中传入被代理对象实例。

查看上面的代码,你可以看到我将Bird实例已经传入到了MyInvocationHandler中,原因就是第三点。如果我们还需要对其它任意对象进行代理,不需要改动newProxyInstance方法的源码,只要你在newProxyInstance方法中指定代理需要实现的接口,指定用于自定义处理的InvocationHandler对象,整个代理的逻辑处理都在你自定义的InvocationHandler实现类中进行处理。至此,而我们终于可以从不断地写代理类用于实现自定义逻辑的重复工作中解放出来了,从此需要做什么,交给InvocationHandler。

四、CGLIB动态代理

CGLIB动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。CGLIB动态代理和jdk代理一样,使用反射完成代理,不同的是他可以直接代理类(jdk动态代理不行,他必须目标业务类必须实现接口),CGLIB动态代理底层使用字节码技术,CGLIB动态代理不能对 final类进行继承。(CGLIB动态代理需要导入jar包)

目标对象

package com.zhuangxiaoyan.designpattern.proxymodel.cglibproxy;/*** @Classname TecherDaoimple* @Description TODO* @Date 2022/5/1 10:21* @Created by xjl*/
public class TecherDaoimple{public void tech() {System.out.println("老师正在授课中………… Cglib代理 不需要实现接口");}
}

代理类

package com.zhuangxiaoyan.designpattern.proxymodel.cglibproxy;import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;/*** @Classname ProxyFactory* @Description TODO* @Date 2022/5/1 10:36* @Created by xjl*/
public class ProxyFactory implements MethodInterceptor {// 维护一个目标对象private Object target;// 构造器 传入一个被代理的对象public ProxyFactory(Object target) {this.target = target;}// 返回一个代理对象 是target 对象的代理对象public Object getProxyInstance(){// 创一个工具类Enhancer enhancer=new Enhancer();// 设置父类enhancer.setSuperclass(target.getClass());// 设置回调函数enhancer.setCallback(this);// 创建子类,即代理对象return enhancer.create();}// 重写intercept方法 会调用目标对象的方法@Overridepublic Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {System.out.println("CGLIB代理开始…………");Object invokevale = method.invoke(target, args);System.out.println("CGLIB代理结束…………");return invokevale;}
}

测试

package com.zhuangxiaoyan.designpattern.proxymodel.cglibproxy;/*** @Classname ProxyTest* @Description TODO* @Date 2022/5/1 10:26* @Created by xjl*/
public class CGLIBProxyTest {public static void main(String[] args) {// 创建目标对象TecherDaoimple target=new TecherDaoimple();// 获取代理对象,并转为TecherDaoimple类ProxyFactory proxyFactory = new ProxyFactory(target);TecherDaoimple techerDaoimple = (TecherDaoimple)proxyFactory.getProxyInstance();techerDaoimple.tech();}
}

JDK和Cglib的区别:

  • jdk动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
  • cglib动态代理是利用ASM开源包,对被代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

五、动态代理的作用

基于这样一种动态特性,我们可以用它做很多事情,例如:

  • 远程代理:就是将工作委托给远程对象(不同的服务器,或者不同的进程)来完成。常见的是用在web Service中。还有就是我们的RPC调用也可以理解为一种远程代理。
  • 保护代理:该模式主要进行安全/权限检查。(接触很少)
  • 缓存代理:这个很好理解,就是通过存储来加速调用,比如Sping中的@Cacheable方法,缓存特定的参数获取到的结果,当下次相同参数调用该方法,直接从缓存中返回数据。
  • 虚拟代理:这种代理主要是为方法增加功能,比如记录一些性能指标等,或进行延迟初始化

博文参考

10分钟看懂动态代理设计模式 - 掘金

软件设计模式——代理模式详解相关推荐

  1. C++设计模式 - 代理模式详解一

    代理模式:提供一种可以对真实对象的访问对象,隐藏真实的对象,去除真实对象的非必要的职责. 大家都喜欢玩游戏,单机版游戏如红警.CS.暗黑了等(不小心就暴露了年龄),网络游戏如传奇.魔兽以及吃鸡游戏.王 ...

  2. cglib动态代理jar包_代理模式详解:静态代理+JDK/CGLIB 动态代理实战

    1. 代理模式 代理模式是一种比较好的理解的设计模式.简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标 ...

  3. 代理模式详解(静态代理和动态代理的区别以及联系)

    原文链接:https://www.cnblogs.com/takumicx/p/9285230.html 1. 前言 代理模式可以说是生活中处处可见.比如说在携程上定火车票,携程在这里就起到了一个代理 ...

  4. 设计模式——状态模式详解

    0. 前言 写在最前面,本人的设计模式类博文,建议先看博文前半部分的理论介绍,再看后半部分的实例分析,最后再返回来复习一遍理论介绍,这时候你就会发现我在重点处标红的用心,对于帮助你理解设计模式有奇效哦 ...

  5. java设计模式之代理模式详解

    代理模式在Java中十分常见,有如Spring实现AOP而使用动态代理,更有RPC实现中使用的调用端调用的代理服务.代理模型除了是一种设计模式之外,它更是一种思维,所以探讨并深入理解这种模型是非常有必 ...

  6. Spring AOP理论 +代理模式详解

    目录 1.理解AOP 1.1.什么是AOP 1.2.AOP体系与概念 1.3.Spring AOP 通知的执行顺序 2.代理模式 2.1.静态代理 2.2.静态代理的缺点 3.动态代理 JDK 动态代 ...

  7. 静态代理与动态代理模式详解(优缺点分析,实例分析,读源码必备)

    1.代理模式 (1)概念 代理就是帮别人做事情,如:工厂的中介,中介负责为工厂招收工人,那么中介就是工厂的代理:客户通过商家购买东西,商家向厂家购买货物,商家就是工厂的代理 在开发中存在a类需要调用c ...

  8. 代理模式详解(包含原理详解)

    http://www.cnblogs.com/zuoxiaolong/p/pattern3.html 作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为 ...

  9. (二)代理模式详解(包含原理详解)

    作者:zuoxiaolong8810(左潇龙),转载请注明出处. 我特意将本系列改了下名字,原名是<设计模式学习之路>,原因是因为之前写过一篇<spring源码学习之路>,但是 ...

最新文章

  1. build unity 丢失_Unity Build PS4文件时的一些坑
  2. 使用ramdisk 优化nagios IO性能
  3. 【EventBus】事件通信框架 ( 发送事件 | 根据事件类型获取订阅者 | 调用订阅方法 )
  4. Deeplearning 入门篇
  5. mybatis学习教程中级(十)mybatis和ehcache缓存框架整合(重点)
  6. java mssql mysql,在JSP中访问MSSQLServer数据库_MySQL
  7. 12种Javascript解决常见浏览器兼容问题的方法
  8. 算法学习笔记(5)-------位运算的tips
  9. 嵌入式实时操作系统Ucos3
  10. 超经典两万字,MySQL数据库快速入门。
  11. 异常:java.lang.NoSuchMethodError: org.apache.poi.ss.usermodel.Workbook.getCellStyleAt
  12. mvc实例详解java_MVC模式在Java Web应用程序中的实例分析
  13. 《Oracle从入门到精通》
  14. 微信公众号开发——java后台开发(一)
  15. 计算机联锁静态数据表,计算机联锁功能.doc
  16. linux SFTP用户创建 不允许用户登录,并且连接只允许在制定的目录下进行操作
  17. 如何临时删除桌面右键菜单上的登录画面修改
  18. 简单 python 爬虫(一)
  19. h5物体拖动_【点我解锁】11种网易爆款H5交互手势及案例
  20. Asp.net Core 6.0 使用EF DB First 连接mysql

热门文章

  1. 群邑智库:2019年跨年晚会分析
  2. 华为服务器系统图标,云服务器的图标
  3. 教你PSD文件如何预览(支持32以及64位win7系统)
  4. php需要开启gd服务,中国面积最大的省:总面积超200万平方公里,风景绝美不是新疆!...
  5. NSError错误码解读
  6. nodejs 删除文件夹、清空文件夹内容
  7. acro autoExpandParent在Tree组件内不生效
  8. 离职腾讯,总得说点什么吧
  9. 游戏链改公司 企业链改方案 链改费用
  10. 一个人,一座城,你到底在乎什么?Python 爬虫告诉你!