CGLib 简析
背景
JDK 动态代理存在的一些问题:
调用效率低
JDK 通过反射实现动态代理调用,这意味着低下的调用效率:
每次调用
Method.invoke()
都会检查方法的可见性、校验参数是否匹配,过程涉及到很多 native 调用,具体参见JNI 调用开销反射调用涉及动态类解析,这种不可预测性,导致被反射调用的代码无法被 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 使用的方式很简单,大致可以分为两步:
- 配置 Enhancer
- 设置需要代理的目标类与接口
- 通过
Callback
设置需要增强的功能 - 通过
CallbackFilter
将方法匹配到具体的Callback
- 创建代理对象
- 通过
CallbackFilter
获取方法与Callback
的关联关系 - 继承目标类并重写
override
方法,在调用代码中嵌入 Callback - 编译动态生成的字节码生成代理类
- 通过反射调用构造函数生成代理对象
Callback 分类
此外,CGLib 支持多种 Callback,这里简单介绍几种:
NoOp
不使用动态代理,匹配到的方法不会被重写FixedValue
返回固定值,被代理方法的返回值被忽略Dispatcher
指定上下文,将代理方法调用委托给特定对象MethodInterceptor
调用拦截器,用于实现环绕通知around advice
其中 MethodInterceptor
最为常用,可以实现多种丰富的代理特性。
但这类 Callback
也是其中最重的,会导致生成更多的动态类,具体原因后续介绍。
字节码生成过程
底层通过 Enhancer.generateClass()
生成代理类,其具体过程不作深究,可以简单概括为:
- 通过
ClassVisitor
获取目标类信息 - 通过
ClassEmitter
调用 asm 库注入增强方法,并生成byte[]
形式的字节码 - 通过反射调用
ClassLoader.defineClass()
将byte[]
转换为Class
对象 - 将生成完成的代理类缓存至
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.adviceBefore
与 Weaving.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
来实现高效的方法调用,其主要职责有两个
- 方法映射:解析 Class 对象并为每个 Constructor 与 Method 指定一个整数索引值 index
- 方法调用:通过 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();
}
/* 忽略多余的属性与方法 */
}
本文案例仅涉及 MethodInterceptor
与 Dispatcher
,这两个 Callback 也是 Spring AOP 实现的关键,后续将继续分析相关的源码实现。
附录
CGLib 简析的更多相关文章
- 简析.NET Core 以及与 .NET Framework的关系
简析.NET Core 以及与 .NET Framework的关系 一 .NET 的 Framework 们 二 .NET Core的到来 1. Runtime 2. Unified BCL 3. W ...
- 简析 .NET Core 构成体系
简析 .NET Core 构成体系 Roslyn 编译器 RyuJIT 编译器 CoreCLR & CoreRT CoreFX(.NET Core Libraries) .NET Core 代 ...
- RecycleView + CardView 控件简析
今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...
- Java Android 注解(Annotation) 及几个常用开源项目注解原理简析
不少开源库(ButterKnife.Retrofit.ActiveAndroid等等)都用到了注解的方式来简化代码提高开发效率. 本文简单介绍下 Annotation 示例.概念及作用.分类.自定义. ...
- PHP的错误报错级别设置原理简析
原理简析 摘录php.ini文件的默认配置(php5.4): ; Common Values: ; E_ALL (Show all errors, warnings and notices inclu ...
- Android 启动过程简析
首先我们先来看android构架图: android系统是构建在linux系统上面的. 所以android设备启动经历3个过程. Boot Loader,Linux Kernel & Andr ...
- Android RecycleView + CardView 控件简析
今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...
- Java Annotation 及几个常用开源项目注解原理简析
PDF 版: Java Annotation.pdf, PPT 版:Java Annotation.pptx, Keynote 版:Java Annotation.key 一.Annotation 示 ...
- 【ACM/ICPC2013】POJ基础图论题简析(一)
前言:昨天contest4的惨败经历让我懂得要想在ACM领域拿到好成绩,必须要真正的下苦功夫,不能再浪了!暑假还有一半,还有时间!今天找了POJ的分类题库,做了简单题目类型中的图论专题,还剩下二分图和 ...
随机推荐
- 点云上的深度学习及其在三维场景理解中的应用(PPT内容整理PointNet)
这篇博客主要是整理了PointNet提出者祁芮中台介绍PointNet.PointNet++.Frustum PointNets的PPT内容,内容包括如何将点云进行深度学习,如何设计新型的网络架构 ...
- Java互联网架构师系统进阶课程学习 (4)【享学】
Java互联网架构师系统进阶课程学习 (4)[享学] 4.显式锁和AQS 显式锁 Lock接口和核心方法 Lock接口和synchronized的比较 synchronized 代码简洁,Lock ...
- 3个月零基础入门Python+数据分析,详细时间表+计划表分享
大家好,我是白云. 今天想给大家分享的是三个月零基础入门数据分析学习计划.有小伙伴可能会说,英语好像有点不太好,要怎么办?所以今天我给大家分享的资源呢就是对国内的小伙伴很友好,还附赠大家一份三个月学 ...
- 在vmware虚拟机下的Window2003服务器下安装IIS服务详细教程——超级详细(解决关于:800a0bb9的解决办法)
总的来说,就是9步: 1.控制面板添加或者删除程序2.删除想要删的3.打开IIS配置4.开始共享5.导入源码6.配置权限7.网站属性.文档.应用程序配置8.web服务扩展9.访问网站 在安装好虚拟机的 ...
- JIPB | 两篇连发:华中农大黄俊斌团队报道二羟基异丁酰化调控稻曲病菌致病新机制
水稻是我国重要的粮食作物,稻曲病是水稻三大病害之一,不仅造成稻米产量损失,更重要的是稻曲球中的稻曲菌素的毒性和致畸作用,给人畜健康带来严重威胁.病原菌对植物的侵袭是由病原菌的毒力和植物免疫系统相互作用 ...
- RHCSA_DAY09
常用特殊符号的使用 Linux系统下通配符起到了很大的作用,对于不确定的文档名称可以使用以下特殊字符表示 *常用的特殊符号,在文件名上,用来代表任意多个任意字符** ? 常用的特殊符号,在文件名上,用 ...
- 面试常见SQL中where和having的区别你确定你知道吗!
"Where" 是一个约束声明,使用Where来约束来之数据库的数据,Where是在结果返回之前起作用的,且Where中不能使用聚合函数. "Having" 是 ...
- 『Java』成员内部类 匿名内部类
成员内部类 成员方法类是定义在一个类中的方法外的类,例如: public class Outer{ // 外部类 class Inner { // 内部类 // 内部类成员 } // 外部类成员 } ...
- 给 Arm 生态添把火,腾讯 Kona JDK Arm 架构优化实践
前言 Arm 架构以其兼具性能与功耗的特点,在智能终端以及嵌入式领域得到了广泛的使用,不断扩大其影响力.而在 PC 端以及数据中心,之前往往是 x86 架构在其中发挥着主要的作用.最近,随着人工智能. ...
- 课程设计-基于SSM的在线课程教学系统代码-基于java的线上课程资源共享论坛系统
注意:该项目只展示部分功能,如需了解,评论区咨询即可. 1.开发环境 开发语言:Java 后台框架:SSM 前端框架:vue 数据库:MySQL 设计模式:MVC 架构:B/S 源码类型: Web 编 ...