日志 note_深入源码解析日志框架Log4j2(二)

异步

AsyncAppender

a665236aece32047f7918c98a086acda.png

log4j2突出于其他日志的优势,异步日志实现。我们先从日志打印看进去。找到Logger,随便找一个log日志的方法。

public void debug(final Marker marker, final Message msg) {logIfEnabled(FQCN, Level.DEBUG, marker, msg, msg != null ? msg.getThrowable() : null);}

一路跟进

@PerformanceSensitive// NOTE: This is a hot method. Current implementation compiles to 29 bytes of byte code.// This is within the 35 byte MaxInlineSize threshold. Modify with care!private void logMessageTrackRecursion(final String fqcn,final Level level,final Marker marker,final Message msg,final Throwable throwable) {try {incrementRecursionDepth(); // LOG4J2-1518, LOG4J2-2031tryLogMessage(fqcn, level, marker, msg, throwable);} finally {decrementRecursionDepth();}}

可以看出这个在打日志之前做了调用次数的记录。跟进tryLogMessage,

@PerformanceSensitive// NOTE: This is a hot method. Current implementation compiles to 26 bytes of byte code.// This is within the 35 byte MaxInlineSize threshold. Modify with care!private void tryLogMessage(final String fqcn,final Level level,final Marker marker,final Message msg,final Throwable throwable) {try {logMessage(fqcn, level, marker, msg, throwable);} catch (final Exception e) {// LOG4J2-1990 Log4j2 suppresses all exceptions that occur once application called the loggerhandleLogMessageException(e, fqcn, msg);}}

继续跟进:

@Overridepublic void logMessage(final String fqcn, final Level level, final Marker marker, final Message message,final Throwable t) {final Message msg = message == null ? new SimpleMessage(Strings.EMPTY) : message;final ReliabilityStrategy strategy = privateConfig.loggerConfig.getReliabilityStrategy();strategy.log(this, getName(), fqcn, marker, level, msg, t);}

这里可以看到在实际打日志的时候,会从config中获取打日志的策略,跟踪ReliabilityStrategy的创建,发现默认的实现类为DefaultReliabilityStrategy,跟进看实际打日志的方法

@Overridepublic void log(final Supplier<LoggerConfig> reconfigured, final String loggerName, final String fqcn, final Marker marker, final Level level,final Message data, final Throwable t) {loggerConfig.log(loggerName, fqcn, marker, level, data, t);}

这里实际打日志的方法居然是交给一个config去实现的。。。感觉有点奇怪。。跟进看看

