背景

 JDK 动态代理存在的一些问题:

调用效率低

 JDK 通过反射实现动态代理调用,这意味着低下的调用效率:

  1. 每次调用 Method.invoke() 都会检查方法的可见性、校验参数是否匹配,过程涉及到很多 native 调用,具体参见JNI 调用开销

  2. 反射调用涉及动态类解析,这种不可预测性,导致被反射调用的代码无法被 JIT 内联优化,具体参见反射调用方法

 可以通过java.lang.invoke.MethodHandle来规避以上问题,但是这不在本文讨论的范围。

只能代理接口

 java.lang.reflect.Proxy只支持通过接口生成代理类,这意味着 JDK 动态代理只能代理接口,无法代理具体的类。

 对于一些外部依赖或者现有模块来说,无法通过该方式实现动态代理。

应用场景

 CGLib 是一款用于实现高效动态代理的字节码增强库,通过字节码生成技术,动态编译生成代理类,从而将反射调用转换为普通的方法调用。

 下面通过两个案例体验一下 CGLib 的使用方式。

案例一:Weaving

 现有一个输出问候语句的类 Greet,现在有个新需求:在输出内容前后加上姓名,实现个性化输出。下面通过 CGLib 实现该功能:


class Greet { // 需要被增强目标类
public String hello() { return "hello"; }
public String hi() { return "hi"; }
public String toString() { return "@Greet"; }
} public class Weaving { // 模拟切面织入过程 // 增加 before: 前缀(模拟前置通知)
static MethodInterceptor adviceBefore = (target, method, args, methodProxy) -> "before:" + methodProxy.invokeSuper(target, args); // 增加 :after 后缀(模拟后置通知)
static MethodInterceptor adviceAfter = (target, method, args, methodProxy) -> methodProxy.invokeSuper(target, args) + ":after"; // 通知
static Callback[] advices = new Callback[] { NoOp.INSTANCE/*默认*/, adviceBefore, adviceAfter }; // 切入点
static CallbackFilter pointCut = method -> {
switch (method.getName()) {
case "hello" : return 1; // hello() 方法植入前置通知
case "hi" : return 2; // hi() 方法植入后置通知
default: return 0; // 其他方法不添加通知
}
}; public static void main(String[] args) throws InterruptedException {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Greet.class); // 设置目标类
enhancer.setCallbacks(advices); // 设置通知 Advice
enhancer.setCallbackFilter(pointCut); // 设置切点 PointCut
Greet greet = (Greet) enhancer.create(); // 创建 Proxy 对象
System.out.println(greet.hello());
System.out.println(greet.hi());
System.out.println(greet.toString());
TimeUnit.HOURS.sleep(1);
}
}

案例二:Introduction

 随着业务发展,系统需要支持法语的问候 FranceGreet,在不修改现有业务代码的前提下,可以通过 CGLib 实现该功能:

interface FranceGreet { // 支持新功能的接口
String bonjour();
} class FranceGreeting implements Dispatcher { // 新接口的实现
private final FranceGreet delegate = () -> "bonjour";
@Override
public Object loadObject() throws Exception {
return delegate;
}
} class FranceGreetingMatcher implements CallbackFilter { // 将新接口调用委托给 Dispatcher
@Override
public int accept(Method method) {
return method.getDeclaringClass().equals(FranceGreet.class) ? 1 : 0;
}
} public class Introduction { // 模拟引入新接口 public static void main(String[] args) throws InterruptedException {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Greet.class);
enhancer.setInterfaces(new Class[]{FranceGreet.class}); // 扩展新接口
enhancer.setCallbacks(new Callback[]{ NoOp.INSTANCE, new FranceGreeting()}); // 实现新接口
enhancer.setCallbackFilter(new FranceGreetingMatcher()); // 关联接口与实现
Greet greet = (Greet) enhancer.create();
System.out.println(greet.hello()); // 原方法不受影响
System.out.println(greet.hi());
FranceGreet franceGreet = (FranceGreet) greet;
System.out.println(franceGreet.bonjour()); // 新接口方法正常调用
TimeUnit.HOURS.sleep(1);
}
}

