热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

logger(三)log4j2简介及其实现原理

一、log4j2简介log4j2是log4j1.x和logback的改进版,据说采用了一些新技术(无锁异步、等等),使得日志

一、log4j2简介

log4j2是log4j 1.x和logback的改进版,据说采用了一些新技术(无锁异步、等等),使得日志的吞吐量、性能比log4j 1.x提高10倍,并解决了一些死锁的bug,而且配置更加简单灵活

maven配置


<dependency><groupId>org.apache.logging.log4jgroupId><artifactId>log4j-apiartifactId><version>2.9.1version>
dependency>
<dependency><groupId>org.apache.logging.log4jgroupId><artifactId>log4j-coreartifactId><version>2.9.1version>
dependency>

<dependency><groupId>org.apache.logging.log4jgroupId><artifactId>log4j-webartifactId><version>2.9.1version>
dependency>

<dependency><groupId>org.apache.logging.log4jgroupId><artifactId>log4j-slf4j-implartifactId><version>2.9.1version>
dependency>

<dependency><groupId>org.slf4jgroupId><artifactId>slf4j-apiartifactId><version>1.7.25version>

也可以配置starter

<dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-log4j2artifactId>
dependency>

二、log4j2.xml配置

实现类在log4j2.xml配置文件中的标签名。

xml version&#61;"1.0" encoding&#61;"UTF-8"?>



<configuration status&#61;"WARN" monitorInterval&#61;"30"><properties><property name&#61;"server.port">property>properties><appenders><console name&#61;"Console" target&#61;"SYSTEM_OUT"><PatternLayout pattern&#61;"%d{yyyy-MM-dd HH:mm:ss,SSS} [%thread] %p %m%n"/>console><RollingFile name&#61;"RollingFile" filePattern&#61;"/data/log/tomcat${sys:server.port}/catalina.%d{yyyy-MM-dd}.log"><ThresholdFilter level&#61;"info" onMatch&#61;"ACCEPT" onMismatch&#61;"DENY"/><PatternLayout pattern&#61;"%d{yyyy-MM-dd HH:mm:ss,SSS} [%thread] %p %m%n"/><Policies><TimeBasedTriggeringPolicy interval&#61;"1" modulate&#61;"true"/>Policies><DirectWriteRolloverStrategy/>RollingFile>appenders><loggers><logger name&#61;"org.springframework" level&#61;"INFO">logger><logger name&#61;"org.mybatis" level&#61;"INFO">logger><root level&#61;"INFO"><appender-ref ref&#61;"Console"/><appender-ref ref&#61;"RollingFile"/>root>loggers>
configuration>

简单说Appender就是一个管道&#xff0c;定义了日志内容的去向(保存位置)。

配置一个或者多个Filter进行过滤

配置Layout来控制日志信息的输出格式。

配置Policies以控制日志何时(When)进行滚动。

配置Strategy以控制日志如何(How)进行滚动。

简单说了下配置项&#xff0c;具体可参考博客&#xff1a;https://www.imooc.com/article/78966

https://www.cnblogs.com/hafiz/p/6170702.html

三、log4j2其实现原理

首先介绍下log4j2中的几个重要的概念

LoggerContext

 LoggerContext在Logging System中扮演了锚点的角色。根据情况的不同&#xff0c;一个应用可能同时存在于多个有效的LoggerContext中。在同一LoggerContext下&#xff0c;log system是互通的。如&#xff1a;Standalone Application、Web Applications、Java EE Applications、”Shared” Web Applications 和REST Service Containers&#xff0c;就是不同广度范围的log上下文环境。

Configuration

 每一个LoggerContext都有一个有效的Configuration。Configuration包含了所有的Appenders、上下文范围内的过滤器、LoggerConfigs以及StrSubstitutor.的引用。在重配置期间&#xff0c;新与旧的Configuration将同时存在。当所有的Logger对象都被重定向到新的Configuration对象后&#xff0c;旧的Configuration对象将被停用和丢弃。

 Logger

