CGLib(Code Generation Library)是一个高性能的字节码生成库,可以在运行期扩展Java类和接口。
实现原理:利用ASM(直接操作字节码的框架)加载被代理类的class文件,修改字节码生成继承自被代理类的子类(代理类),代理类通过重写被代理类方法实现代理。
限制:
public class PrintService {
public void print(){
System.out.println("打印信息");
}
}
package com.dotwait.proxy.interceptor;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class UserMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
doBefore();
Object result = methodProxy.invokeSuper(obj, args);
doAfter();
return result;
}
private void doBefore(){
System.out.println("CGLIB动态代理:方法执行前");
}
private void doAfter(){
System.out.println("CGLIB动态代理:方法执行后");
}
}
package com.dotwait.proxy;
import com.dotwait.proxy.interceptor.UserMethodInterceptor;
import com.dotwait.proxy.service.PrintService;
import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
public class CGLibProxyApp {
public static void main(String[] args) {
//代理类class文件存入本地磁盘本项目目录下
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, System.getProperty("user.dir"));
//实例化Enhancer类生成器
Enhancer enhancer = new Enhancer();
//设置Enhancer要生成的目标类的父类
enhancer.setSuperclass(PrintService.class);
//设置目标类执行方法的拦截器
enhancer.setCallback(new UserMethodInterceptor());
//获取代理对象
PrintService proxy= (PrintService) enhancer.create();
proxy.print();
}
}
CGLIB动态代理:方法执行前
打印信息
CGLIB动态代理:方法执行后
(1)获取代理对象调用了enhancer.create方法,实现如下
public Object create() {
classOnly = false;
argumentTypes = null;
return createHelper();
}
(2)create方法重置了两个变量后调用了createHelper方法,查看createHelper实现
private Object createHelper() {
// 预声明回调方法的验证
preValidate();
// 构建一个对这类增强操作唯一定位的 key
Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null,
ReflectUtils.getNames(interfaces),
filter == ALL_ZERO ? null : new WeakCacheKey<CallbackFilter>(filter),
callbackTypes,
useFactory,
interceptDuringConstruction,
serialVersionUID);
this.currentKey = key;
// 调用父类的 create(...) 方法
Object result = super.create(key);
return result;
}
(3)createHelper关键逻辑调用了父类的create,实现如下
protected Object create(Object key) {
try {
// 获取加载生成类的ClassLoader
ClassLoader loader = getClassLoader();
// 从缓存中拿到ClassLoader加载过的数据
Map<ClassLoader, ClassLoaderData> cache = CACHE;
ClassLoaderData data = cache.get(loader);
// 缓存中不存在,则将该ClassLoader的数据加入缓存
if (data == null) {
synchronized (AbstractClassGenerator.class) {
// 进入同步代码块后再次确认缓存中无此ClassLoader对应数据
cache = CACHE;
data = cache.get(loader);
if (data == null) {
// 新建缓存,拷贝原有数据,初始化该ClassLoader的数据,加入缓存
Map<ClassLoader, ClassLoaderData> newCache = new WeakHashMap<ClassLoader, ClassLoaderData>(cache);
data = new ClassLoaderData(loader);
newCache.put(loader, data);
CACHE = newCache;
}
}
}
this.key = key;
// 获取代理类的Class对象或ClassLoaderData对象
Object obj = data.get(this, getUseCache());
if (obj instanceof Class) {
// 初次实例化对象
return firstInstance((Class) obj);
}
// 从ClassLoaderData中实例化
return nextInstance(obj);
} catch (RuntimeException e) {
throw e;
} catch (Error e) {
throw e;
} catch (Exception e) {
throw new CodeGenerationException(e);
}
}
(4)data.get是真正获取代理类的Class对象或ClassLoaderData对象的方法,获取到之后Class对象则通过firstInstance实例化,ClassLoaderData对象则通过nextInstance实例化,查看比较核心的data.get方法实现
public Object get(AbstractClassGenerator gen, boolean useCache) {
// 不使用缓存,则直接构建
if (!useCache) {
return gen.generate(ClassLoaderData.this);
} else {
// 使用缓存
Object cachedValue = generatedClasses.get(gen);
return gen.unwrapCachedValue(cachedValue);
}
}
(5)gen.generate方法会生成代理类的Class对象,不使用缓存直接调用创建,使用缓存,最终也会调用此方法,因为gen.generate方法的调用存在于ClassLoaderData构造器的Function函数式实例中,gen.generate实现如下
protected Class generate(ClassLoaderData data) {
Class gen;
Object save = CURRENT.get();
CURRENT.set(this);
try {
// 获取类加载器
ClassLoader classLoader = data.getClassLoader();
if (classLoader == null) {
throw new IllegalStateException("ClassLoader is null while trying to define class " +
getClassName() + ". It seems that the loader has been expired from a weak reference somehow. " +
"Please file an issue at cglib's issue tracker.");
}
synchronized (classLoader) {
// 为代理类生成唯一类名
String name = generateClassName(data.getUniqueNamePredicate());
data.reserveName(name);
this.setClassName(name);
}
if (attemptLoad) {
// 尝试直接加载
try {
gen = classLoader.loadClass(getClassName());
return gen;
} catch (ClassNotFoundException e) {
// ignore
}
}
// 通过特定策略实现的形式生成新的字节码
byte[] b = strategy.generate(this);
// 解析新的字节码,获取类的className
String className = ClassNameReader.getClassName(new ClassReader(b));
ProtectionDomain protectionDomain = getProtectionDomain();
synchronized (classLoader) {
// 通过反射加载代理类
if (protectionDomain == null) {
gen = ReflectUtils.defineClass(className, b, classLoader);
} else {
gen = ReflectUtils.defineClass(className, b, classLoader, protectionDomain);
}
}
return gen;
} catch (Exception e) {
throw new CodeGenerationException(e);
} finally {
CURRENT.set(save);
}
}
(6)核心代码是strategy.genereate方法,该方法生成了代理类的字节码,拿到字节码后通过反射工具类ReflectUtils的defineClass加载代理类,查看strategy.genereate方法实现
public byte[] generate(ClassGenerator cg) throws Exception {
DebuggingClassWriter cw = getClassVisitor();
// 生成字节码
transform(cg).generateClass(cw);
return transform(cw.toByteArray());
}
(7)generateClass方法是真正的生成字节码,实现如下
public void generateClass(ClassVisitor v) throws Exception {
Class sc = (superclass == null) ? Object.class : superclass;
// 父类不可以被final修饰
if (TypeUtils.isFinal(sc.getModifiers()))
throw new IllegalArgumentException("Cannot subclass final class " + sc.getName());
// 获取父类直接声明的构造方法
List constructors = new ArrayList(Arrays.asList(sc.getDeclaredConstructors()));
filterConstructors(sc, constructors);
// Order is very important: must add superclass, then
// its superclass chain, then each interface and
// its superinterfaces.
List actualMethods = new ArrayList();
List interfaceMethods = new ArrayList();
final Set forcePublic = new HashSet();
getMethods(sc, interfaces, actualMethods, interfaceMethods, forcePublic);
List methods = CollectionUtils.transform(actualMethods, new Transformer() {
public Object transform(Object value) {
Method method = (Method)value;
int modifiers = Constants.ACC_FINAL
| (method.getModifiers()
& ~Constants.ACC_ABSTRACT
& ~Constants.ACC_NATIVE
& ~Constants.ACC_SYNCHRONIZED);
if (forcePublic.contains(MethodWrapper.create(method))) {
modifiers = (modifiers & ~Constants.ACC_PROTECTED) | Constants.ACC_PUBLIC;
}
return ReflectUtils.getMethodInfo(method, modifiers);
}
});
ClassEmitter e = new ClassEmitter(v);
if (currentData == null) {
e.begin_class(Constants.V1_8,
Constants.ACC_PUBLIC,
getClassName(),
Type.getType(sc),
(useFactory ?
TypeUtils.add(TypeUtils.getTypes(interfaces), FACTORY) :
TypeUtils.getTypes(interfaces)),
Constants.SOURCE_FILE);
} else {
e.begin_class(Constants.V1_8,
Constants.ACC_PUBLIC,
getClassName(),
null,
new Type[]{
FACTORY},
Constants.SOURCE_FILE);
}
List constructorInfo = CollectionUtils.transform(constructors, MethodInfoTransformer.getInstance());
e.declare_field(Constants.ACC_PRIVATE, BOUND_FIELD, Type.BOOLEAN_TYPE, null);
e.declare_field(Constants.ACC_PUBLIC | Constants.ACC_STATIC, FACTORY_DATA_FIELD, OBJECT_TYPE, null);
if (!interceptDuringConstruction) {
e.declare_field(Constants.ACC_PRIVATE, CONSTRUCTED_FIELD, Type.BOOLEAN_TYPE, null);
}
e.declare_field(Constants.PRIVATE_FINAL_STATIC, THREAD_CALLBACKS_FIELD, THREAD_LOCAL, null);
e.declare_field(Constants.PRIVATE_FINAL_STATIC, STATIC_CALLBACKS_FIELD, CALLBACK_ARRAY, null);
if (serialVersionUID != null) {
e.declare_field(Constants.PRIVATE_FINAL_STATIC, Constants.SUID_FIELD_NAME, Type.LONG_TYPE, serialVersionUID);
}
for (int i = 0; i < callbackTypes.length; i++) {
e.declare_field(Constants.ACC_PRIVATE, getCallbackField(i), callbackTypes[i], null);
}
// This is declared private to avoid "public field" pollution
e.declare_field(Constants.ACC_PRIVATE | Constants.ACC_STATIC, CALLBACK_FILTER_FIELD, OBJECT_TYPE, null);
if (currentData == null) {
emitMethods(e, methods, actualMethods);
emitConstructors(e, constructorInfo);
} else {
emitDefaultConstructor(e);
}
emitSetThreadCallbacks(e);
emitSetStaticCallbacks(e);
emitBindCallbacks(e);
if (useFactory || currentData != null) {
int[] keys = getCallbackKeys();
emitNewInstanceCallbacks(e);
emitNewInstanceCallback(e);
emitNewInstanceMultiarg(e, constructorInfo);
emitGetCallback(e, keys);
emitSetCallback(e, keys);
emitGetCallbacks(e);
emitSetCallbacks(e);
}
e.end_class();
}
(8)第(3)步中的firstInstance和nextInstance的实现如下
protected Object firstInstance(Class type) throws Exception {
if (classOnly) {
return type;
} else {
return createUsingReflection(type);
}
}
protected Object nextInstance(Object instance) {
EnhancerFactoryData data = (EnhancerFactoryData) instance;
if (classOnly) {
return data.generatedClass;
}
Class[] argumentTypes = this.argumentTypes;
Object[] arguments = this.arguments;
if (argumentTypes == null) {
argumentTypes = Constants.EMPTY_CLASS_ARRAY;
arguments = null;
}
return data.newInstance(argumentTypes, arguments, callbacks);
}
(9)最后,都是通过ReflectUtils.newInstance来实例化代理类
private Object createUsingReflection(Class type) {
setThreadCallbacks(type, callbacks);
try{
if (argumentTypes != null) {
return ReflectUtils.newInstance(type, argumentTypes, arguments);
} else {
return ReflectUtils.newInstance(type);
}
}finally{
// clear thread callbacks to allow them to be gc'd
setThreadCallbacks(type, null);
}
}
public Object newInstance(Class[] argumentTypes, Object[] arguments, Callback[] callbacks) {
setThreadCallbacks(callbacks);
try {
// Explicit reference equality is added here just in case Arrays.equals does not have one
if (primaryConstructorArgTypes == argumentTypes ||
Arrays.equals(primaryConstructorArgTypes, argumentTypes)) {
// If we have relevant Constructor instance at hand, just call it
// This skips "get constructors" machinery
return ReflectUtils.newInstance(primaryConstructor, arguments);
}
// Take a slow path if observing unexpected argument types
return ReflectUtils.newInstance(generatedClass, argumentTypes, arguments);
} finally {
// clear thread callbacks to allow them to be gc'd
setThreadCallbacks(null);
}
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.dotwait.proxy.service;
import java.lang.reflect.Method;
import net.sf.cglib.core.ReflectUtils;
import net.sf.cglib.core.Signature;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/**
* 新的类名,通过 $$EnhancerByCGLIB 以及 $$133b414e(这个是哈希值) 对类名进行唯一区分
* 如果还是重复了,在动态生成前会进行类名的检测,并通过增加序号 "_<index>" 的形式进行进一步区分
*/
public class PrintService$$EnhancerByCGLIB$$133b414e extends PrintService implements Factory {
private boolean CGLIB$BOUND;
public static Object CGLIB$FACTORY_DATA;
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback[] CGLIB$STATIC_CALLBACKS;
private MethodInterceptor CGLIB$CALLBACK_0;
private static Object CGLIB$CALLBACK_FILTER;
// 被代理类的print方法
private static final Method CGLIB$print$0$Method;
// 代理类的print方法
private static final MethodProxy CGLIB$print$0$Proxy;
private static final Object[] CGLIB$emptyArgs;
private static final Method CGLIB$equals$1$Method;
private static final MethodProxy CGLIB$equals$1$Proxy;
private static final Method CGLIB$toString$2$Method;
private static final MethodProxy CGLIB$toString$2$Proxy;
private static final Method CGLIB$hashCode$3$Method;
private static final MethodProxy CGLIB$hashCode$3$Proxy;
private static final Method CGLIB$clone$4$Method;
private static final MethodProxy CGLIB$clone$4$Proxy;
// 初始化工作,每个方法对象绑定类的实际方法
static void CGLIB$STATICHOOK1() {
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
CGLIB$emptyArgs = new Object[0];
Class var0 = Class.forName("com.dotwait.proxy.service.PrintService$$EnhancerByCGLIB$$133b414e");
Class var1;
CGLIB$print$0$Method = ReflectUtils.findMethods(new String[]{
"print", "()V"}, (var1 = Class.forName("com.dotwait.proxy.service.PrintService")).getDeclaredMethods())[0];
CGLIB$print$0$Proxy = MethodProxy.create(var1, var0, "()V", "print", "CGLIB$print$0");
Method[] var10000 = ReflectUtils.findMethods(new String[]{
"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
CGLIB$equals$1$Method = var10000[0];
CGLIB$equals$1$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$1");
CGLIB$toString$2$Method = var10000[1];
CGLIB$toString$2$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$2");
CGLIB$hashCode$3$Method = var10000[2];
CGLIB$hashCode$3$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$3");
CGLIB$clone$4$Method = var10000[3];
CGLIB$clone$4$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$4");
}
final void CGLIB$print$0() {
super.print();
}
// 对print方法进行了代理
public final void print() {
// 获取CallBack实例
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
// 为null则尝试重新获取
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
// 调用MethodInterceptor的intercept方法,执行增强后的逻辑
// 这里调用的是UserMethodInterceptor的intercept
var10000.intercept(this, CGLIB$print$0$Method, CGLIB$emptyArgs, CGLIB$print$0$Proxy);
} else {
// 为null调用父类PrintService的print,不做代理
super.print();
}
}
final boolean CGLIB$equals$1(Object var1) {
return super.equals(var1);
}
public final boolean equals(Object var1) {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
Object var2 = var10000.intercept(this, CGLIB$equals$1$Method, new Object[]{
var1}, CGLIB$equals$1$Proxy);
return var2 == null ? false : (Boolean)var2;
} else {
return super.equals(var1);
}
}
final String CGLIB$toString$2() {
return super.toString();
}
public final String toString() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
return var10000 != null ? (String)var10000.intercept(this, CGLIB$toString$2$Method, CGLIB$emptyArgs, CGLIB$toString$2$Proxy) : super.toString();
}
final int CGLIB$hashCode$3() {
return super.hashCode();
}
public final int hashCode() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
Object var1 = var10000.intercept(this, CGLIB$hashCode$3$Method, CGLIB$emptyArgs, CGLIB$hashCode$3$Proxy);
return var1 == null ? 0 : ((Number)var1).intValue();
} else {
return super.hashCode();
}
}
final Object CGLIB$clone$4() throws CloneNotSupportedException {
return super.clone();
}
protected final Object clone() throws CloneNotSupportedException {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
return var10000 != null ? var10000.intercept(this, CGLIB$clone$4$Method, CGLIB$emptyArgs, CGLIB$clone$4$Proxy) : super.clone();
}
public static MethodProxy CGLIB$findMethodProxy(Signature var0) {
String var10000 = var0.toString();
switch(var10000.hashCode()) {
case -1166389848:
if (var10000.equals("print()V")) {
return CGLIB$print$0$Proxy;
}
break;
case -508378822:
if (var10000.equals("clone()Ljava/lang/Object;")) {
return CGLIB$clone$4$Proxy;
}
break;
case 1826985398:
if (var10000.equals("equals(Ljava/lang/Object;)Z")) {
return CGLIB$equals$1$Proxy;
}
break;
case 1913648695:
if (var10000.equals("toString()Ljava/lang/String;")) {
return CGLIB$toString$2$Proxy;
}
break;
case 1984935277:
if (var10000.equals("hashCode()I")) {
return CGLIB$hashCode$3$Proxy;
}
}
return null;
}
public PrintService$$EnhancerByCGLIB$$133b414e() {
CGLIB$BIND_CALLBACKS(this);
}
public static void CGLIB$SET_THREAD_CALLBACKS(Callback[] var0) {
CGLIB$THREAD_CALLBACKS.set(var0);
}
public static void CGLIB$SET_STATIC_CALLBACKS(Callback[] var0) {
CGLIB$STATIC_CALLBACKS = var0;
}
private static final void CGLIB$BIND_CALLBACKS(Object var0) {
PrintService$$EnhancerByCGLIB$$133b414e var1 = (PrintService$$EnhancerByCGLIB$$133b414e)var0;
if (!var1.CGLIB$BOUND) {
var1.CGLIB$BOUND = true;
Object var10000 = CGLIB$THREAD_CALLBACKS.get();
if (var10000 == null) {
var10000 = CGLIB$STATIC_CALLBACKS;
if (var10000 == null) {
return;
}
}
var1.CGLIB$CALLBACK_0 = (MethodInterceptor)((Callback[])var10000)[0];
}
}
public Object newInstance(Callback[] var1) {
CGLIB$SET_THREAD_CALLBACKS(var1);
PrintService$$EnhancerByCGLIB$$133b414e var10000 = new PrintService$$EnhancerByCGLIB$$133b414e();
CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
return var10000;
}
public Object newInstance(Callback var1) {
CGLIB$SET_THREAD_CALLBACKS(new Callback[]{
var1});
PrintService$$EnhancerByCGLIB$$133b414e var10000 = new PrintService$$EnhancerByCGLIB$$133b414e();
CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
return var10000;
}
public Object newInstance(Class[] var1, Object[] var2, Callback[] var3) {
CGLIB$SET_THREAD_CALLBACKS(var3);
PrintService$$EnhancerByCGLIB$$133b414e var10000 = new PrintService$$EnhancerByCGLIB$$133b414e;
switch(var1.length) {
case 0:
var10000.<init>();
CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
return var10000;
default:
throw new IllegalArgumentException("Constructor not found");
}
}
public Callback getCallback(int var1) {
CGLIB$BIND_CALLBACKS(this);
MethodInterceptor var10000;
switch(var1) {
case 0:
var10000 = this.CGLIB$CALLBACK_0;
break;
default:
var10000 = null;
}
return var10000;
}
public void setCallback(int var1, Callback var2) {
switch(var1) {
case 0:
this.CGLIB$CALLBACK_0 = (MethodInterceptor)var2;
default:
}
}
public Callback[] getCallbacks() {
CGLIB$BIND_CALLBACKS(this);
return new Callback[]{
this.CGLIB$CALLBACK_0};
}
public void setCallbacks(Callback[] var1) {
this.CGLIB$CALLBACK_0 = (MethodInterceptor)var1[0];
}
static {
CGLIB$STATICHOOK1();
}
}
文章浏览阅读4k次。默认情况,Lettuce 对 所有非阻塞和非事务型操作 共享 同一个线程安全的本地连接。可配置LettucePool,为阻塞和事务操作,提供独占连接。通过源码与debug验证这些特性。_org.springframework.data.redis.connection.lettuce.lettuceconnectionfactory
文章浏览阅读7.4k次,点赞20次,收藏15次。问题出现的原因由于我的服务器架构为arm所以我需要拉取arm架构类型的镜像首先进入Docker Hub查找所需镜像例如我需要下载kube-controller-manager镜像的arm64版本在下方添加筛选条件选择ARM64选择镜像仓库通过tags来选择需要的版本首先复制右边的docker pull代码选择arm64架构的镜像 并点击这个ID复制digest的值在docker中运行拉取镜像命令直接粘贴我们刚复制的pull命令然后输入一个@在粘贴digest的值 代码如下_docker pull digest
文章浏览阅读488次。修复Windows10中的“此应用程序已被阻止以保护您”_win10系统管理员已阻止这个应用
文章浏览阅读4.2k次,点赞10次,收藏30次。(1)Session用于记录用户的状态。Session指的是一段时间内,单个客户端与Web服务器的一连串相关的交互过程。(2)在一个Session中,客户可能会多次请求访问同一个资源,也有可能请求访问各种不同的服务器资源。(3)Session是由服务器端创建的。_创建session
文章浏览阅读287次。The Head Elder of the tropical island of Lagrishan has a problem. A burst of foreign aid money was spent on extra roads between villages some years ago. But the jungle overtakes roads relentlessly, so_jungle roadsprim
文章浏览阅读1w次,点赞6次,收藏29次。oracle 11g下自带样例,sh 用户下 sales 表数据 有91w条数据select count(1) from sales t;我们用sales 表数据来重新建表并进行分区,比较分区与不分区的效率,以及分区后分区局部索引与位图索引的效率比较。1、 创建包含主分区表和子分区表sales_part_test都按照time_id字段进行分区create table sales..._oracle partition by range
文章浏览阅读1w次,点赞8次,收藏47次。设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构。看看下面的图片(来源于互联网,侵删),再想想Apple的产品、微软的产品设计,就能形象生动的理解这句话。_康威定律
本文介绍了在CentOS 7 minimal环境下安装和使用OpenCV的方法。首先通过源码安装OpenCV,然后使用yum安装Python3。在使用过程中还提到了一些问题的解决方法。
文章浏览阅读3.9k次,点赞3次,收藏3次。本文参考csdn大神的文章:使用BetterTouchTool自定义你的touchBar关于bbt的下载,界面功能介绍可以参考上方链接????的大神文章,方便你根据自己的需求来进行diy 你的touchbar显示内容哦。其实设置起来还是挺简单的。不多说了,上效果题:点开图一的最右边的设置按钮就可以设置声音,亮度,键盘亮光,一键静音哦。但是我发现也是有一个缺点:没有切歌的功能(我没有找到切换上..._better touch tool预设
文章浏览阅读530次。matlab膨胀函数imdilate()使用非对称结构元素进行灰度膨胀的误区_imdilate
文章浏览阅读759次。https://github.com/pytorch/vision/tree/master/torchvision/models_densenet121权重下载
文章浏览阅读1.4w次,点赞15次,收藏150次。又到了一年一届的蓝桥杯比赛报名的时间,很荣幸受老师邀请于昨天早上通过腾讯会议线上给大数据专业的学弟学妹们分享蓝桥杯参赛心得,想必也会有更多初次参加蓝桥杯的同学们在寻找相关的信息。记录生活的同时我想把这份文章分享给大家,如果有这方面的疑问,欢迎评论留言。内容主要涉及以下几个问题:比赛有哪些规则?(针对于第13界蓝桥杯,以后可能有变)需要做哪些准备?参加这个比赛有什么意义?比赛规则来自于蓝桥杯官网:https://dasai.lanqiao.cn/,摘录了我认为比较重要的信息。参赛组别竞赛分为:C/C_蓝桥杯