原理简析

 从前面的案例可以看到,CGLib 使用的方式很简单,大致可以分为两步:

  1. 配置 Enhancer
  • 设置需要代理的目标类与接口
  • 通过 Callback 设置需要增强的功能
  • 通过 CallbackFilter 将方法匹配到具体的 Callback
  1. 创建代理对象
  • 通过 CallbackFilter 获取方法与 Callback 的关联关系
  • 继承目标类并重写override方法,在调用代码中嵌入 Callback
  • 编译动态生成的字节码生成代理类
  • 通过反射调用构造函数生成代理对象

Callback 分类

 此外,CGLib 支持多种 Callback,这里简单介绍几种:

  • NoOp 不使用动态代理,匹配到的方法不会被重写
  • FixedValue 返回固定值,被代理方法的返回值被忽略
  • Dispatcher 指定上下文,将代理方法调用委托给特定对象
  • MethodInterceptor 调用拦截器,用于实现环绕通知around advice

 其中 MethodInterceptor 最为常用,可以实现多种丰富的代理特性。

 但这类 Callback 也是其中最重的,会导致生成更多的动态类,具体原因后续介绍。

字节码生成过程

 底层通过 Enhancer.generateClass() 生成代理类,其具体过程不作深究,可以简单概括为:

  1. 通过ClassVisitor获取目标类信息
  2. 通过ClassEmitter调用 asm 库注入增强方法,并生成byte[] 形式的字节码
  3. 通过反射调用ClassLoader.defineClass()byte[]转换为Class对象
  4. 将生成完成的代理类缓存至LoadingCache,避免重复生成

生成的类结构

 通过 arthas 的 jad 命令可以观察到,案例 Weaving 中实际生成了以下类:

  • 目标类:buttercup.test.Greet
  • 代理类:buttercup.test.Greet$$EnhancerByCGLIB(省略后缀)
  • 目标类 FastClass:buttercup.test.Greet$$FastClassByCGLIB(省略后缀)
  • 代理类 FastClass:buttercup.test.Greet$$EnhancerByCGLIB$$FastClassByCGLIB(省略后缀)

代理类

 代理类就是 Ehancer.create() 中为了创建代理对象动态生成的类,该类不但继承了目标类,并且还重写了需要被代理的方法。其命名规则为:目标类 + $$EnhancerByCGLIB

 在案例一中,我们分别给 Greet.hello()Greet.hi() 分别添加了拦截器Weaving.adviceBeforeWeaving.adviceAfter,下面我们分析代理类是如何完成这一功能的:

public class Greet$$EnhancerByCGLIB extends Greet implements Factory {