Loggers 是通过调用LogManager.getLogger方法获得的。Logger对象本身并不实行任何实际的动作。它只是拥有一个name 以及与一个LoggerConfig相关联。它继承了AbstractLogger类并实现了所需的方法。当Configuration改变时&#xff0c;Logger将会与另外的LoggerConfig相关联&#xff0c;从而改变这个Logger的行为。

LoggerConfig

每个LoggerConfig和logger是对应的&#xff0c;获取到一个logger&#xff0c;写日志时其实是通过LoggerConfig来记日志的

1、获取LoggerFactory

和logback一样&#xff0c;slf4j委托具体实现框架的StaticLoggerBinder来返回一个ILoggerFactory&#xff0c;从而对接到具体实现框架上&#xff0c;我们看下这个类(省略了部分代码)

public final class StaticLoggerBinder implements LoggerFactoryBinder {private static final StaticLoggerBinder SINGLETON &#61; new StaticLoggerBinder();private final ILoggerFactory loggerFactory;/*** Private constructor to prevent instantiation*/private StaticLoggerBinder() {loggerFactory &#61; new Log4jLoggerFactory();}/*** Returns the singleton of this class.** &#64;return the StaticLoggerBinder singleton*/public static StaticLoggerBinder getSingleton() {return SINGLETON;}/*** Returns the factory.* &#64;return the factor.*/&#64;Overridepublic ILoggerFactory getLoggerFactory() {return loggerFactory;}
}

可以看到

  • 1、通过getSingleton()获取该类的单例
  • 2、通过构造函数新建了Log4jLoggerFactory实例&#xff0c;
  • 3、通过getLoggerFactory()方法返回该实例

2、获取logger

进入Log4jLoggerFactory类中查看getLogger()方法&#xff0c;发现是在AbstractLoggerAdapter类中

&#64;Overridepublic L getLogger(final String name) {final LoggerContext context &#61; getContext();final ConcurrentMap loggers &#61; getLoggersInContext(context);final L logger &#61; loggers.get(name);if (logger !&#61; null) {return logger;}loggers.putIfAbsent(name, newLogger(name, context));return loggers.get(name);}

1、通过getContext()得到LoggerContext实例

2、在context中查找是否已经有该logger&#xff0c;有就返回

3、如果没有则调用newLogger(name, context)方法新建logger

Log4jLoggerFactory只有两个方法&#xff0c;就是上面说的getContext()和newLogger(name, context)。下面分两节分别讲下这两个方法