@PerformanceSensitive("allocation")public void log(final String loggerName, final String fqcn, final Marker marker, final Level level,final Message data, final Throwable t) {List<Property> props = null;if (!propertiesRequireLookup) {props = properties;} else {if (properties != null) {props = new ArrayList<>(properties.size());final LogEvent event = Log4jLogEvent.newBuilder().setMessage(data).setMarker(marker).setLevel(level).setLoggerName(loggerName).setLoggerFqcn(fqcn).setThrown(t).build();for (int i = 0; i < properties.size(); i++) {final Property prop = properties.get(i);final String value = prop.isValueNeedsLookup() // since LOG4J2-1575? config.getStrSubstitutor().replace(event, prop.getValue()) //: prop.getValue();props.add(Property.createProperty(prop.getName(), value));}}}final LogEvent logEvent = logEventFactory.createEvent(loggerName, marker, fqcn, level, data, props, t);try {log(logEvent, LoggerConfigPredicate.ALL);} finally {// LOG4J2-1583 prevent scrambled logs when logging calls are nested (logging in toString())ReusableLogEventFactory.release(logEvent);}}

可以清楚的看到try之前是在创建LogEvent,try里面做的才是真正的log(好tm累),一路跟进。

private void processLogEvent(final LogEvent event, LoggerConfigPredicate predicate) {event.setIncludeLocation(isIncludeLocation());if (predicate.allow(this)) {callAppenders(event);}logParent(event, predicate);}

接下来就是callAppender了,我们直接开始看AsyncAppender的append方法:

/*** Actual writing occurs here.** @param logEvent The LogEvent.*/@Overridepublic void append(final LogEvent logEvent) {if (!isStarted()) {throw new IllegalStateException("AsyncAppender " + getName() + " is not active");}final Log4jLogEvent memento = Log4jLogEvent.createMemento(logEvent, includeLocation);InternalAsyncUtil.makeMessageImmutable(logEvent.getMessage());if (!transfer(memento)) {if (blocking) {if (AbstractLogger.getRecursionDepth() > 1) { // LOG4J2-1518, LOG4J2-2031// If queue is full AND we are in a recursive call, call appender directly to prevent deadlockAsyncQueueFullMessageUtil.logWarningToStatusLogger();logMessageInCurrentThread(logEvent);} else {// delegate to the event router (which may discard, enqueue and block, or log in current thread)final EventRoute route = asyncQueueFullPolicy.getRoute(thread.getId(), memento.getLevel());route.logMessage(this, memento);}} else {error("Appender " + getName() + " is unable to write primary appenders. queue is full");logToErrorAppenderIfNecessary(false, memento);}}}

这里主要的步骤就是:

  1. 生成logEvent
  2. 将logEvent放入BlockingQueue,就是transfer方法
  3. 如果BlockingQueue满了则启用相应的策略

同样的,这里也有一个线程用来做异步消费的事情

private class AsyncThread extends Log4jThread {private volatile boolean shutdown = false;private final List<AppenderControl> appenders;private final BlockingQueue<LogEvent> queue;public AsyncThread(final List<AppenderControl> appenders, final BlockingQueue<LogEvent> queue) {super("AsyncAppender-" + THREAD_SEQUENCE.getAndIncrement());this.appenders = appenders;this.queue = queue;setDaemon(true);}@Overridepublic void run() {while (!shutdown) {LogEvent event;try {event = queue.take();if (event == SHUTDOWN_LOG_EVENT) {shutdown = true;continue;}} catch (final InterruptedException ex) {break; // LOG4J2-830}event.setEndOfBatch(queue.isEmpty());final boolean success = callAppenders(event);if (!success && errorAppender != null) {try {errorAppender.callAppender(event);} catch (final Exception ex) {// Silently accept the error.}}}// Process any remaining items in the queue.LOGGER.trace("AsyncAppender.AsyncThread shutting down. Processing remaining {} queue events.",queue.size());int count = 0;int ignored = 0;while (!queue.isEmpty()) {try {final LogEvent event = queue.take();if (event instanceof Log4jLogEvent) {final Log4jLogEvent logEvent = (Log4jLogEvent) event;logEvent.setEndOfBatch(queue.isEmpty());callAppenders(logEvent);count++;} else {ignored++;LOGGER.trace("Ignoring event of class {}", event.getClass().getName());}} catch (final InterruptedException ex) {// May have been interrupted to shut down.// Here we ignore interrupts and try to process all remaining events.}}LOGGER.trace("AsyncAppender.AsyncThread stopped. Queue has {} events remaining. "+ "Processed {} and ignored {} events since shutdown started.", queue.size(), count, ignored);}/*** Calls {@link AppenderControl#callAppender(LogEvent) callAppender} on all registered {@code AppenderControl}* objects, and returns {@code true} if at least one appender call was successful, {@code false} otherwise. Any* exceptions are silently ignored.** @param event the event to forward to the registered appenders* @return {@code true} if at least one appender call succeeded, {@code false} otherwise*/boolean callAppenders(final LogEvent event) {boolean success = false;for (final AppenderControl control : appenders) {try {control.callAppender(event);success = true;} catch (final Exception ex) {// If no appender is successful the error appender will get it.}}return success;}public void shutdown() {shutdown = true;if (queue.isEmpty()) {queue.offer(SHUTDOWN_LOG_EVENT);}if (getState() == State.TIMED_WAITING || getState() == State.WAITING) {this.interrupt(); // LOG4J2-1422: if underlying appender is stuck in wait/sleep/join/park call}}}

直接看run方法:

  1. 阻塞获取logEvent
  2. 将logEvent分发出去
  3. 如果线程要退出了,将blockingQueue里面的event消费完在退出。

AsyncLogger

d877b81e70ee94f7be4e352c44359b23.png

接从AsyncLogger的logMessage看进去:

public void logMessage(final String fqcn, final Level level, final Marker marker, final Message message,final Throwable thrown) {if (loggerDisruptor.isUseThreadLocals()) {logWithThreadLocalTranslator(fqcn, level, marker, message, thrown);} else {// LOG4J2-1172: avoid storing non-JDK classes in ThreadLocals to avoid memory leaks in web appslogWithVarargTranslator(fqcn, level, marker, message, thrown);}}

跟进logWithThreadLocalTranslator,

private void logWithThreadLocalTranslator(final String fqcn, final Level level, final Marker marker,final Message message, final Throwable thrown) {// Implementation note: this method is tuned for performance. MODIFY WITH CARE!final RingBufferLogEventTranslator translator = getCachedTranslator();initTranslator(translator, fqcn, level, marker, message, thrown);initTranslatorThreadValues(translator);publish(translator);}

这里的逻辑很简单,就是将日志相关的信息转换成RingBufferLogEvent(RingBuffer是Disruptor的无所队列),然后将其发布到RingBuffer中。发布到RingBuffer中,那肯定也有消费逻辑。这时候有两种方式可以找到这个消费的逻辑。

  • 找disruptor被使用的地方,然后查看,但是这样做会很容易迷惑
  • 按照Log4j2的尿性,这种Logger都有对应的start方法,我们可以从start方法入手寻找

在start方法中,我们找到了一段代码:

final RingBufferLogEventHandler[] handlers = {new RingBufferLogEventHandler()};disruptor.handleEventsWith(handlers);

直接看看这个RingBufferLogEventHandler的实现:

public class RingBufferLogEventHandler implementsSequenceReportingEventHandler<RingBufferLogEvent>, LifecycleAware {private static final int NOTIFY_PROGRESS_THRESHOLD = 50;private Sequence sequenceCallback;private int counter;private long threadId = -1;@Overridepublic void setSequenceCallback(final Sequence sequenceCallback) {this.sequenceCallback = sequenceCallback;}@Overridepublic void onEvent(final RingBufferLogEvent event, final long sequence,final boolean endOfBatch) throws Exception {event.execute(endOfBatch);event.clear();// notify the BatchEventProcessor that the sequence has progressed.// Without this callback the sequence would not be progressed// until the batch has completely finished.if (++counter > NOTIFY_PROGRESS_THRESHOLD) {sequenceCallback.set(sequence);counter = 0;}}/*** Returns the thread ID of the background consumer thread, or {@code -1} if the background thread has not started* yet.* @return the thread ID of the background consumer thread, or {@code -1}*/public long getThreadId() {return threadId;}@Overridepublic void onStart() {threadId = Thread.currentThread().getId();}@Overridepublic void onShutdown() {}
}

顺着接口找上去,发现一个接口:

/*** Callback interface to be implemented for processing events as they become available in the {@link RingBuffer}** @param <T> event implementation storing the data for sharing during exchange or parallel coordination of an event.* @see BatchEventProcessor#setExceptionHandler(ExceptionHandler) if you want to handle exceptions propagated out of the handler.*/
public interface EventHandler<T>
{/*** Called when a publisher has published an event to the {@link RingBuffer}** @param event      published to the {@link RingBuffer}* @param sequence   of the event being processed* @param endOfBatch flag to indicate if this is the last event in a batch from the {@link RingBuffer}* @throws Exception if the EventHandler would like the exception handled further up the chain.*/void onEvent(T event, long sequence, boolean endOfBatch) throws Exception;
}

通过注释可以发现,这个onEvent就是处理逻辑,回到RingBufferLogEventHandler的onEvent方法,发现里面有一个execute方法,跟进:

public void execute(final boolean endOfBatch) {this.endOfBatch = endOfBatch;asyncLogger.actualAsyncLog(this);}

这个方法就是实际打日志了,AsyncLogger看起来还是比较简单的,只是使用了一个Disruptor。

插件化

之前在很多代码里面都可以看到

final PluginManager manager = new PluginManager(CATEGORY);
manager.collectPlugins(pluginPackages);

其实整个log4j2为了获得更好的扩展性,将自己的很多组件都做成了插件,然后在配置的时候去加载plugin。 跟进collectPlugins。

public void collectPlugins(final List<String> packages) {final String categoryLowerCase = category.toLowerCase();final Map<String, PluginType<?>> newPlugins = new LinkedHashMap<>();// First, iterate the Log4j2Plugin.dat files found in the main CLASSPATHMap<String, List<PluginType<?>>> builtInPlugins = PluginRegistry.getInstance().loadFromMainClassLoader();if (builtInPlugins.isEmpty()) {// If we didn't find any plugins above, someone must have messed with the log4j-core.jar.// Search the standard package in the hopes we can find our core plugins.builtInPlugins = PluginRegistry.getInstance().loadFromPackage(LOG4J_PACKAGES);}mergeByName(newPlugins, builtInPlugins.get(categoryLowerCase));// Next, iterate any Log4j2Plugin.dat files from OSGi Bundlesfor (final Map<String, List<PluginType<?>>> pluginsByCategory : PluginRegistry.getInstance().getPluginsByCategoryByBundleId().values()) {mergeByName(newPlugins, pluginsByCategory.get(categoryLowerCase));}// Next iterate any packages passed to the static addPackage method.for (final String pkg : PACKAGES) {mergeByName(newPlugins, PluginRegistry.getInstance().loadFromPackage(pkg).get(categoryLowerCase));}// Finally iterate any packages provided in the configuration (note these can be changed at runtime).if (packages != null) {for (final String pkg : packages) {mergeByName(newPlugins, PluginRegistry.getInstance().loadFromPackage(pkg).get(categoryLowerCase));}}LOGGER.debug("PluginManager '{}' found {} plugins", category, newPlugins.size());plugins = newPlugins;}

处理逻辑如下:

  1. 从Log4j2Plugin.dat中加载所有的内置的plugin
  2. 然后将OSGi Bundles中的Log4j2Plugin.dat中的plugin加载进来
  3. 再加载传入的package路径中的plugin
  4. 最后加载配置中的plugin

逻辑还是比较简单的,但是我在看源码的时候发现了一个很有意思的东西,就是在加载log4j2 core插件的时候,也就是

PluginRegistry.getInstance().loadFromMainClassLoader()

这个方法,跟进到decodeCacheFiles:

private Map<String, List<PluginType<?>>> decodeCacheFiles(final ClassLoader loader) {final long startTime = System.nanoTime();final PluginCache cache = new PluginCache();try {final Enumeration<URL> resources = loader.getResources(PluginProcessor.PLUGIN_CACHE_FILE);if (resources == null) {LOGGER.info("Plugin preloads not available from class loader {}", loader);} else {cache.loadCacheFiles(resources);}} catch (final IOException ioe) {LOGGER.warn("Unable to preload plugins", ioe);}final Map<String, List<PluginType<?>>> newPluginsByCategory = new HashMap<>();int pluginCount = 0;for (final Map.Entry<String, Map<String, PluginEntry>> outer : cache.getAllCategories().entrySet()) {final String categoryLowerCase = outer.getKey();final List<PluginType<?>> types = new ArrayList<>(outer.getValue().size());newPluginsByCategory.put(categoryLowerCase, types);for (final Map.Entry<String, PluginEntry> inner : outer.getValue().entrySet()) {final PluginEntry entry = inner.getValue();final String className = entry.getClassName();try {final Class<?> clazz = loader.loadClass(className);final PluginType<?> type = new PluginType<>(entry, clazz, entry.getName());types.add(type);++pluginCount;} catch (final ClassNotFoundException e) {LOGGER.info("Plugin [{}] could not be loaded due to missing classes.", className, e);} catch (final LinkageError e) {LOGGER.info("Plugin [{}] could not be loaded due to linkage error.", className, e);}}}final long endTime = System.nanoTime();final DecimalFormat numFormat = new DecimalFormat("#0.000000");final double seconds = (endTime - startTime) * 1e-9;LOGGER.debug("Took {} seconds to load {} plugins from {}",numFormat.format(seconds), pluginCount, loader);return newPluginsByCategory;}

可以发现加载时候是从一个文件(PLUGIN_CACHE_FILE)获取所有要获取的plugin。看到这里的时候我有一个疑惑就是,为什么不用反射的方式直接去扫描,而是要从文件中加载进来,而且文件是写死的,很不容易扩展啊。然后我找了一下PLUGIN_CACHE_FILE这个静态变量的用处,发现了PluginProcessor这个类,这里用到了注解处理器。

/*** Annotation processor for pre-scanning Log4j 2 plugins.*/
@SupportedAnnotationTypes("org.apache.logging.log4j.core.config.plugins.*")
public class PluginProcessor extends AbstractProcessor {// TODO: this could be made more abstract to allow for compile-time and run-time plugin processing/*** The location of the plugin cache data file. This file is written to by this processor, and read from by* {@link org.apache.logging.log4j.core.config.plugins.util.PluginManager}.*/public static final String PLUGIN_CACHE_FILE ="META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat";private final PluginCache pluginCache = new PluginCache();@Overridepublic boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {System.out.println("Processing annotations");try {final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Plugin.class);if (elements.isEmpty()) {System.out.println("No elements to process");return false;}collectPlugins(elements);writeCacheFile(elements.toArray(new Element[elements.size()]));System.out.println("Annotations processed");return true;} catch (final IOException e) {e.printStackTrace();error(e.getMessage());return false;} catch (final Exception ex) {ex.printStackTrace();error(ex.getMessage());return false;}}
}

(不太重要的方法省略) 我们可以看到在process方法中,PluginProcessor会先收集所有的Plugin,然后在写入文件。这样做的好处就是可以省去反射时候的开销。 然后我又看了一下Plugin这个注解,发现它的RetentionPolicy是RUNTIME,一般来说PluginProcessor是搭配RetentionPolicy.SOURCE,CLASS使用的,而且既然你把自己的Plugin扫描之后写在文件中了,RetentionPolicy就没有必要是RUNTIME了吧,这个是一个很奇怪的地方。

小结

总算是把Log4j2的代码看完了,发现它的设计理念很值得借鉴,为了灵活性,所有的东西都设计成插件式。互联网技术日益发展,各种中间件层出不穷,而作为工程师的我们更需要做的是去思考代码与代码之间的关系,毫无疑问的是,解耦是最具有美感的关系。

添加Java高级架构交流群 378461078

关注微信公众号“托尼的技术成长之路”

001bfee00ef01f5186391f0a562f9f70.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/489940.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

垂死挣扎-1

进程控制块是描述进程状态和特性的数据结构&#xff0c;一个进程只能有惟一的进程控制块。 编译&#xff1a;进行语法检查、函数分配空间&#xff0c;将各个函数编译成二进制码&#xff0c;按照特定目标文件格式生成目标文件。连接&#xff1a;把程序中各个文件编译生成的目标文…

最近5年,诺贝尔化学奖都颁给了谁?

来源&#xff1a;科研圈诺贝尔化学奖&#xff08;瑞典语&#xff1a;Nobelpriset i kemi&#xff09;由诺贝尔基金会管理&#xff0c;每年颁发一次&#xff0c;用于表彰在化学各个领域作出杰出贡献的人。第一枚诺贝尔化学奖章于 1901 年授予荷兰物理化学家雅各布斯亨里克斯范托…

6-4 二叉树的非递归遍历 (25分)_本周小结!(二叉树)

给「代码随想录」一个星标吧&#xff01;❝以后每周加上一个本周小结怎么样&#xff1f;❞本周小结发现大家周末的时候貌似都不在学习状态&#xff0c;周末的文章浏览量和打卡情况照工作日差很多呀&#xff0c;可能是本周日是工作日了&#xff0c;周六得好好放松放松&#xff0…

5G核心网关键技术总览

来源&#xff1a;电子万花筒未来智能实验室是人工智能学家与科学院相关机构联合成立的人工智能&#xff0c;互联网和脑科学交叉研究机构。未来智能实验室的主要工作包括&#xff1a;建立AI智能系统智商评测体系&#xff0c;开展世界人工智能智商评测&#xff1b;开展互联网&…

samba安装_Centos安装Samba

1.先安装相关依赖以及库&#xff1a;yum install -y gcc-cyum install -y pcre pcre-develyum install -y zlib zlib-develyum install -y openssl openssl-devel2.下载nginx安装包&#xff1a;wget -c https://nginx.org/download/nginx-1.12.0.tar.gz然后直接解压tar -zxvf n…

c++函数为什么带imp_二次函数含参最值问题,老师怎么讲学生都不明白,试试这九张动图...

“一入函数深似海,从此数学是路人”很多同学都有这样的感觉。问&#xff1a;数学是从什么开始听不懂了&#xff1f;答&#xff1a;学函数的时候。函数问题作为中学阶段数学重要的知识点&#xff0c;真的是难倒了很多同学。数学老师也非常的痛苦&#xff0c;每次讲完函数问题&am…

解析 | 自动驾驶视觉定位与导航技术的研究与应用

来源&#xff1a;驭势未来视觉定位与导航技术是智能体具备的基础能力之一&#xff0c;随着无人驾驶的发展&#xff0c;基于低成本摄像头及人工智能算法的视觉定位与导航技术成为了无人驾驶的主流技术路线之一。在传统工作方式中&#xff0c;视觉定位与导航技术通常采用SLAM&…

visual studio 调试 定义debug常量_有趣的阅读 12个提高生产力的Visual Studio调试技巧...

以前因为工作的关系&#xff0c;接触过不少程序员同行&#xff0c;发现很多同学对所谓的小技巧tips感兴趣。本文就是一篇很有趣的文章&#xff0c;总结介绍了12个关于“宇宙第一IDE”—— Visual Studio的实用技巧。善加利用这些小技巧&#xff0c;能够在使用Visual Studio进行…

淘金尖端领域:全球量子技术最新投资趋势

IBM50量子位量子计算机原型&#xff08;来源&#xff1a;IBM&#xff09;来源&#xff1a;资本实验室 作为一个少有人参与的尖端领域&#xff0c;量子技术除了在少数国家的政府部门与科研机构&#xff0c;以及科技巨头的推动之下缓慢成长&#xff0c;也正在通过越来越多的创业公…

teamviewer设备数量上限怎么解决_支小蜜刷脸支付设备解决方案,支小蜜刷脸支付设备怎么使用...

高校刷脸支付解决方案&#xff0c;用于解决高校消费场景支付需求&#xff0c;在线上通过高校人脸小程序完成学生认证&#xff0c;面容录入&#xff0c;开通刷脸支付。开通刷脸支付后&#xff0c;可以在学校内通过刷脸进行支付(校园食堂消费、校园超市消费等)。这个方案将支付宝…

reload vue 重新加载_vue面试,谈下router拦截

最近开始准备面试&#xff0c;尽管是电话面试全身还是充斥着紧张感&#xff0c;一面感觉还不错&#xff0c;主要是问react和vue的一些生命周期&#xff0c;介绍下redux...&#xff0c;二面印象深刻的是问到了介绍下router拦截&#xff0c;我觉得还是个人对router 的理解不够&am…

文小刚:创新就是孩子的游戏

来源&#xff1a; 返朴 撰文 | 文小刚 &#xff08;麻省理工学院终身教授、格林讲席教授&#xff09;1什么是创新应用科学&#xff08;工程技术&#xff09;的目的是利用已知的知识&#xff0c;创造社会价值&#xff0c;造福人类。而基础科学的目的是创造新知识&#xff0c;开拓…

bzoj1532: [POI2005]Kos-Dicing

1532: [POI2005]Kos-Dicing Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 1520 Solved: 516[Submit][Status][Discuss]Description Dicing 是一个两人玩的游戏,这个游戏在Byteotia非常流行. 甚至人们专门成立了这个游戏的一个俱乐部. 俱乐部的人时常在一起玩这个游戏然后评…

python搭配什么数据库_python 连接操作数据库(一)

一、下面我们所说的就是连接mysql的应用&#xff1a; 1、其实在python中连接操作mysql的模块有多个&#xff0c;在这里我只给大家演示pymysql这一个模块&#xff08;其实我是感觉它比较好用而已&#xff09;&#xff1a; pymysql是第三方的一个模块&#xff0c;需要我们自己安装…

GE数字化重塑的启示:调整阵型,再战工业互联网!

来源&#xff1a;中国华能&#xff08;部分内容参考《财经》杂志韩舒淋“从GE数字化业务大调整看工业互联网未来”&#xff09;打响“工业互联网”第一枪的GE&#xff08;美国通用电气&#xff09;&#xff0c;曾是全球市值最高的工业巨头&#xff0c;如今市值却缩水至巅峰时期…

python简单图画程序_用Python的Turtple画图形

不知道各位是否还记得在小学或者初中的时候&#xff0c;我们接触过一种语言叫做logo语言&#xff0c;这个语言可以画正方形&#xff0c;画三角形&#xff0c;画圆。而用Python画图形也有点类似logo语言的意思。 在画图之前&#xff0c;我们需要启动一个模块&#xff0c;turtple…

freeRTOS实时操作系统学习笔记

温馨提示&#xff1a;点击图片查看大图更清晰 —————————————————————————————↑↑↑上方资源下载后可获取xmind原文件。 1、freeRTOS移植和配置脑图 2、内核源码学习

通用AI——未来真正强健的人工智能?

来源&#xff1a;王宏琳科学网博客一本新书纽约大学马库斯&#xff08;Gary Marcus&#xff09;教授和戴维斯&#xff08;Ernest Davis&#xff09;教授2019年新书&#xff08;参考资料[1]&#xff09;的书名&#xff0c;颇吸引眼球&#xff1a;《Rebooting AI : 构建我们可以信…

python开发工具管理系统_Python开发桌面软件文档及网址管理工具,强迫症的福音...

原标题&#xff1a;Python开发桌面软件文档及网址管理工具&#xff0c;强迫症的福音写在前面 这两天用python鼓捣开发了一个软件&#xff0c;分享给同事&#xff0c;觉得很实用&#xff0c;可以大大提高工作效率&#xff0c;想通过平台分享出来给大家 希望给爱好python的童鞋做…

halcon 将数据保存到excel_pandas筛选、合并、批量保存excel数据

人生苦短&#xff0c;我学python&#xff01;python是目前最流行的编程语言&#xff0c;功能十分强大&#xff0c;在爬虫、数据分析、人工智能方面均得以广泛应用。本专辑主要分享两个数据分析库numpy和pandas在数据分析方面的基础知识和各种技能&#xff0c;pandas的快、准、简…