  private static final Object[] CGLIB$emptyArgs = new Object[0]; // 默认空参数
private static final Callback[] CGLIB$STATIC_CALLBACKS; // 静态 Callback(忽略)
private static final ThreadLocal CGLIB$THREAD_CALLBACKS; // 用于给构造函数传递 Callback // 通过 MethodProxy 代理 Greet.hell() 方法
private static final Method CGLIB$hello$0$Method;
private static final MethodProxy CGLIB$hello$0$Proxy; // 通过 MethodProxy 代理 Greet.hi() 方法
private static final Method CGLIB$hi$1$Method;
private static final MethodProxy CGLIB$hi$1$Proxy; private boolean CGLIB$BOUND; // 判断 Callback 是否已经初始化
private NoOp CGLIB$CALLBACK_0; // 默认不拦截,直接调用目标类方法
private MethodInterceptor CGLIB$CALLBACK_1; // Weaving.adviceBefore(增加 before: 前缀)
private MethodInterceptor CGLIB$CALLBACK_2; // Weaving.adviceAfter(增加 :after 后缀) static {
Greet$$EnhancerByCGLIB.CGLIB$STATICHOOK1();
} static void CGLIB$STATICHOOK1() { // 静态初始化
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
Class<?> clazz = Class.forName("buttercup.test.Greet");
Class<?> clazz2 = Class.forName("buttercup.test.Greet$$EnhancerByCGLIB");
Method[] methodArray = ReflectUtils.findMethods(new String[]{"hello", "()Ljava/lang/String;", "hi", "()Ljava/lang/String;"}, clazz.getDeclaredMethods());
CGLIB$hello$0$Method = methodArray[0];
CGLIB$hello$0$Proxy = MethodProxy.create(clazz, clazz2, "()Ljava/lang/String;", "hello", "CGLIB$hello$0");
CGLIB$hi$1$Method = methodArray[1];
CGLIB$hi$1$Proxy = MethodProxy.create(clazz, clazz2, "()Ljava/lang/String;", "hi", "CGLIB$hi$1");
} // 通过 ThreadLocal 传参
public static void CGLIB$SET_THREAD_CALLBACKS(Callback[] callbackArray) {
CGLIB$THREAD_CALLBACKS.set(callbackArray);
} // 工厂方法,创建增强后的对象
public Object newInstance(Callback[] callbackArray) {
Greet$$EnhancerByCGLIB.CGLIB$SET_THREAD_CALLBACKS(callbackArray);
Greet$$EnhancerByCGLIB Greet$$EnhancerByCGLIB = new Greet$$EnhancerByCGLIB();
Greet$$EnhancerByCGLIB.CGLIB$SET_THREAD_CALLBACKS(null);
return Greet$$EnhancerByCGLIB;
} // 重写 hello() 方法通知 CALLBACK_1
public final String hello() {
MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_1;
if (methodInterceptor == null) { // 初始化 CALLBACK_1
Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
methodInterceptor = this.CGLIB$CALLBACK_1;
}
if (methodInterceptor != null) { // 调用拦截器 Weaving.adviceBefore
return (String)methodInterceptor.intercept(this, CGLIB$hello$0$Method, CGLIB$emptyArgs, CGLIB$hello$0$Proxy);
}
return super.hello();
} // 重写 hi() 方法通知 CALLBACK_2
public final String hi() {
MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_2;
if (methodInterceptor == null) { // 初始化 CALLBACK_2
Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
methodInterceptor = this.CGLIB$CALLBACK_2;
}
if (methodInterceptor != null) { // 调用拦截器 Weaving.adviceAfter
return (String)methodInterceptor.intercept(this, CGLIB$hi$1$Method, CGLIB$emptyArgs, CGLIB$hi$1$Proxy);
}
return super.hi();
} // 直接调用目标类的 hello()
final String CGLIB$hello$0() {
return super.hello();
} // 直接调用目标类的 hi()
final String CGLIB$hi$1() {
return super.hi();
} public Greet$$EnhancerByCGLIB() {
Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
} private static final void CGLIB$BIND_CALLBACKS(Object object) {
block2: {
Object object2;
Greet$$EnhancerByCGLIB Greet$$EnhancerByCGLIB;
block3: {
Greet$$EnhancerByCGLIB = (Greet$$EnhancerByCGLIB) object;
// 如果已经初始化过,则直接返回
if (Greet$$EnhancerByCGLIB.CGLIB$BOUND) break block2;
Greet$$EnhancerByCGLIB.CGLIB$BOUND = true;
if (object2 = CGLIB$THREAD_CALLBACKS.get()) != null) break block3; // 从 ThreadLocal 获取 Callback 参数
if ((object2 = CGLIB$STATIC_CALLBACKS) == null) break block2;
}
Callback[] callbackArray = (Callback[])object2; // 初始化 Callback 参数
Greet$$EnhancerByCGLIB greet$$EnhancerByCGLIB = Greet$$EnhancerByCGLIB;
greet$$EnhancerByCGLIB.CGLIB$CALLBACK_2 = (MethodInterceptor)callbackArray[2];
greet$$EnhancerByCGLIB.CGLIB$CALLBACK_1 = (MethodInterceptor)callbackArray[1];
greet$$EnhancerByCGLIB.CGLIB$CALLBACK_0 = (NoOp)callbackArray[0];
}
}
}

 在动态生成的类中,可以看到 CGLib 为每个被代理的方法创建了 MethodProxy 对象。

 该对象替代了 Method.invoke() 功能,是实现高效方法调用的的关键。下面我们以 Greet.hello() 为例对该类进行分析:

public class MethodProxy {