public class Log4jLoggerFactory extends AbstractLoggerAdapter implements ILoggerFactory {private static final String FQCN &#61; Log4jLoggerFactory.class.getName();private static final String PACKAGE &#61; "org.slf4j";&#64;Overrideprotected Logger newLogger(final String name, final LoggerContext context) {final String key &#61; Logger.ROOT_LOGGER_NAME.equals(name) ? LogManager.ROOT_LOGGER_NAME : name;return new Log4jLogger(context.getLogger(key), name);}&#64;Overrideprotected LoggerContext getContext() {final Class anchor &#61; StackLocatorUtil.getCallerClass(FQCN, PACKAGE);return anchor &#61;&#61; null ? LogManager.getContext() : getContext(StackLocatorUtil.getCallerClass(anchor));}}

2.1  getContext()

getContext()方法就是返回合适的loggerContext&#xff0c;进入LogManager.getContext()方法

public static LoggerContext getContext() {try {return factory.getContext(FQCN, null, null, true);} catch (final IllegalStateException ex) {LOGGER.warn(ex.getMessage() &#43; " Using SimpleLogger");return new SimpleLoggerContextFactory().getContext(FQCN, null, null, true);}}

factory实在LoggerContext静态代码块中初始化的&#xff0c;继续进入factory.getContext(FQCN, null, null, true)方法中&#xff0c;进入实现类Log4jContextFactory中

&#64;Overridepublic LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext,final boolean currentContext) {final LoggerContext ctx &#61; selector.getContext(fqcn, loader, currentContext);if (externalContext !&#61; null && ctx.getExternalContext() &#61;&#61; null) {ctx.setExternalContext(externalContext);}if (ctx.getState() &#61;&#61; LifeCycle.State.INITIALIZED) {ctx.start();}return ctx;}

LoggerContext是从selector.getContext(fqcn, loader, currentContext)中获取的&#xff0c;此时判断ctx.getState()是否等于LifeCycle.State.INITIALIZED&#xff0c;第一次调用getlogger()时&#xff0c;会进入此方法&#xff0c;我们看下ctx.start();

public void start() {LOGGER.debug("Starting LoggerContext[name&#61;{}, {}]...", getName(), this);if (PropertiesUtil.getProperties().getBooleanProperty("log4j.LoggerContext.stacktrace.on.start", false)) {LOGGER.debug("Stack trace to locate invoker",new Exception("Not a real error, showing stack trace to locate invoker"));}if (configLock.tryLock()) {try {if (this.isInitialized() || this.isStopped()) {this.setStarting();reconfigure();if (this.configuration.isShutdownHookEnabled()) {setUpShutdownHook();}this.setStarted();}} finally {configLock.unlock();}}LOGGER.debug("LoggerContext[name&#61;{}, {}] started OK.", getName(), this);}

进入reconfigure()方法

private void reconfigure(final URI configURI) {final ClassLoader cl &#61; ClassLoader.class.isInstance(externalContext) ? (ClassLoader) externalContext : null;LOGGER.debug("Reconfiguration started for context[name&#61;{}] at URI {} ({}) with optional ClassLoader: {}",contextName, configURI, this, cl);final Configuration instance &#61; ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl);if (instance &#61;&#61; null) {LOGGER.error("Reconfiguration failed: No configuration found for &#39;{}&#39; at &#39;{}&#39; in &#39;{}&#39;", contextName, configURI, cl);} else {setConfiguration(instance);/** instance.start(); Configuration old &#61; setConfiguration(instance); updateLoggers(); if (old !&#61; null) {* old.stop(); }*/final String location &#61; configuration &#61;&#61; null ? "?" : String.valueOf(configuration.getConfigurationSource());LOGGER.debug("Reconfiguration complete for context[name&#61;{}] at URI {} ({}) with optional ClassLoader: {}",contextName, location, this, cl);}}

我们的配置文件log4j2.xml就是该函数中实现的&#xff0c;ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl)得到了配置文件&#xff0c;并解析成Configuration。进入setConfiguration(instance)方法&#xff0c;启动当前的configuration&#xff0c;并启动该配置下的所有appender&#xff0c;logger和root。

public Configuration setConfiguration(final Configuration config) {if (config &#61;&#61; null) {LOGGER.error("No configuration found for context &#39;{}&#39;.", contextName);// No change, return the current configuration.return this.configuration;}configLock.lock();try {final Configuration prev &#61; this.configuration;config.addListener(this);final ConcurrentMap map &#61; config.getComponent(Configuration.CONTEXT_PROPERTIES);try { // LOG4J2-719 network access may throw android.os.NetworkOnMainThreadExceptionmap.putIfAbsent("hostName", NetUtils.getLocalHostname());} catch (final Exception ex) {LOGGER.debug("Ignoring {}, setting hostName to &#39;unknown&#39;", ex.toString());map.putIfAbsent("hostName", "unknown");}map.putIfAbsent("contextName", contextName);config.start();this.configuration &#61; config;updateLoggers();if (prev !&#61; null) {prev.removeListener(this);prev.stop();}firePropertyChangeEvent(new PropertyChangeEvent(this, PROPERTY_CONFIG, prev, config));try {Server.reregisterMBeansAfterReconfigure();} catch (final LinkageError | Exception e) {// LOG4J2-716: Android has no java.lang.managementLOGGER.error("Could not reconfigure JMX", e);}// AsyncLoggers update their nanoClock when the configuration changes
Log4jLogEvent.setNanoClock(configuration.getNanoClock());return prev;} finally {configLock.unlock();}}

2.2 newLogger(name, context)

protected Logger newLogger(final String name, final LoggerContext context) {final String key &#61; Logger.ROOT_LOGGER_NAME.equals(name) ? LogManager.ROOT_LOGGER_NAME : name;return new Log4jLogger(context.getLogger(key), name);}

进入context.getLogger(key)方法

&#64;Overridepublic Logger getLogger(final String name) {return getLogger(name, null);}
&#64;Override
public Logger getLogger(final String name, final MessageFactory messageFactory) {// Note: This is the only method where we add entries to the &#39;loggerRegistry&#39; ivar.Logger logger &#61; loggerRegistry.getLogger(name, messageFactory);if (logger !&#61; null) {AbstractLogger.checkMessageFactory(logger, messageFactory);return logger;}logger &#61; newInstance(this, name, messageFactory);loggerRegistry.putIfAbsent(name, messageFactory, logger);return loggerRegistry.getLogger(name, messageFactory);}

进入newInstance(this, name, messageFactory)方法

protected Logger newInstance(final LoggerContext ctx, final String name, final MessageFactory messageFactory) {return new Logger(ctx, name, messageFactory);}

protected Logger(final LoggerContext context, final String name, final MessageFactory messageFactory) {super(name, messageFactory);this.context &#61; context;privateConfig &#61; new PrivateConfig(context.getConfiguration(), this);}

public PrivateConfig(final Configuration config, final Logger logger) {this.config &#61; config;this.loggerConfig &#61; config.getLoggerConfig(getName());this.loggerConfigLevel &#61; this.loggerConfig.getLevel();this.intLevel &#61; this.loggerConfigLevel.intLevel();this.logger &#61; logger;}

public LoggerConfig getLoggerConfig(final String loggerName) {LoggerConfig loggerConfig &#61; loggerConfigs.get(loggerName);if (loggerConfig !&#61; null) {return loggerConfig;}String substr &#61; loggerName;while ((substr &#61; NameUtil.getSubName(substr)) !&#61; null) {loggerConfig &#61; loggerConfigs.get(substr);if (loggerConfig !&#61; null) {return loggerConfig;}}return root;}

可以看到首先从loggerConfigs也就是配置文件中配置的logger中获取&#xff0c;如果获取不到则循环递归name中"."之前的logger&#xff0c;如果还是获取不到&#xff0c;则默认使用root的配置。

3、logger.info()

Log4jLogger.class

public void info(final String format) {logger.logIfEnabled(FQCN, Level.INFO, null, format);}

&#64;Overridepublic void logIfEnabled(final String fqcn, final Level level, final Marker marker, final String message) {if (isEnabled(level, marker, message)) {logMessage(fqcn, level, marker, message);}}public boolean isEnabled(final Level level, final Marker marker, final String message) {return privateConfig.filter(level, marker, message);}protected void logMessage(final String fqcn, final Level level, final Marker marker, final String message) {final Message msg &#61; messageFactory.newMessage(message);logMessageSafely(fqcn, level, marker, msg, msg.getThrowable());}

可以看到isEnabled()方法中用来通过配置的filter来判断是否符合&#xff0c;如果符合则进入logMessage()方法

protected void logMessage(final String fqcn, final Level level, final Marker marker, final String message) {final Message msg &#61; messageFactory.newMessage(message);logMessageSafely(fqcn, level, marker, msg, msg.getThrowable());}private void logMessageSafely(final String fqcn, final Level level, final Marker marker, final Message msg,final Throwable throwable) {try {logMessageTrackRecursion(fqcn, level, marker, msg, throwable);} finally {// LOG4J2-1583 prevent scrambled logs when logging calls are nested (logging in toString())
ReusableMessageFactory.release(msg);}}private void logMessageTrackRecursion(final String fqcn,final Level level,final Marker marker,final Message msg,final Throwable throwable) {try {incrementRecursionDepth(); // LOG4J2-1518, LOG4J2-2031
tryLogMessage(fqcn, level, marker, msg, throwable);} finally {decrementRecursionDepth();}}

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 logger
handleLogMessageException(e, fqcn, msg);}}

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

public void log(final Supplier 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);}

public void log(final String loggerName, final String fqcn, final Marker marker, final Level level,final Message data, final Throwable t) {List props &#61; null;if (!propertiesRequireLookup) {props &#61; properties;} else {if (properties !&#61; null) {props &#61; new ArrayList<>(properties.size());final LogEvent event &#61; Log4jLogEvent.newBuilder().setMessage(data).setMarker(marker).setLevel(level).setLoggerName(loggerName).setLoggerFqcn(fqcn).setThrown(t).build();for (int i &#61; 0; i ) {final Property prop &#61; properties.get(i);final String value &#61; prop.isValueNeedsLookup() // since LOG4J2-1575? config.getStrSubstitutor().replace(event, prop.getValue()) //
: prop.getValue();props.add(Property.createProperty(prop.getName(), value));}}}final LogEvent logEvent &#61; 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);}}

protected void log(final LogEvent event, final LoggerConfigPredicate predicate) {if (!isFiltered(event)) { processLogEvent(event, predicate);}}

private void processLogEvent(final LogEvent event, final LoggerConfigPredicate predicate) {event.setIncludeLocation(isIncludeLocation());if (predicate.allow(this)) {callAppenders(event);}logParent(event, predicate);}protected void callAppenders(final LogEvent event) {final AppenderControl[] controls &#61; appenders.get();//noinspection ForLoopReplaceableByForEachfor (int i &#61; 0; i ) {controls[i].callAppender(event);}}

这时候终于到了appender的处理了&#xff0c;直接定位到RollingFileAppender类中

public void append(final LogEvent event) {getManager().checkRollover(event);super.append(event);}

private void tryAppend(final LogEvent event) {if (Constants.ENABLE_DIRECT_ENCODERS) {directEncodeEvent(event);} else {writeByteArrayToManager(event);}}
protected void directEncodeEvent(final LogEvent event) {getLayout().encode(event, manager);if (this.immediateFlush || event.isEndOfBatch()) {manager.flush();}}

这时候可以看到layout和encode的使用了

public void encode(final StringBuilder source, final ByteBufferDestination destination) {try {final Object[] threadLocalState &#61; getThreadLocalState();final CharsetEncoder charsetEncoder &#61; (CharsetEncoder) threadLocalState[0];final CharBuffer charBuffer &#61; (CharBuffer) threadLocalState[1];final ByteBuffer byteBuffer &#61; (ByteBuffer) threadLocalState[2];TextEncoderHelper.encodeText(charsetEncoder, charBuffer, byteBuffer, source, destination);} catch (final Exception ex) {logEncodeTextException(ex, source, destination);TextEncoderHelper.encodeTextFallBack(charset, source, destination);}}

最后写日志。

 

四、通过代码动态生成logger对象

public class LoggerHolder {//加个前缀防止配置的name正好是我们某个类名&#xff0c;导致使用的日志路径使用了类名的路径private static final String PREFIX &#61; "logger_";/*** 支持生成写大数据文件的logger** &#64;param name logger name* &#64;return Logger*/public static Logger getLogger(String name) {String loggerName &#61; PREFIX &#43; name;Log4jLoggerFactory loggerFactory &#61; (Log4jLoggerFactory) LoggerFactory.getILoggerFactory();LoggerContext context &#61; (LoggerContext) LogManager.getContext();//如果未加载过该logger,则新建一个if (loggerFactory.getLoggersInContext(context).get(loggerName) &#61;&#61; null) {buildLogger(name);}//
return loggerFactory.getLogger(loggerName);}/*** 包装了Loggerfactory&#xff0c;和LoggerFactory.getLogger(T.class)功能一致** &#64;param clazz* &#64;return*/public static Logger getLogger(Class clazz) {Log4jLoggerFactory loggerFactory &#61; (Log4jLoggerFactory) LoggerFactory.getILoggerFactory();return loggerFactory.getLogger(clazz.getName());}/*** &#64;param name logger name*/private static void buildLogger(String name) {String loggerName &#61; PREFIX &#43; name;LoggerContext context &#61; (LoggerContext) LogManager.getContext();Configuration configuration &#61; context.getConfiguration();//配置PatternLayout输出格式PatternLayout layout &#61; PatternLayout.newBuilder().withCharset(UTF_8).withPattern("%msg%n").build();//配置基于时间的滚动策略TimeBasedTriggeringPolicy policy &#61; TimeBasedTriggeringPolicy.newBuilder().withInterval(24).build();//配置同类型日志策略DirectWriteRolloverStrategy strategy &#61; DirectWriteRolloverStrategy.newBuilder().withConfig(configuration).build();//配置appenderRollingFileAppender appender &#61; RollingFileAppender.newBuilder().setName(loggerName).withFilePattern("/data/bigdata/" &#43; name &#43; "/" &#43; name &#43; ".%d{yyyyMMdd}.log").setLayout(layout).withPolicy(policy).withStrategy(strategy).withAppend(true).build();//改变appender状态
appender.start();//新建loggerLoggerConfig loggerConfig &#61; new LoggerConfig(loggerName, Level.INFO, false);loggerConfig.addAppender(appender, Level.INFO, null);configuration.addLogger(loggerName, loggerConfig);
context.updateLoggers();}
}

 

 

 


转:https://www.cnblogs.com/pjfmeng/p/11277124.html



推荐阅读
  • 理解浏览器历史记录(2)hashchange、pushState
    阅读目录1.hashchange2.pushState本文也是一篇基础文章。继上文之后,本打算去研究pushState,偶然在一些信息中发现了锚点变 ... [详细]
  • spring boot使用jetty无法启动 ... [详细]
  • 使用TabActivity实现Android顶部选项卡功能
    本文介绍如何通过继承TabActivity来创建Android应用中的顶部选项卡。通过简单的步骤,您可以轻松地添加多个选项卡,并实现基本的界面切换功能。 ... [详细]
  • Web动态服务器Python基本实现
    Web动态服务器Python基本实现 ... [详细]
  • 本文详细介绍了如何在 Vue CLI 3.0 和 2.0 中配置 proxy 来解决开发环境下的跨域问题,包括具体的配置项和使用场景。 ... [详细]
  • Maven + Spring + MyBatis + MySQL 环境搭建与实例解析
    本文详细介绍如何使用MySQL数据库进行环境搭建,包括创建数据库表并插入示例数据。随后,逐步指导如何配置Maven项目,整合Spring框架与MyBatis,实现高效的数据访问。 ... [详细]
  • 长期从事ABAP开发工作的专业人士,在面对行业新趋势时,往往需要重新审视自己的发展方向。本文探讨了几位资深专家对ABAP未来走向的看法,以及开发者应如何调整技能以适应新的技术环境。 ... [详细]
  • 本文介绍了SIP(Session Initiation Protocol,会话发起协议)的基本概念、功能、消息格式及其实现机制。SIP是一种在IP网络上用于建立、管理和终止多媒体通信会话的应用层协议。 ... [详细]
  • Beetl是一款先进的Java模板引擎,以其丰富的功能、直观的语法、卓越的性能和易于维护的特点著称。它不仅适用于高响应需求的大型网站,也适合功能复杂的CMS管理系统,提供了一种全新的模板开发体验。 ... [详细]
  • 一、Advice执行顺序二、Advice在同一个Aspect中三、Advice在不同的Aspect中一、Advice执行顺序如果多个Advice和同一个JointPoint连接& ... [详细]
  • 本文介绍了如何通过C#语言调用动态链接库(DLL)中的函数来实现IC卡的基本操作,包括初始化设备、设置密码模式、获取设备状态等,并详细展示了将TextBox中的数据写入IC卡的具体实现方法。 ... [详细]
  • 问题场景用Java进行web开发过程当中,当遇到很多很多个字段的实体时,最苦恼的莫过于编辑字段的查看和修改界面,发现2个页面存在很多重复信息,能不能写一遍?有没有轮子用都不如自己造。解决方式笔者根据自 ... [详细]
  • 本文详细介绍了如何正确设置Shadowsocks公共代理,包括调整超时设置、检查系统限制、防止滥用及遵守DMCA法规等关键步骤。 ... [详细]
  • 本文探讨了如何通过Service Locator模式来简化和优化在B/S架构中的服务命名访问,特别是对于需要频繁访问的服务,如JNDI和XMLNS。该模式通过缓存机制减少了重复查找的成本,并提供了对多种服务的统一访问接口。 ... [详细]
  • Android与JUnit集成测试实践
    本文探讨了如何在Android项目中集成JUnit进行单元测试,并详细介绍了修改AndroidManifest.xml文件以支持测试的方法。 ... [详细]
author-avatar
恋之-风景-
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有