  private Signature sig1; // 目标类方法签名:hello()Ljava/lang/String;
private Signature sig2; // 代理类方法签名:CGLIB$hello$0()Ljava/lang/String; private MethodProxy.CreateInfo createInfo; /* 省略初始化过程 */ private static class CreateInfo {
Class c1; // 目标类 buttercup.test.Greet
Class c2; // 代理类 buttercup.test.Greet$$EnhancerByCGLIB
} private final Object initLock = new Object();
private volatile MethodProxy.FastClassInfo fastClassInfo; private static class FastClassInfo {
FastClass f1; // 目标类 FastClass :
FastClass f2; // 代理类 FastClass :
int i1; // 方法在 f1 中对应的索引
int i2; // 方法在 f2 中对应的索引
} // 只在 MethodProxy 被调用时加载 FastClass,减少不必要的类生成(lazy-init)
private void init() {
if (fastClassInfo == null) {
synchronized (initLock) {
if (fastClassInfo == null) {
MethodProxy.FastClassInfo fci = new MethodProxy.FastClassInfo();
fci.f1 = helper(ci, ci.c1); //
fci.f2 = helper(ci, ci.c2); //
fci.i1 = fci.f1.getIndex(this.sig1);
fci.i2 = fci.f2.getIndex(this.sig2);
fastClassInfo = new FastClassInfo(); }
}
}
} // 根据 Class 对象生成 FastClass
private static FastClass helper(MethodProxy.CreateInfo ci, Class type) {
Generator g = new Generator();
g.setType(type);
return g.create();
} // 调用 buttercup.test.Greet.hello()
// 但实际上会调用代理类的 EnhancerByCGLIB.hello() 实现
public Object invoke(Object obj, Object[] args) throws Throwable {
init();
return fastClassInfo.f1.invoke(fci.i1, obj, args);
} // 调用 buttercup.test.Greet$$EnhancerByCGLIB.CGLIB$hello$0()
// 通过 super.hello() 调用目标类的 Greet.hello() 实现
public Object invokeSuper(Object obj, Object[] args) throws Throwable {
init();
return fastClassInfo.f2.invoke(fci.i2, obj, args);
}
}

 可以看到 MethodProxy 的调用实际是通过 FastClass 完成的,这是 CGLib 实现高性能反射调用的秘诀,下面来解析这个类的细节。

目标类 FastClass

 为了规避反射带来的性能消耗,cglib 定义了 FastClass 来实现高效的方法调用,其主要职责有两个

  1. 方法映射:解析 Class 对象并为每个 Constructor 与 Method 指定一个整数索引值 index
  2. 方法调用:通过 switch(index) 的方式,将反射调用转化为硬编码调用
abstract public class FastClass {

    // 映射:根据方法名称与参数类型,获取其对应的 index
public abstract int getIndex(String methodName, Class[] argClass); // 调用:根据 index 找到指定的方法,并进行调用
public abstract Object invoke(int index, Object obj, Object[] args) throws InvocationTargetException; }

其命名规则为:目标类 + $$FastClassByCGLIB。下面具体分析一下对目标类 Greet 对应的 FastClass

public class Greet$$FastClassByCGLIB extends FastClass {

    public Greet$$FastClassByCGLIB(Class clazz) {
super(clazz);
} // 获取 index 的最大值
public int getMaxIndex() {
return 4; // 当前 FastClass 总共支持 5 个方法
//索引值分别为 0:hello(), 1:hi(), 2:equals(), 3:hasCode(), 4:toString()
} // 根据方法名称以及参数类型,获取到指定方法对应的 index
public int getIndex(String methodName, Class[] argClass) {
switch (methodName.hashCode()) {
case 3329: {
if (!methodName.equals("hi")) break;
switch (argClass.length) {
case 0: { return 1; } // hi() 对应 index 为 1
}
break;
}
case 99162322: {
if (!methodName.equals("hello")) break;
switch (argClass.length) {
case 0: { return 0; } // hello() 对应 index 为 0
}
break;
}
/* 忽略 Object 方法 */
}
return -1;
} // 根据方法签名,获取到指定方法对应的 index
public int getIndex(Signature signature) {
String sig = ((Object)signature).toString();
switch (sig.hashCode()) {
case 397774237: {
if (!sig.equals("hello()Ljava/lang/String;")) break;
return 0; // hello() 对应 index 为 0
}
case 1155503180: {
if (!sig.equals("hi()Ljava/lang/String;")) break;
return 1; // hi() 对应 index 为 1
}
/* 忽略 Object 方法 */
}
return -1;
} // 方法调用(硬编码调用)
public Object invoke(int n, Object obj, Object[] args) throws InvocationTargetException {
Greet greet = (Greet) obj;
switch (n) { // 通过 index 指定目标函数
case 0: { return greet.hello(); } // 通过索引 0 调用 hello()
case 1: { return greet.hi(); } // 通过索引 1 调用 hi()
/* 忽略 Object 方法 */
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
} // 构造函数(硬编码调用)
public Object newInstance(int n, Object[] argClass) throws InvocationTargetException {
switch (n) {
case 0: { return new Greet(); }
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
}
}

代理类 FastClass

 之前提及过:使用 MethodInterceptor 会比其他 Callback 生成更多的动态类,这是因为需要支持 MethodProxy.invokeSuper() 调用:

public interface MethodInterceptor extends Callback {

    // 所有生成的代理方法都调用此方法
// 大多数情况需要通过 MethodProxy.invokeSuper() 来实现目标类的调用
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}

 MethodProxy.invokeSuper() 通过调用代理类中带 $CGLIB$ 前缀的方法,绕过被重写的代理方法,避免出现无限递归。

 为了保证调用效率,需要对代理类也生成 FastClass

public class Greet$$EnhancerByCGLIB$$FastClassByCGLIB extends FastClass {

    public Object invoke(int n, Object obj, Object[] args) throws InvocationTargetException {
Greet$$EnhancerByCGLIB greet$$EnhancerByCGLIB = (Greet$$EnhancerByCGLIB)obj;
switch (n) {
case 9: { // 调用目标类的原始 Greet.hello() 方法
return greet$$EnhancerByCGLIB.CGLIB$hello$0();
}
case 10: { // 调用目标类的原始 Greet.hi() 方法
return greet$$EnhancerByCGLIB.CGLIB$hi$1();
}
/* 忽略其他 Enhancer 方法 */
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
} public int getIndex(String methodName, Class[] argClass) {
switch (methodName.hashCode()) {
case 1837078673: {
if (!methodName.equals("CGLIB$hi$1")) break;
switch (argClass.length) {
case 0: { return 10; }
}
break;
}
case 1891304123: {
if (!methodName.equals("CGLIB$hello$0")) break;
switch (argClass.length) {
case 0: { return 9; }
}
break;
}
/* 忽略其他 Enhancer 方法 */
}
return -1;
} public int getIndex(Signature signature) {
String sig = ((Object)signature).toString();
switch (sig.hashCode()) {
case -1632605946: {
if (!sig.equals("CGLIB$hello$0()Ljava/lang/String;")) break;
return 9;
}
case 540391388: {
if (!sig.equals("CGLIB$hi$1()Ljava/lang/String;")) break;
return 10;
}
/* 忽略其他 Enhancer 方法 */
}
return -1;
} }

补充

 案例 Introduction 中仅使用了 Dispatcher,因此只生成了代理类,未使用到 FastClass

public class Greet$$EnhancerByCGLIB extends Greet implements FranceGreet, Factory {

    private NoOp CGLIB$CALLBACK_0;
private Dispatcher CGLIB$CALLBACK_1; public final String bonjour() {
Dispatcher dispatcher = this.CGLIB$CALLBACK_1;
if (dispatcher == null) {
Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
dispatcher = this.CGLIB$CALLBACK_1;
}
return ((FranceGreet)dispatcher.loadObject()).bonjour();
} /* 忽略多余的属性与方法 */
}

 本文案例仅涉及 MethodInterceptorDispatcher,这两个 Callback 也是 Spring AOP 实现的关键,后续将继续分析相关的源码实现。

附录

JIT 编译优化

CGLib 简析的更多相关文章

  1. 简析.NET Core 以及与 .NET Framework的关系

    简析.NET Core 以及与 .NET Framework的关系 一 .NET 的 Framework 们 二 .NET Core的到来 1. Runtime 2. Unified BCL 3. W ...

  2. 简析 .NET Core 构成体系

    简析 .NET Core 构成体系 Roslyn 编译器 RyuJIT 编译器 CoreCLR & CoreRT CoreFX(.NET Core Libraries) .NET Core 代 ...

  3. RecycleView + CardView 控件简析

    今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...

  4. Java Android 注解(Annotation) 及几个常用开源项目注解原理简析

    不少开源库(ButterKnife.Retrofit.ActiveAndroid等等)都用到了注解的方式来简化代码提高开发效率. 本文简单介绍下 Annotation 示例.概念及作用.分类.自定义. ...

  5. PHP的错误报错级别设置原理简析

    原理简析 摘录php.ini文件的默认配置(php5.4): ; Common Values: ; E_ALL (Show all errors, warnings and notices inclu ...

  6. Android 启动过程简析

    首先我们先来看android构架图: android系统是构建在linux系统上面的. 所以android设备启动经历3个过程. Boot Loader,Linux Kernel & Andr ...

  7. Android RecycleView + CardView 控件简析

    今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...

  8. Java Annotation 及几个常用开源项目注解原理简析

    PDF 版: Java Annotation.pdf, PPT 版:Java Annotation.pptx, Keynote 版:Java Annotation.key 一.Annotation 示 ...

  9. 【ACM/ICPC2013】POJ基础图论题简析(一)

    前言:昨天contest4的惨败经历让我懂得要想在ACM领域拿到好成绩,必须要真正的下苦功夫,不能再浪了!暑假还有一半,还有时间!今天找了POJ的分类题库,做了简单题目类型中的图论专题,还剩下二分图和 ...

随机推荐

  1. Spring Cloud分区发布实践(4) FeignClient

    上面看到直接通过网关访问微服务是可以实现按区域调用的, 那么微服务之间调用是否也能按区域划分哪? 下面我们使用FeignClient来调用微服务, 就可以配合LoadBalancer实现按区域调用. ...

  2. 20初识前端HTML(1)

    1 .HTML 1.1 网页的组成 文字 图片 链接 等元素构成.除了这些元素之外 网页中还可以包含音频 视频 等 1.2 WEB前端开发的流程 现在主流的开发流程: 前后端分离的开发模式. 美工:p ...

  3. 分布式ID(CosId)之号段链模式性能(1.2亿/s)解析

    分布式ID(CosId)之号段链模式性能(1.2亿/s)解析 上一篇文章<分布式ID生成器(CosId)设计与实现>我们已经简单讨论过CosId的设计与实现全貌. 但是有很多同学有一些疑问 ...

  4. 离线webpack创建vue 项目

    参考地址: https://blog.csdn.net/feinifi/article/details/104578546 画重点: // 需要带上参数--offline表示离线初始化. --offl ...

  5. 线程的常用知识(包括 Thread/Executor/Lock-free/阻塞/并发/锁等)

    本次内容列表: 1.使用线程的经验:设置名称.响应中断.使用ThreadLocal 2.Executor:ExecutorService和Future 3.阻塞队列:put和take.offer和po ...

  6. Servlet基本知识

    Servlet基本知识 1.IDEA创建第一个Servlet程序xing 这里说明如何使用 IDEA Ultimate 2020.1.3版本来新建第一个web程序.参考 MoonChasing 1.1 ...

  7. Java基础技术多线程与并发面试【笔记】

    Java基础技术多线程与并发 什么是线程死锁? ​死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去,我们就可以称 ...

  8. SpringBoot开发七-开发注册功能

    需求介绍-开发注册功能 首先访问注册页面-点击顶部的链接,打开注册页面 提交注册数据 通过表单提交数据 服务端验证账号是否存在,邮箱是否已经注册 服务端发送激活邮件 激活注册账号 点击邮件中的链接,访 ...

  9. 【Vulnhub】DC-1靶机

    一.信息收集 1.1 环境 kali : 192.168.124.141 DC-1 : 192.168.124.150 1.2 nmap进行扫描 :nmap -sV 192.168.124.150 I ...

  10. VLAN-2 配置Trunk接口

    一.实验拓扑图 二.实验编址 三.实验步骤 1.给对应的PC设置对应的IP和掩码还有接口,以及根据需要划分不同的vlan区域,再用文本标记出不同部门. 2.启动设备(全选) 3.首先用ping命令检查 ...