SpringBoot系列教程web篇之自定义异常处理HandlerExceptionResolver

关于Web应用的全局异常处理,上一篇介绍了ControllerAdvice结合@ExceptionHandler的方式来实现web应用的全局异常管理; 本篇博文则带来另外一种并不常见的使用方式,通过实现自定义的HandlerExceptionResolver,来处理异常状态 上篇博文链接: SpringBoot系列教程web篇之全局异常处理本篇原文: SpringBoot系列教程web篇之自定义异常处理HandlerExceptionResolver<!-- more --> I. 环境搭建首先得搭建一个web应用才有可能继续后续的测试,借助SpringBoot搭建一个web应用属于比较简单的活; 创建一个maven项目,pom文件如下 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.7</version> <relativePath/> <!-- lookup parent from update --></parent><properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> <java.version>1.8</java.version></properties><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.45</version> </dependency></dependencies><build> <pluginManagement> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </pluginManagement></build><repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository></repositories>II. HandlerExceptionResolver1. 自定义异常处理HandlerExceptionResolver顾名思义,就是处理异常的类,接口就一个方法,出现异常之后的回调,四个参数中还携带了异常堆栈信息 @NullableModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);我们自定义异常处理类就比较简单了,实现上面的接口,然后将完整的堆栈返回给调用方 public class SelfExceptionHandler implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { String msg = GlobalExceptionHandler.getThrowableStackInfo(ex); try { response.addHeader("Content-Type", "text/html; charset=UTF-8"); response.getWriter().append("自定义异常处理!!! \n").append(msg).flush(); } catch (Exception e) { e.printStackTrace(); } return null; }}// 堆栈信息打印方法如下public static String getThrowableStackInfo(Throwable e) { ByteArrayOutputStream buf = new ByteArrayOutputStream(); e.printStackTrace(new java.io.PrintWriter(buf, true)); String msg = buf.toString(); try { buf.close(); } catch (Exception t) { return e.getMessage(); } return msg;}仔细观察上面的代码实现,有下面几个点需要注意 ...

October 14, 2019 · 2 min · jiezi

一步一步搭建前端监控系统JS错误监控篇

摘要: 徒手写JS错误监控。 作者:一步一个脚印一个坑原文:搭建前端监控系统(二)JS错误监控篇Fundebug经授权转载,版权归原作者所有。 背景:市面上的监控系统有很多,大多收费,对于小型前端项目来说,必然是痛点。另一点主要原因是,功能通用,却未必能够满足我们自己的需求, 所以我们自给自足。 这是搭建前端监控系统的第二章,主要是介绍如何统计js报错,跟着我一步步做,你也能搭建出一个属于自己的前端监控系统。 请移步线上:前端监控系统 对于前端应用来说,Js错误的发生直接影响前端应用的质量。对前端异常的监控是整个前端监控系统中的一个重要环节。前端异常包含很多种情况:1. js编译时异常(开发阶段就能排)2. js运行时异常;3. 加载静态资源异常(路径写错、资源服务器异常、CDN异常、跨域)4. 接口请求异常等。这一篇我们只介绍Js运行时异常。 监控流程:监控错误 -> 搜集错误 -> 存储错误 -> 分析错误 -> 错误报警-> 定位错误 -> 解决错误 首先,我们应该对Js报错情况有个大致的了解,这样才能够及时的了解前端项目的健康状况。所以我们需要分析出一些必要的数据。 如:一段时间内,应用JS报错的走势(chart图表)、JS错误发生率、JS错误在PC端发生的概率、JS错误在IOS端发生的概率、JS错误在Android端发生的概率,以及JS错误的归类。 然后,我们再去其中的Js错误进行详细的分析,辅助我们排查出错的位置和发生错误的原因。 如:JS错误类型、 JS错误信息、JS错误堆栈、JS错误发生的位置以及相关位置的代码;JS错误发生的几率、浏览器的类型,版本号,设备机型等等辅助信息 一、JS Error 监控功能 (数据概览) 为了得到这些数据,我们需要在上传的时候将其分析出来。在众多日志分析中,很多字段及功能是重复通用的,所以应该将其封装起来。 // 设置日志对象类的通用属性 function setCommonProperty() { this.happenTime = new Date().getTime(); // 日志发生时间 this.webMonitorId = WEB_MONITOR_ID; // 用于区分应用的唯一标识(一个项目对应一个) this.simpleUrl = window.location.href.split('?')[0].replace('#', ''); // 页面的url this.customerKey = utils.getCustomerKey(); // 用于区分用户,所对应唯一的标识,清理本地数据后失效 this.pageKey = utils.getPageKey(); // 用于区分页面,所对应唯一的标识,每个新页面对应一个值 this.deviceName = DEVICE_INFO.deviceName; this.os = DEVICE_INFO.os + (DEVICE_INFO.osVersion ? " " + DEVICE_INFO.osVersion : ""); this.browserName = DEVICE_INFO.browserName; this.browserVersion = DEVICE_INFO.browserVersion; // TODO 位置信息, 待处理 this.monitorIp = ""; // 用户的IP地址 this.country = "china"; // 用户所在国家 this.province = ""; // 用户所在省份 this.city = ""; // 用户所在城市 // 用户自定义信息, 由开发者主动传入, 便于对线上进行准确定位 this.userId = USER_INFO.userId; this.firstUserParam = USER_INFO.firstUserParam; this.secondUserParam = USER_INFO.secondUserParam; } // JS错误日志,继承于日志基类MonitorBaseInfo function JavaScriptErrorInfo(uploadType, errorMsg, errorStack) { setCommonProperty.apply(this); this.uploadType = uploadType; this.errorMessage = encodeURIComponent(errorMsg); this.errorStack = errorStack; this.browserInfo = BROWSER_INFO; } JavaScriptErrorInfo.prototype = new MonitorBaseInfo();封装了一个Js错误对象JavaScriptErrorInfo,用以保存页面中产生的Js错误。其中,setCommonProperty用以设置所有日志对象的通用属性。 ...

July 6, 2019 · 3 min · jiezi

Java异常体系

在使用JdbcTemplate中queryForObject方法的时候抛出一个异常: org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0 queryForObject的内部逻辑是这样的:使用queryForObject时,会查询一个结果,当查询中结果多余一个或者没有都会抛出一个异常IncorrectResultSizeDataAccessException。这个向外抛出的异常为什么不需要我使用try-catch处理或者继续向外抛出呢?下面的图片展示这个异常的继承结构。 Google之后认识到所有运行时异常都可以不向外抛出。 Java异常体系 首先,Throwable标志这是一个异常。 其次,Throwable有两个子类分别是Error和Exception,Error表示的是JVM发生的异常,如内存溢出,这是应用自身程序本身无法处理的异常;而Exception则表示应用程序自身可以处理的异常。 最后,Exception的子类分为两类,一个是RuntimeException,另外就是其他继承自Exception的异常,如IOException。区分这两类异常主要特性是是否受检。 什么是受检异常,什么是非受检异常?受检异常指的是非受检异常指的是因为代码的逻辑问题而导致的异常。参考:Java 的 Checked 和 Unchecked Exception【译】

June 3, 2019 · 1 min · jiezi

存疑-JVMCFRE003-bad-major-version-offset6

今天Deploy静态资源包到我们Production环境, package中只涉及到静态资源文件, 例如数据库连接信息, SQL查询语句等. 成功部署后, 启动服务报错如下: Exception in thread "main" java.lang.UnsupportedClassVersionError: JVMCFRE003 bad major version; class=support/operations/gtm/iosbackendmanager/BackendManager, offset=6 at java.lang.ClassLoader.defineClassImpl(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:324) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:155) at java.net.URLClassLoader.defineClass(URLClassLoader.java:715) at java.net.URLClassLoader.access$400(URLClassLoader.java:94) at java.net.URLClassLoader$ClassFinder.run(URLClassLoader.java:1169) at java.security.AccessController.doPrivileged(AccessController.java:492) at java.net.URLClassLoader.findClass(URLClassLoader.java:598) at java.lang.ClassLoader.loadClassHelper(ClassLoader.java:777) at java.lang.ClassLoader.loadClass(ClassLoader.java:750) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:341) at java.lang.ClassLoader.loadClass(ClassLoader.java:731) at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)</verbosegc>经过分析和排查, 我们认为是本地RAD(eclipse的一种)的JDK level问题, server上java版本是1.7, Ant打包涉及的source和target均是1.7, 但本地RAD的JDK编译版本是1.8, 将其改为1.7, 打包部署后, 进程正常启动. 对这个问题, 网络上普遍的说法是"是因为我们使用高版本的JDK编译的Java class文件试图在较低版本的JVM上运行" , 所以解决办法通常都是更改本地JDK编译环境, 很典型的例子是这个: UnsupportedClassVersionError: JVMCFRE003 bad major version in WebSphere AS 7 . 但在这个case里, 我deploy的部分不涉及任何已编译的class.所以在此存在两个疑问: ...

April 29, 2019 · 1 min · jiezi

Laravel异常捕获处理和创建

很多开发者在开发过程中都会遇到异常,处理过程大同小异:捕获然后处理,事实上也确实是如此。但本文不打算谈太多错误与异常的原理,只是从laravel自带的Exception入手,谈一谈怎样用一个更好的方式处理错误信息。 异常先举个简单的例子,在laravel中,如果一个Model找不到或者没有,很容易就抛出一个异常,大家常见的Whoops, something went wrong诸如此类。这也只是在APP_DEBUG=false的情况下,但这并不能带给用户更有用的信息。 User::findOrFail(1);findOrFail方法在Model没有的情况下会显示:Sorry, the page you are looking for could not be found.。这是一个404的错误页面,很多时候都应该这样返回,如果我们想知道更多有用的信息呢? try...catch我在工作中也喜欢用try catch来处理可能会抛出的异常,也建议大家这么做。好处是及时捕获不可预知的错误,给用户一个更好的体验。简单的demo,如下 try { $user = User::findOrFail(1); } catch (ModelNotFoundException $exception) { return back()->withError($exception->getMessage())->withInput(); }我们也可以这样: if (! User::find(1)) { throw new ModelNotFoundException('...', 404);}自定义异常Laravel框架允许我们自定义exception执行命令 php artisan make:exception UserNotFoundException系统会自动在Exceptions目录下创建一个UserNotFoundException类,这个类继承了Exception,这就给了我们一个自由发挥的机会 namespace App\Exceptions;use Exception;class UserNotFoundException extends Exception{ public function render($request, $e) { if ($request->expectsJson()) { // 如果是ajax请求... } return redirect()->to('...'); }}判断异常在Exceptions中的Handle.php文件中,我们看到有个render()方法,这里就是我们判断自定义异常的地方 // Handle.phpif ($exception instanceof UserNotFoundException) { return $exception->render($exception, $request);}可以看到,我们只需要判断抛出的异常是否是UserNotFoundException的实例即可。而在UserNotFoundException类中,我们也可以自定义返回的数据格式和状态码等等。在工作中,我个人比较需要建一些自定义的异常类,也会很好管理。 ...

April 27, 2019 · 1 min · jiezi

探索Java日志的奥秘:底层日志系统-log4j2

前言log4j2是apache在log4j的基础上,参考logback架构实现的一套新的日志系统(我感觉是apache害怕logback了)。log4j2的官方文档上写着一些它的优点:在拥有全部logback特性的情况下,还修复了一些隐藏问题API 分离:现在log4j2也是门面模式使用日志,默认的日志实现是log4j2,当然你也可以用logback(应该没有人会这么做)性能提升:log4j2包含下一代基于LMAX Disruptor library的异步logger,在多线程场景下,拥有18倍于log4j和logback的性能多API支持:log4j2提供Log4j 1.2, SLF4J, Commons Logging and java.util.logging (JUL) 的API支持避免锁定:使用Log4j2 API的应用程序始终可以选择使用任何符合SLF4J的库作为log4j-to-slf4j适配器的记录器实现自动重新加载配置:与Logback一样,Log4j 2可以在修改时自动重新加载其配置。与Logback不同,它会在重新配置发生时不会丢失日志事件。高级过滤: 与Logback一样,Log4j 2支持基于Log事件中的上下文数据,标记,正则表达式和其他组件进行过滤。插件架构: Log4j使用插件模式配置组件。因此,您无需编写代码来创建和配置Appender,Layout,Pattern Converter等。Log4j自动识别插件并在配置引用它们时使用它们。属性支持:您可以在配置中引用属性,Log4j将直接替换它们,或者Log4j将它们传递给将动态解析它们的底层组件。Java 8 Lambda支持自定义日志级别产生垃圾少:在稳态日志记录期间,Log4j 2 在独立应用程序中是无垃圾的,在Web应用程序中是低垃圾。这减少了垃圾收集器的压力,并且可以提供更好的响应时间性能。和应用server集成:版本2.10.0引入了一个模块log4j-appserver,以改进与Apache Tomcat和Eclipse Jetty的集成。Log4j2类图:这次从四个地方去探索源码:启动,配置,异步,插件化源码探索启动log4j2的关键组件LogManager根据配置指定LogContexFactory,初始化对应的LoggerContextLoggerContext1、解析配置文件,解析为对应的java对象。2、通过LoggerRegisty缓存Logger配置3、Configuration配置信息4、start方法解析配置文件,转化为对应的java对象5、通过getLogger获取logger对象LoggerLogManaer该组件是Log4J启动的入口,后续的LoggerContext以及Logger都是通过调用LogManager的静态方法获得。我们可以使用下面的代码获取LoggerLogger logger = LogManager.getLogger();可以看出LogManager是十分关键的组件,因此在这个小节中我们详细分析LogManager的启动流程。LogManager启动的入口是下面的static代码块:/** * Scans the classpath to find all logging implementation. Currently, only one will be used but this could be * extended to allow multiple implementations to be used. / static { // Shortcut binding to force a specific logging implementation. final PropertiesUtil managerProps = PropertiesUtil.getProperties(); final String factoryClassName = managerProps.getStringProperty(FACTORY_PROPERTY_NAME); if (factoryClassName != null) { try { factory = LoaderUtil.newCheckedInstanceOf(factoryClassName, LoggerContextFactory.class); } catch (final ClassNotFoundException cnfe) { LOGGER.error(“Unable to locate configured LoggerContextFactory {}”, factoryClassName); } catch (final Exception ex) { LOGGER.error(“Unable to create configured LoggerContextFactory {}”, factoryClassName, ex); } } if (factory == null) { final SortedMap<Integer, LoggerContextFactory> factories = new TreeMap<>(); // note that the following initial call to ProviderUtil may block until a Provider has been installed when // running in an OSGi environment if (ProviderUtil.hasProviders()) { for (final Provider provider : ProviderUtil.getProviders()) { final Class<? extends LoggerContextFactory> factoryClass = provider.loadLoggerContextFactory(); if (factoryClass != null) { try { factories.put(provider.getPriority(), factoryClass.newInstance()); } catch (final Exception e) { LOGGER.error(“Unable to create class {} specified in provider URL {}”, factoryClass.getName(), provider .getUrl(), e); } } } if (factories.isEmpty()) { LOGGER.error(“Log4j2 could not find a logging implementation. " + “Please add log4j-core to the classpath. Using SimpleLogger to log to the console…”); factory = new SimpleLoggerContextFactory(); } else if (factories.size() == 1) { factory = factories.get(factories.lastKey()); } else { final StringBuilder sb = new StringBuilder(“Multiple logging implementations found: \n”); for (final Map.Entry<Integer, LoggerContextFactory> entry : factories.entrySet()) { sb.append(“Factory: “).append(entry.getValue().getClass().getName()); sb.append(”, Weighting: “).append(entry.getKey()).append(’\n’); } factory = factories.get(factories.lastKey()); sb.append(“Using factory: “).append(factory.getClass().getName()); LOGGER.warn(sb.toString()); } } else { LOGGER.error(“Log4j2 could not find a logging implementation. " + “Please add log4j-core to the classpath. Using SimpleLogger to log to the console…”); factory = new SimpleLoggerContextFactory(); } } }这段静态代码段主要分为下面的几个步骤:首先根据特定配置文件的配置信息获取loggerContextFactory如果没有找到对应的Factory的实现类则通过ProviderUtil中的getProviders()方法载入providers,随后通过provider的loadLoggerContextFactory方法载入LoggerContextFactory的实现类如果provider中没有获取到LoggerContextFactory的实现类或provider为空,则使用SimpleLoggerContextFactory作为LoggerContextFactory。根据配置文件载入LoggerContextFactory// Shortcut binding to force a specific logging implementation. final PropertiesUtil managerProps = PropertiesUtil.getProperties(); final String factoryClassName = managerProps.getStringProperty(FACTORY_PROPERTY_NAME); if (factoryClassName != null) { try { factory = LoaderUtil.newCheckedInstanceOf(factoryClassName, LoggerContextFactory.class); } catch (final ClassNotFoundException cnfe) { LOGGER.error(“Unable to locate configured LoggerContextFactory {}”, factoryClassName); } catch (final Exception ex) { LOGGER.error(“Unable to create configured LoggerContextFactory {}”, factoryClassName, ex); } }在这段逻辑中,LogManager优先通过配置文件”log4j2.component.properties”通过配置项”log4j2.loggerContextFactory”来获取LoggerContextFactory,如果用户做了对应的配置,通过newCheckedInstanceOf方法实例化LoggerContextFactory的对象,最终的实现方式为:public static <T> T newInstanceOf(final Class<T> clazz) throws InstantiationException, IllegalAccessException, InvocationTargetException { try { return clazz.getConstructor().newInstance(); } catch (final NoSuchMethodException ignored) { // FIXME: looking at the code for Class.newInstance(), this seems to do the same thing as above return clazz.newInstance(); } }在默认情况下,不存在初始的默认配置文件log4j2.component.properties,因此需要从其他途径获取LoggerContextFactory。通过Provider实例化LoggerContextFactory对象代码:if (factory == null) { final SortedMap<Integer, LoggerContextFactory> factories = new TreeMap<>(); // note that the following initial call to ProviderUtil may block until a Provider has been installed when // running in an OSGi environment if (ProviderUtil.hasProviders()) { for (final Provider provider : ProviderUtil.getProviders()) { final Class<? extends LoggerContextFactory> factoryClass = provider.loadLoggerContextFactory(); if (factoryClass != null) { try { factories.put(provider.getPriority(), factoryClass.newInstance()); } catch (final Exception e) { LOGGER.error(“Unable to create class {} specified in provider URL {}”, factoryClass.getName(), provider .getUrl(), e); } } } if (factories.isEmpty()) { LOGGER.error(“Log4j2 could not find a logging implementation. " + “Please add log4j-core to the classpath. Using SimpleLogger to log to the console…”); factory = new SimpleLoggerContextFactory(); } else if (factories.size() == 1) { factory = factories.get(factories.lastKey()); } else { final StringBuilder sb = new StringBuilder(“Multiple logging implementations found: \n”); for (final Map.Entry<Integer, LoggerContextFactory> entry : factories.entrySet()) { sb.append(“Factory: “).append(entry.getValue().getClass().getName()); sb.append(”, Weighting: “).append(entry.getKey()).append(’\n’); } factory = factories.get(factories.lastKey()); sb.append(“Using factory: “).append(factory.getClass().getName()); LOGGER.warn(sb.toString()); } } else { LOGGER.error(“Log4j2 could not find a logging implementation. " + “Please add log4j-core to the classpath. Using SimpleLogger to log to the console…”); factory = new SimpleLoggerContextFactory(); } }这里比较有意思的是hasProviders和getProviders都会通过线程安全的方式去懒加载ProviderUtil这个对象。跟进lazyInit方法:protected static void lazyInit() { //noinspection DoubleCheckedLocking if (INSTANCE == null) { try { STARTUP_LOCK.lockInterruptibly(); if (INSTANCE == null) { INSTANCE = new ProviderUtil(); } } catch (final InterruptedException e) { LOGGER.fatal(“Interrupted before Log4j Providers could be loaded.”, e); Thread.currentThread().interrupt(); } finally { STARTUP_LOCK.unlock(); } } }再看构造方法:private ProviderUtil() { for (final LoaderUtil.UrlResource resource : LoaderUtil.findUrlResources(PROVIDER_RESOURCE)) { loadProvider(resource.getUrl(), resource.getClassLoader()); } }这里的懒加载其实就是懒加载Provider对象。在创建新的providerUtil实例的过程中就会直接实例化provider对象,其过程是先通过getClassLoaders方法获取provider的类加载器,然后通过loadProviders(classLoader);加载类。在providerUtil实例化的最后,会统一查找”META-INF/log4j-provider.properties”文件中对应的provider的url,会考虑从远程加载provider。而loadProviders方法就是在ProviderUtil的PROVIDERS列表中添加对一个的provider。可以看到默认的provider是org.apache.logging.log4j.core.impl.Log4jContextFactoryLoggerContextFactory = org.apache.logging.log4j.core.impl.Log4jContextFactoryLog4jAPIVersion = 2.1.0FactoryPriority= 10很有意思的是这里懒加载加上了锁,而且使用的是lockInterruptibly这个方法。lockInterruptibly和lock的区别如下:lock 与 lockInterruptibly比较区别在于:lock 优先考虑获取锁,待获取锁成功后,才响应中断。lockInterruptibly 优先考虑响应中断,而不是响应锁的普通获取或重入获取。ReentrantLock.lockInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt 方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。 ReentrantLock.lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。上面有一句注释值得注意:/* * Guards the ProviderUtil singleton instance from lazy initialization. This is primarily used for OSGi support. * * @since 2.1 / protected static final Lock STARTUP_LOCK = new ReentrantLock(); // STARTUP_LOCK guards INSTANCE for lazy initialization; this allows the OSGi Activator to pause the startup and // wait for a Provider to be installed. See LOG4J2-373 private static volatile ProviderUtil INSTANCE;原来这里是为了让osgi可以阻止启动。再回到logManager:可以看到在加载完Provider之后,会做factory的绑定:if (factories.isEmpty()) { LOGGER.error(“Log4j2 could not find a logging implementation. " + “Please add log4j-core to the classpath. Using SimpleLogger to log to the console…”); factory = new SimpleLoggerContextFactory(); } else if (factories.size() == 1) { factory = factories.get(factories.lastKey()); } else { final StringBuilder sb = new StringBuilder(“Multiple logging implementations found: \n”); for (final Map.Entry<Integer, LoggerContextFactory> entry : factories.entrySet()) { sb.append(“Factory: “).append(entry.getValue().getClass().getName()); sb.append(”, Weighting: “).append(entry.getKey()).append(’\n’); } factory = factories.get(factories.lastKey()); sb.append(“Using factory: “).append(factory.getClass().getName()); LOGGER.warn(sb.toString()); }到这里,logmanager的启动流程就结束了。配置在不使用slf4j的情况下,我们获取logger的方式是这样的:Logger logger = logManager.getLogger(xx.class)跟进getLogger方法: public static Logger getLogger(final Class<?> clazz) { final Class<?> cls = callerClass(clazz); return getContext(cls.getClassLoader(), false).getLogger(toLoggerName(cls)); }这里有一个getContext方法,跟进,public static LoggerContext getContext(final ClassLoader loader, final boolean currentContext) { try { return factory.getContext(FQCN, loader, null, currentContext); } catch (final IllegalStateException ex) { LOGGER.warn(ex.getMessage() + " Using SimpleLogger”); return new SimpleLoggerContextFactory().getContext(FQCN, loader, null, currentContext); } }上文提到factory的具体实现是Log4jContextFactory,跟进getContext方法:public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext, final boolean currentContext) { final LoggerContext ctx = selector.getContext(fqcn, loader, currentContext); if (externalContext != null && ctx.getExternalContext() == null) { ctx.setExternalContext(externalContext); } if (ctx.getState() == LifeCycle.State.INITIALIZED) { ctx.start(); } return ctx; }直接看start:public void start() { LOGGER.debug(“Starting LoggerContext[name={}, {}]…”, 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={}, {}] started OK.”, getName(), this); }发现其中的核心方法是reconfigure方法,继续跟进:private void reconfigure(final URI configURI) { final ClassLoader cl = ClassLoader.class.isInstance(externalContext) ? (ClassLoader) externalContext : null; LOGGER.debug(“Reconfiguration started for context[name={}] at URI {} ({}) with optional ClassLoader: {}”, contextName, configURI, this, cl); final Configuration instance = ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl); if (instance == null) { LOGGER.error(“Reconfiguration failed: No configuration found for ‘{}’ at ‘{}’ in ‘{}’”, contextName, configURI, cl); } else { setConfiguration(instance); / * instance.start(); Configuration old = setConfiguration(instance); updateLoggers(); if (old != null) { * old.stop(); } / final String location = configuration == null ? “?” : String.valueOf(configuration.getConfigurationSource()); LOGGER.debug(“Reconfiguration complete for context[name={}] at URI {} ({}) with optional ClassLoader: {}”, contextName, location, this, cl); } }可以看到每一个configuration都是从ConfigurationFactory拿出来的,我们先看看这个类的getInstance看看:public static ConfigurationFactory getInstance() { // volatile works in Java 1.6+, so double-checked locking also works properly //noinspection DoubleCheckedLocking if (factories == null) { LOCK.lock(); try { if (factories == null) { final List<ConfigurationFactory> list = new ArrayList<ConfigurationFactory>(); final String factoryClass = PropertiesUtil.getProperties().getStringProperty(CONFIGURATION_FACTORY_PROPERTY); if (factoryClass != null) { addFactory(list, factoryClass); } final PluginManager manager = new PluginManager(CATEGORY); manager.collectPlugins(); final Map<String, PluginType<?>> plugins = manager.getPlugins(); final List<Class<? extends ConfigurationFactory>> ordered = new ArrayList<Class<? extends ConfigurationFactory>>(plugins.size()); for (final PluginType<?> type : plugins.values()) { try { ordered.add(type.getPluginClass().asSubclass(ConfigurationFactory.class)); } catch (final Exception ex) { LOGGER.warn(“Unable to add class {}”, type.getPluginClass(), ex); } } Collections.sort(ordered, OrderComparator.getInstance()); for (final Class<? extends ConfigurationFactory> clazz : ordered) { addFactory(list, clazz); } // see above comments about double-checked locking //noinspection NonThreadSafeLazyInitialization factories = Collections.unmodifiableList(list); } } finally { LOCK.unlock(); } } LOGGER.debug(“Using configurationFactory {}”, configFactory); return configFactory; }这里可以看到ConfigurationFactory中利用了PluginManager来进行初始化,PluginManager会将ConfigurationFactory的子类加载进来,默认使用的XmlConfigurationFactory,JsonConfigurationFactory,YamlConfigurationFactory这三个子类,这里插件化加载暂时按下不表。回到reconfigure这个方法,我们看到获取ConfigurationFactory实例之后会去调用getConfiguration方法:public Configuration getConfiguration(final String name, final URI configLocation, final ClassLoader loader) { if (!isActive()) { return null; } if (loader == null) { return getConfiguration(name, configLocation); } if (isClassLoaderUri(configLocation)) { final String path = extractClassLoaderUriPath(configLocation); final ConfigurationSource source = getInputFromResource(path, loader); if (source != null) { final Configuration configuration = getConfiguration(source); if (configuration != null) { return configuration; } } } return getConfiguration(name, configLocation); }跟进getConfiguration,这里值得注意的是有很多个getConfiguration,注意甄别,如果不确定的话可以通过debug的方式来确定。public Configuration getConfiguration(final String name, final URI configLocation) { if (configLocation == null) { final String config = this.substitutor.replace( PropertiesUtil.getProperties().getStringProperty(CONFIGURATION_FILE_PROPERTY)); if (config != null) { ConfigurationSource source = null; try { source = getInputFromUri(FileUtils.getCorrectedFilePathUri(config)); } catch (final Exception ex) { // Ignore the error and try as a String. LOGGER.catching(Level.DEBUG, ex); } if (source == null) { final ClassLoader loader = LoaderUtil.getThreadContextClassLoader(); source = getInputFromString(config, loader); } if (source != null) { for (final ConfigurationFactory factory : factories) { final String[] types = factory.getSupportedTypes(); if (types != null) { for (final String type : types) { if (type.equals(””) || config.endsWith(type)) { final Configuration c = factory.getConfiguration(source); if (c != null) { return c; } } } } } } } } else { for (final ConfigurationFactory factory : factories) { final String[] types = factory.getSupportedTypes(); if (types != null) { for (final String type : types) { if (type.equals(”*”) || configLocation.toString().endsWith(type)) { final Configuration config = factory.getConfiguration(name, configLocation); if (config != null) { return config; } } } } } } Configuration config = getConfiguration(true, name); if (config == null) { config = getConfiguration(true, null); if (config == null) { config = getConfiguration(false, name); if (config == null) { config = getConfiguration(false, null); } } } if (config != null) { return config; } LOGGER.error(“No log4j2 configuration file found. Using default configuration: logging only errors to the console.”); return new DefaultConfiguration(); }这里就会根据之前加载进来的factory进行配置的获取,具体的不再解析。回到reconfigure,之后的步骤就是setConfiguration,入参就是刚才获取的configprivate synchronized Configuration setConfiguration(final Configuration config) { Assert.requireNonNull(config, “No Configuration was provided”); final Configuration prev = this.config; config.addListener(this); final ConcurrentMap<String, String> map = config.getComponent(Configuration.CONTEXT_PROPERTIES); try { // LOG4J2-719 network access may throw android.os.NetworkOnMainThreadException map.putIfAbsent(“hostName”, NetUtils.getLocalHostname()); } catch (final Exception ex) { LOGGER.debug(“Ignoring {}, setting hostName to ‘unknown’”, ex.toString()); map.putIfAbsent(“hostName”, “unknown”); } map.putIfAbsent(“contextName”, name); config.start(); this.config = config; updateLoggers(); if (prev != null) { prev.removeListener(this); prev.stop(); } firePropertyChangeEvent(new PropertyChangeEvent(this, PROPERTY_CONFIG, prev, config)); try { Server.reregisterMBeansAfterReconfigure(); } catch (final Throwable t) { // LOG4J2-716: Android has no java.lang.management LOGGER.error(“Could not reconfigure JMX”, t); } return prev; }这个方法最重要的步骤就是config.start,这才是真正做配置解析的public void start() { LOGGER.debug(“Starting configuration {}”, this); this.setStarting(); pluginManager.collectPlugins(pluginPackages); final PluginManager levelPlugins = new PluginManager(Level.CATEGORY); levelPlugins.collectPlugins(pluginPackages); final Map<String, PluginType<?>> plugins = levelPlugins.getPlugins(); if (plugins != null) { for (final PluginType<?> type : plugins.values()) { try { // Cause the class to be initialized if it isn’t already. Loader.initializeClass(type.getPluginClass().getName(), type.getPluginClass().getClassLoader()); } catch (final Exception e) { LOGGER.error(“Unable to initialize {} due to {}”, type.getPluginClass().getName(), e.getClass() .getSimpleName(), e); } } } setup(); setupAdvertisement(); doConfigure(); final Set<LoggerConfig> alreadyStarted = new HashSet<LoggerConfig>(); for (final LoggerConfig logger : loggers.values()) { logger.start(); alreadyStarted.add(logger); } for (final Appender appender : appenders.values()) { appender.start(); } if (!alreadyStarted.contains(root)) { // LOG4J2-392 root.start(); // LOG4J2-336 } super.start(); LOGGER.debug(“Started configuration {} OK.”, this); }这里面有如下步骤:获取日志等级的插件初始化初始化Advertiser配置先看一下初始化,也就是setup这个方法,setup是一个需要被复写的方法,我们以XMLConfiguration作为例子,@Override public void setup() { if (rootElement == null) { LOGGER.error(“No logging configuration”); return; } constructHierarchy(rootNode, rootElement); if (status.size() > 0) { for (final Status s : status) { LOGGER.error(“Error processing element {}: {}”, s.name, s.errorType); } return; } rootElement = null; }发现这里面有一个比较重要的方法constructHierarchy,跟进:private void constructHierarchy(final Node node, final Element element) { processAttributes(node, element); final StringBuilder buffer = new StringBuilder(); final NodeList list = element.getChildNodes(); final List<Node> children = node.getChildren(); for (int i = 0; i < list.getLength(); i++) { final org.w3c.dom.Node w3cNode = list.item(i); if (w3cNode instanceof Element) { final Element child = (Element) w3cNode; final String name = getType(child); final PluginType<?> type = pluginManager.getPluginType(name); final Node childNode = new Node(node, name, type); constructHierarchy(childNode, child); if (type == null) { final String value = childNode.getValue(); if (!childNode.hasChildren() && value != null) { node.getAttributes().put(name, value); } else { status.add(new Status(name, element, ErrorType.CLASS_NOT_FOUND)); } } else { children.add(childNode); } } else if (w3cNode instanceof Text) { final Text data = (Text) w3cNode; buffer.append(data.getData()); } } final String text = buffer.toString().trim(); if (text.length() > 0 || (!node.hasChildren() && !node.isRoot())) { node.setValue(text); } }发现这个就是一个树遍历的过程。诚然,配置文件是以xml的形式给出的,xml的结构就是一个树形结构。回到start方法,跟进doConfiguration:protected void doConfigure() { if (rootNode.hasChildren() && rootNode.getChildren().get(0).getName().equalsIgnoreCase(“Properties”)) { final Node first = rootNode.getChildren().get(0); createConfiguration(first, null); if (first.getObject() != null) { subst.setVariableResolver((StrLookup) first.getObject()); } } else { final Map<String, String> map = this.getComponent(CONTEXT_PROPERTIES); final StrLookup lookup = map == null ? null : new MapLookup(map); subst.setVariableResolver(new Interpolator(lookup, pluginPackages)); } boolean setLoggers = false; boolean setRoot = false; for (final Node child : rootNode.getChildren()) { if (child.getName().equalsIgnoreCase(“Properties”)) { if (tempLookup == subst.getVariableResolver()) { LOGGER.error(“Properties declaration must be the first element in the configuration”); } continue; } createConfiguration(child, null); if (child.getObject() == null) { continue; } if (child.getName().equalsIgnoreCase(“Appenders”)) { appenders = child.getObject(); } else if (child.isInstanceOf(Filter.class)) { addFilter(child.getObject(Filter.class)); } else if (child.getName().equalsIgnoreCase(“Loggers”)) { final Loggers l = child.getObject(); loggers = l.getMap(); setLoggers = true; if (l.getRoot() != null) { root = l.getRoot(); setRoot = true; } } else if (child.getName().equalsIgnoreCase(“CustomLevels”)) { customLevels = child.getObject(CustomLevels.class).getCustomLevels(); } else if (child.isInstanceOf(CustomLevelConfig.class)) { final List<CustomLevelConfig> copy = new ArrayList<CustomLevelConfig>(customLevels); copy.add(child.getObject(CustomLevelConfig.class)); customLevels = copy; } else { LOGGER.error(“Unknown object "{}" of type {} is ignored.”, child.getName(), child.getObject().getClass().getName()); } } if (!setLoggers) { LOGGER.warn(“No Loggers were configured, using default. Is the Loggers element missing?”); setToDefault(); return; } else if (!setRoot) { LOGGER.warn(“No Root logger was configured, creating default ERROR-level Root logger with Console appender”); setToDefault(); // return; // LOG4J2-219: creating default root=ok, but don’t exclude configured Loggers } for (final Map.Entry<String, LoggerConfig> entry : loggers.entrySet()) { final LoggerConfig l = entry.getValue(); for (final AppenderRef ref : l.getAppenderRefs()) { final Appender app = appenders.get(ref.getRef()); if (app != null) { l.addAppender(app, ref.getLevel(), ref.getFilter()); } else { LOGGER.error(“Unable to locate appender {} for logger {}”, ref.getRef(), l.getName()); } } } setParents(); }发现就是对刚刚获取的configuration进行解析,然后塞进正确的地方。回到start方法,可以看到昨晚配置之后就是开启logger和appender了。异步AsyncAppenderlog4j2突出于其他日志的优势,异步日志实现。我们先从日志打印看进去。找到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-2031 tryLogMessage(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 logger handleLogMessageException(e, fqcn, msg); } }继续跟进:@Override public 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,跟进看实际打日志的方法@Override public 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. / @Override public 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 deadlock AsyncQueueFullMessageUtil.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); } } }这里主要的步骤就是:生成logEvent将logEvent放入BlockingQueue,就是transfer方法如果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); } @Override public 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方法:阻塞获取logEvent将logEvent分发出去如果线程要退出了,将blockingQueue里面的event消费完在退出。AsyncLogger直接从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 apps logWithVarargTranslator(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 implements SequenceReportingEventHandler<RingBufferLogEvent>, LifecycleAware { private static final int NOTIFY_PROGRESS_THRESHOLD = 50; private Sequence sequenceCallback; private int counter; private long threadId = -1; @Override public void setSequenceCallback(final Sequence sequenceCallback) { this.sequenceCallback = sequenceCallback; } @Override public 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; } @Override public void onStart() { threadId = Thread.currentThread().getId(); } @Override public 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 CLASSPATH Map<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 Bundles for (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; }处理逻辑如下:从Log4j2Plugin.dat中加载所有的内置的plugin然后将OSGi Bundles中的Log4j2Plugin.dat中的plugin加载进来再加载传入的package路径中的plugin最后加载配置中的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(); @Override public 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的代码看完了,发现它的设计理念很值得借鉴,为了灵活性,所有的东西都设计成插件式。互联网技术日益发展,各种中间件层出不穷,而作为工程师的我们更需要做的是去思考代码与代码之间的关系,毫无疑问的是,解耦是最具有美感的关系。本文作者:netflix阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 10, 2019 · 19 min · jiezi

CodeIgniter3.0+框架自定义异常处理实现

背景ci3.0框架核心代码自动实现了异常,并实现了抛出的对应页面和方法,对于一些个性化需求特别是接口类型的应用,会不合适。因此需要在不改版核心代码 (system目录下文件),来改变对异常及404等相关异常的处理。方法说明ci框架3.0比2.0有比较大的改动,其中之一就是对异常的处理。以下是CodeIgniter-3.1.8\system\core\CodeIgniter.php 中对异常处理的部分代码/* * —————————————————— * Define a custom error handler so we can log PHP errors * —————————————————— / set_error_handler(’_error_handler’); set_exception_handler(’_exception_handler’); register_shutdown_function(’_shutdown_handler’);…以上括号内的方法均在common.php中以function_exists为前提声明。…if ( ! function_exists(’_exception_handler’)){…代码实现我们简单粗暴的在项目入口文件index.php中重写以下方法/* * 推送到redis 异常队列 * @time 2019/3/21 15:29 * @author * @param $msg * @return bool|int|string /function redis_list_add($msg){ ini_set(‘default_socket_timeout’, -1); $v = explode(’:’, $_SERVER[‘SITE_REDIS_SERVER’]); if (is_array($v) && !empty($v)) { try { $redis = new redis(); $redis->pconnect($v[0], $v[1]); $trace = $_SERVER[‘SERVER_NAME’] . " exception\n"; $trace .= “clint ip is {$_SERVER[‘REMOTE_ADDR’]} " . “,server is " . $_SERVER[‘SERVER_NAME’] . “(” . $_SERVER[‘SERVER_ADDR’] . “)”."\n”; $trace.= “path is “.(isset($_SERVER[‘REQUEST_URI’])?$_SERVER[‘REQUEST_URI’]:“empty”)."\n”; $trace .= “request params is =” . print_r($_POST, true); return $redis->LPUSH(‘CC_PHP_ERROR_WARNING’, $trace . $msg); } catch (Exception $e) { return $e->getMessage(); } }}/* * 优先重写common.php中对应方法 * @time 2019/3/21 16:19 * @author * @param $severity * @param $message * @param $filepath * @param $line /function _error_handler($severity, $message, $filepath, $line){ $is_error = (((E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR | E_USER_ERROR | E_STRICT) & $severity) === $severity); if ($is_error) { $error_msg = ($message . “\n” . $filepath . “\n” . $line); redis_list_add($error_msg); exit(json_encode([‘success’ => ‘-1’, ‘code’ => 501, ‘msg’ => ’error’])); }}/* * 捕获php本身语法,对象调用,参数类型传递等错误 * 优先重写common.php中对应方法 * ParseError,object(Error),TypeError,Error * @time 2019/3/20 18:33 * @author * @param $exception /function _exception_handler($exception){ $_tmp =& load_class(‘Exceptions’, ‘core’); if (!empty($exception)) { $error_msg = ($exception->getMessage() . “\n” . $exception->getTraceAsString()); redis_list_add($error_msg); exit(json_encode([‘success’ => ‘-1’, ‘code’ => 501, ‘msg’ => ’exception’])); }}/* * 优先重写common.php中对应方法 * require_once(’no_exists.php’) * @time 2019/3/21 9:49 * @author /function _shutdown_handler(){ $last_error = error_get_last(); if (isset($last_error) && ($last_error[’type’] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING))) { redis_list_add($msg = $last_error[‘message’] . “\n” . $last_error[‘file’] . “\n” . $last_error[’line’] . “\n”); exit(json_encode([‘success’ => ‘-1’, ‘code’ => 501, ‘msg’ => ‘shutdown’])); }}/* * 优先重写common.php中对应方法 * ci 框架内部的load异常、config异常、loader异常等会自动抛出, * 但common.php中的函数定义之类错误无法捕捉 * @time 2019/3/20 18:46 * @author * @param $message * @param int $status_code /function show_error($message){ redis_list_add($message); exit(json_encode([‘success’ => ‘-1’, ‘code’ => ‘503’, ‘msg’ => ‘ci_exception_1’]));}/* * 优先重写common.php中对应方法 * @time 2019/3/21 15:34 * @author * @param string $page */function show_404($page = ‘’){ redis_list_add(“url: " . $page . " not found”); exit(json_encode([‘success’ => ‘-1’, ‘code’ => ‘404’, ‘msg’ => ‘Not Found’]));}延伸在基类中可以处理错误等级区分对待 ...

March 27, 2019 · 2 min · jiezi

Laravel 全局异常错误处理源码解析及使用场景

如果没有全局的异常错误拦截器,那我们在每个可能发生错误异常的业务逻辑分支中,都要使用 try … catch,然后将执行结果返回 Controller层,再由其根据结果来构造相应的 Response,那代码冗余的会相当可以。全局异常错误处理,是每个框架都应该具备的,这次我们就通过简析 Laravel 的源码和执行流程,来看一下此模式是如何被运用的。源码解析laravel/laravel 脚手架中有一个预定义好的异常处理器:app/Exceptions/Handler.phpnamespace App\Exceptions;use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;class Handler extends ExceptionHandler{ // 不被处理的异常错误 protected $dontReport = []; // 认证异常时不被flashed的数据 protected $dontFlash = [ ‘password’, ‘password_confirmation’, ]; // 上报异常至错误driver,如日志文件(storage/logs/laravel.log),第三方日志存储分析平台 public function report(Exception $exception) { parent::report($exception); } // 将异常信息响应给客户端 public function render($request, Exception $exception) { return parent::render($request, $exception); }}当 Laravel 处理一次请求时,在启动文件中注册了以下服务:bootstrap/app.php// 绑定 http 服务提供者$app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);// 绑定 cli 服务提供者$app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class);// 这里将异常处理器的服务提供者绑定到了 App\Exceptions\Handler::class$app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class);而后进入请求捕获,处理阶段:public/index.php// 使用 http 服务处理请求$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);// http 服务处理捕获的请求 $requeset$response = $kernel->handle( $request = Illuminate\Http\Request::capture());因Illuminate\Contracts\Http\Kernel::class 具体提供者是 App\Http\Kernel::class 继承至 Illuminate\Foundation\Http\Kernel::class,我们去其中看 http 服务 的 handle 方法是如何处理请求的。请求处理阶段:Illuminate\Foundation\Http\Kernel::class 的 handle 方法对请求做一次处理,如果没有异常则分发路由,如果有异常则调用 reportException 和 renderException 方法记录&渲染异常。具体处理者则是我们在 bootstrap/app.php 中注册绑定的异常处理服务 Illuminate\Contracts\Debug\ExceptionHandler::class 的 report & render,具体的服务即绑定的 App\Exceptions\Handler::class。public function handle($request){ try { // 没有异常 则进入路由分发 $request->enableHttpMethodParameterOverride(); $response = $this->sendRequestThroughRouter($request); } catch (Exception $e) { // 捕获异常 则 report & render $this->reportException($e); $response = $this->renderException($request, $e); } catch (Throwable $e) { $this->reportException($e = new FatalThrowableError($e)); $response = $this->renderException($request, $e); } $this->app[’events’]->dispatch( new Events\RequestHandled($request, $response) ); return $response;}//Report the exception to the exception handler.protected function reportException(Exception $e){ // 服务Illuminate\Contracts\Debug\ExceptionHandler::class 的 report 方法 $this->app[ExceptionHandler::class]->report($e);}//Render the exception to a response.protected function renderException($request, Exception $e){ // 服务Illuminate\Contracts\Debug\ExceptionHandler::class 的 render 方法 return $this->app[ExceptionHandler::class]->render($request, $e);}handler 方法作为请求处理的入口,后续的路由分发,用户业务调用(controller, model)等执行的上下文依然在此方法中,故异常也能在这一层被捕获。然后我们就可以在业务中通过 throw new CustomException($code, “错误异常描述”); 的方式将控制流程转交给全局异常处理器,由其解析异常并构建响应实体给客户端,这一模式在 Api服务 的开发中是效率极高的。laravel 的依赖中有 symfony 这个超级棒的组件库,symfony 为我们提供了详细的 Http 异常库,我们可以直接借用这些异常类(当然也可以自定义)laravel 有提供 abort 助手函数来实现创建一个异常错误,但主要面向 web 网站(因为laravel主要就是用来开发后台的嘛)的,对 Api 不太友好,而且看源码发现只顾及了 404 这货。/** * abort(401, “你需要登录”) * abort(403, “你登录了也白搭”) * abort(404, “页面找不到了”) * Throw an HttpException with the given data. * * @param int $code * @param string $message * @param array $headers * @return void * * @throws \Symfony\Component\HttpKernel\Exception\HttpException */public function abort($code, $message = ‘’, array $headers = []){ if ($code == 404) { throw new NotFoundHttpException($message); } throw new HttpException($code, $message, null, $headers);}即只有 404 用了具体的异常类去抛出,其他的状态码都一股脑的归为 HttpException,这样就不太方便我们在全局异常处理器的 render 中根据 Exception 的具体类型来分而治之了,但 abort 也的确是为了方便你调用具体的错误页面的 resources/views/errors/{statusCode.blade.php} 的,需要对 Api 友好自己改写吧。使用场景// 业务代码 不满足直接抛出异常即可if ("" = trim($username)) { throw new BadRequestHttpException(“用户名必须”);}// 全局处理器public function render($request, Exception $exception){ if ($exception instanceof BadRequestHttpException) { return response()->json([ “err” => 400, “msg” => $exception->getMessage() ]); } if ($exception instanceof AccessDeniedHttpException) { return response()->json([ “err” => 403, “msg” => “unauthorized” ]); } if ($exception instanceof NotFoundHttpException) { return response()->json([ “err” => 403, “msg” => “forbidden” ]); } if ($exception instanceof NotFoundHttpException) { return response()->json([ “err” => 404, “msg” => “not found” ]); } if ($exception instanceof MethodNotAllowedHttpException) { return response()->json([ “err” => 405, “msg” => “method not allowed” ]); } if ($exception instanceof MethodNotAllowedHttpException) { return response()->json([ “err” => 406, “msg” => “你想要的数据类型我特么给不了啊” ]); } if ($exception instanceof TooManyRequestsHttpException) { return response()->json([ “err” => 429, “msg” => “to many request” ]); } return parent::render($request, $exception);} ...

March 4, 2019 · 2 min · jiezi

猫头鹰的深夜翻译:Spring REST服务异常处理

前言这篇教程主要专注于如何优雅的处理WEB中的异常。虽然我们可以手动的设置ResponseStatus ,但是还有更加优雅的方式将这部分逻辑隔离开来。Spring提供了整个应用层面的异常处理的抽象,并且只是要求您添加一些注释 - 它会处理其他所有内容。下面是一些代码的示例如何手动处理异常下面的代码中, DogController将返回一个ResponseEntity实例,该实例中包含返回的数据和HttpStatus属性如果没有抛出任何异常,则下面的代码将会返回List<Dog>数据作为响应体,以及200作为状态码对于DogsNotFoundException,它返回空的响应体和404状态码对于DogServiceException, 它返回500状态码和空的响应体@RestController@RequestMapping("/dogs")public class DogsController { @Autowired private final DogsService service; @GetMapping public ResponseEntity<List<Dog>> getDogs() { List<Dog> dogs; try { dogs = service.getDogs(); } catch (DogsServiceException ex) { return new ResponseEntity<>(null, null, HttpStatus.INTERNAL_SERVER_ERROR); } catch (DogsNotFoundException ex) { return new ResponseEntity<>(null, null, HttpStatus.NOT_FOUND); } return new ResponseEntity<>(dogs, HttpStatus.OK); }}这种处理异常的方式最大的问题就在于代码的重复。catch部分的代码在很多其它地方也会使用到(比如删除,更新等操作)Controller AdviceSpring提供了一种更好的解决方法,也就是Controller Advice。它将处理异常的代码在应用层面上集中管理。现在我们的的DogsController的代码更加简单清晰了:import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;import static org.springframework.http.HttpStatus.NOT_FOUND;@ControllerAdvicepublic class DogsServiceErrorAdvice { @ExceptionHandler({RuntimeException.class}) public ResponseEntity<String> handleRunTimeException(RuntimeException e) { return error(INTERNAL_SERVER_ERROR, e); } @ExceptionHandler({DogsNotFoundException.class}) public ResponseEntity<String> handleNotFoundException(DogsNotFoundException e) { return error(NOT_FOUND, e); } @ExceptionHandler({DogsServiceException.class}) public ResponseEntity<String> handleDogsServiceException(DogsServiceException e){ return error(INTERNAL_SERVER_ERROR, e); } private ResponseEntity<String> error(HttpStatus status, Exception e) { log.error(“Exception : “, e); return ResponseEntity.status(status).body(e.getMessage()); }}handleRunTimeException:这个方法会处理所有的RuntimeException并返回INTERNAL_SERVER_ERROR状态码handleNotFoundException: 这个方法会处理DogsNotFoundException并返回NOT_FOUND状态码。handleDogsServiceException: 这个方法会处理DogServiceException并返回INTERNAL_SERVER_ERROR状态码这种实现的关键就在于在代码中捕获需检查异常并将其作为RuntimeException抛出。还可以用@ResponseStatus将异常映射成状态码@ControllerAdvicepublic class DogsServiceErrorAdvice { @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler({DogsNotFoundException.class}) public void handle(DogsNotFoundException e) {} @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler({DogsServiceException.class, SQLException.class, NullPointerException.class}) public void handle() {} @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler({DogsServiceValidationException.class}) public void handle(DogsServiceValidationException e) {}}在自定义的异常上添加状态码@ResponseStatus(HttpStatus.NOT_FOUND)public class DogsNotFoundException extends RuntimeException { public DogsNotFoundException(String message) { super(message); }} ...

January 27, 2019 · 1 min · jiezi

Java 中断异常的正确处理方式

处理InterruptedException这个故事可能很熟悉:你正在写一个测试程序,你需要暂停某个线程一段时间,所以你调用 Thread.sleep()。然后编译器或 IDE 就会抱怨说 InterruptedException 没有抛出声明或捕获。什么是 InterruptedException,你为什么要处理它?最常见的响应 InterruptedException 做法是吞下它 - 捕获它并且什么也不做(或者记录它,也没好多少) - 正如我们将在清单4中看到的那样。不幸的是,这种方法抛弃了关于中断发生的重要信息,这可能会损害应用程序取消活动或响应及时关闭的能力。阻塞方法当一个方法抛出 InterruptedException 时,意味着几件事情: 除了它可以抛出一个特定的检查异常, 它还告诉你它是一种阻塞方法,它会尝试解除阻塞并提前返回。阻塞方法不同于仅需要很长时间才能运行完成的普通方法。普通方法的完成仅取决于你要求它做多少事以及是否有足够的计算资源(CPU周期和内存)。另一方面,阻塞方法的完成还取决于某些外部事件,例如计时器到期,I/O 完成或另一个线程的操作(释放锁,设置标志或放置任务到工作队列)。普通方法可以在完成工作后立即结束,但阻塞方法不太好预测,因为它们依赖于外部事件。因为如果他们正在等待永远不会在事件,发生堵塞的方法有可能永远不结束,常用在阻塞可取消的操作。对于长时间运行的非阻塞方法,通常也是可以取消的。可取消操作是可以在通常自行完成之前从外部强制移动到完成状态的操作。 Thread提供的Thread.sleep() 和 Object.wait() 方法中断机制是一种取消线程继续阻塞的机制; 它允许一个线程请求另一个线程提前停止它正在做的事情。当一个方法抛出时 InterruptedException,它告诉你如果执行方法的线程被中断,它将尝试停止它正在做的事情提前返回, 并通过抛出 InterruptedException 表明它的提早返回。表现良好的阻塞库方法应该响应中断并抛出 InterruptedException 异常, 以便它们可以应用在可取消的活动中而不会妨碍程序的响应性。线程中断每个线程都有一个与之关联的布尔属性,表示其中断状态。中断状态最初为假; 当某个线程被其他线程通过调用中断 Thread.interrupt() 时, 会发生以下两种情况之一: 如果该线程正在执行低级别的中断阻塞方法 Thread.sleep(),Thread.join()或 Object.wait()等,它取消阻塞并抛出 InterruptedException。除此以外,interrupt() 仅设置线程的中断状态。在中断的线程中运行的代码可以稍后轮询中断的状态以查看是否已经请求停止它正在做的事情; 中断状态可以通过 Thread.isInterrupted() 读取,并且可以在命名不佳的单个操作Thread.interrupted()中读取和清除 。中断是一种合作机制。当一个线程中断另一个线程时,被中断的线程不一定会立即停止它正在做的事情。相反,中断是一种礼貌地要求另一个线程在方便的时候停止它正在做什么的方式。有些方法,比如Thread.sleep()认真对待这个请求,但方法不一定要注意中断请求。不阻塞但仍可能需要很长时间才能执行完成的方法可以通过轮询中断状态来尊重中断请求,并在中断时提前返回。你可以自由地忽略中断请求,但这样做可能会影响响应速度。中断的合作性质的一个好处是它为安全地构建可取消的活动提供了更大的灵活性。我们很少想立即停止活动; 如果活动在更新期间被取消,程序数据结构可能会处于不一致状态。中断允许可取消活动清理正在进行的任何工作,恢复不变量,通知其他活动取消事件,然后终止。处理InterruptedException如果 throw InterruptedException 意味着这个方法是一个阻塞方法,那么调用一个阻塞方法意味着你的方法也是一个阻塞方法,你应该有一个处理策略 InterruptedException。通常最简单的策略是你自己也抛出 InterruptedException 异常,如清单1 中的 putTask() 和 getTask() 方法所示。这样做会使你的方法响应中断,并且通常只需要添加 InterruptedException 到 throws 子句。清单1.通过不捕获它来向调用者传播InterruptedExceptionpublic class TaskQueue { private static final int MAX_TASKS = 1000; private BlockingQueue<Task> queue = new LinkedBlockingQueue<Task>(MAX_TASKS); public void putTask(Task r) throws InterruptedException { queue.put(r); } public Task getTask() throws InterruptedException { return queue.take(); }}有时在传播异常之前需要进行一些清理。在这种情况下,你可以捕获 InterruptedException,执行清理,然后重新抛出异常。清单2是一种用于匹配在线游戏服务中的玩家的机制,说明了这种技术。该 matchPlayers() 方法等待两个玩家到达然后开始新游戏。如果在一个玩家到达之后但在第二个玩家到达之前它被中断,则在重新投掷之前将该玩家放回队列 InterruptedException,以便玩家的游戏请求不会丢失。清单2.在重新抛出 InterruptedException 之前执行特定于任务的清理public class PlayerMatcher { private PlayerSource players; public PlayerMatcher(PlayerSource players) { this.players = players; } public void matchPlayers() <strong>throws InterruptedException</strong> { Player playerOne, playerTwo; try { while (true) { playerOne = playerTwo = null; // 等待两个玩家到来以便开始游戏 playerOne = players.waitForPlayer(); // 会抛出中断异常 playerTwo = players.waitForPlayer(); // 会抛出中断异常 startNewGame(playerOne, playerTwo); } } catch (InterruptedException e) { // 如一个玩家中断了, 将这个玩家放回队列 if (playerOne != null) players.addFirst(playerOne); // 然后传播异常 throw e; } }}不要吞下中断有时抛出 InterruptedException 不是一种选择,例如当通过 Runnable 调用可中断方法定义的任务时。在这种情况下,你不能重新抛出 InterruptedException,但你也不想做任何事情。当阻塞方法检测到中断和抛出时 InterruptedException,它会清除中断状态。如果你抓住 InterruptedException 但不能重新抛出它,你应该保留中断发生的证据,以便调用堆栈上的代码可以了解中断并在需要时响应它。此任务通过调用 interrupt()实现“重新中断”当前线程,如清单3所示。至少,无论何时捕获 InterruptedException 并且不重新抛出它,都要在返回之前重新中断当前线程。清单3.捕获InterruptedException后恢复中断状态public class TaskRunner implements Runnable { private BlockingQueue<Task> queue; public TaskRunner(BlockingQueue<Task> queue) { this.queue = queue; } public void run() { try { while (true) { Task task = queue.take(10, TimeUnit.SECONDS); task.execute(); } } catch (InterruptedException e) { //重要: 恢复中断状态 Thread.currentThread().interrupt(); } }}你可以做的最糟糕的事情 InterruptedException 就是吞下它 - 抓住它,既不重新抛出它也不重新确定线程的中断状态。处理你没有规划的异常的标准方法 - 捕获它并记录它 - 也算作吞噬中断,因为调用堆栈上的代码将无法找到它。(记录 InterruptedException 也很愚蠢,因为当人类读取日志时,对它做任何事都为时已晚。)清单4显示了吞下中断的常见模式:清单4.吞下中断 - 不要这样做// 不要这么做!public class TaskRunner implements Runnable { private BlockingQueue<Task> queue; public TaskRunner(BlockingQueue<Task> queue) { this.queue = queue; } public void run() { try { while (true) { Task task = queue.take(10, TimeUnit.SECONDS); task.execute(); } } catch (InterruptedException swallowed) { /* DON’T DO THIS - RESTORE THE INTERRUPTED STATUS INSTEAD / / 不要这么做 - 要让线程中断 / } }}如果你不能重新抛出 InterruptedException,无论你是否计划对中断请求执行操作,你仍然希望重新中断当前线程,因为单个中断请求可能有多个“收件人”。标准线程池(ThreadPoolExecutor)工作线程实现响应中断,因此中断线程池中运行的任务可能具有取消任务和通知执行线程线程池正在关闭的效果。如果作业吞下中断请求,则工作线程可能不会知道请求了中断,这可能会延迟应用程序或服务关闭。实施可取消的任务语言规范中没有任何内容给出任何特定语义的中断,但在较大的程序中,除了取消之外,很难保持中断的任何语义。根据活动,用户可以通过 GUI 或通过 JMX 或 Web 服务等网络机制请求取消。它也可以由程序逻辑请求。例如,如果 Web 爬虫检测到磁盘已满,则可能会自动关闭自身,或者并行算法可能会启动多个线程来搜索解决方案空间的不同区域,并在其中一个找到解决方案后取消它们。仅仅因为一个任务是取消并不意味着它需要一个中断请求响应立即。对于在循环中执行代码的任务,通常每次循环迭代仅检查一次中断。根据循环执行的时间长短,在任务代码通知线程中断之前可能需要一些时间(通过使用 Thread.isInterrupted()或通过调用阻塞方法轮询中断状态)。如果任务需要更具响应性,则可以更频繁地轮询中断状态。阻止方法通常在进入时立即轮询中断状态,InterruptedException 如果设置为提高响应性则抛出 。吞下一个中断是可以接受的,当你知道线程即将退出时。这种情况只发生在调用可中断方法的类是一个 Thread,而不是 Runnable 一般或通用库代码的一部分时,如清单5所示。它创建一个枚举素数的线程,直到它被中断并允许线程退出中断。寻求主要的循环在两个地方检查中断:一次是通过轮询 isInterrupted() while 循环的头部中的方法,一次是在调用阻塞 BlockingQueue.put() 方法时。清单5.如果你知道线程即将退出,则可以吞下中断public class PrimeProducer extends Thread { private final BlockingQueue<BigInteger> queue; PrimeProducer(BlockingQueue<BigInteger> queue) { this.queue = queue; } public void run() { try { BigInteger p = BigInteger.ONE; while (!Thread.currentThread().isInterrupted()) queue.put(p = p.nextProbablePrime()); } catch (InterruptedException consumed) { / Allow thread to exit / / 允许线程退出 */ } } public void cancel() { interrupt(); }}不间断阻塞并非所有阻止方法都抛出 InterruptedException。输入和输出流类可能会阻止等待 I/O 完成,但它们不会抛出InterruptedException,并且如果它们被中断,它们不会提前返回。但是,在套接字 I/O 的情况下,如果一个线程关闭了套接字,那么阻塞其他线程中该套接字上的 I/O 操作将在早期完成SocketException。非阻塞 I/O 类 java.nio 也不支持可中断 I/O,但可以通过关闭通道或请求唤醒来类似地取消阻塞操作 Selector。同样,尝试获取内在锁(输入一个 synchronized 块)不能被中断,但 ReentrantLock 支持可中断的采集模式。不可取消的任务有些任务只是拒绝被打断,使它们无法取消。但是,即使是不可取消的任务也应该尝试保留中断状态,以但在调用堆栈上层的代码在非可取消任务完成后想要对发生的中断进行响应。清单6显示了一个等待阻塞队列直到某个项可用的方法,无论它是否被中断。为了成为一个好公民,它在完成后恢复最终块中的中断状态,以免剥夺呼叫者的中断请求。它无法提前恢复中断状态,因为它会导致无限循环 - BlockingQueue.take(), 完成后则可以在进入时立即轮询中断状态, 如果发现中断状态设置,则可以抛出InterruptedException。清单6. 在返回之前恢复中断状态的非可执行任务public Task getNextTask(BlockingQueue<Task> queue) { boolean interrupted = false; try { while (true) { try { return queue.take(); } catch (InterruptedException e) { interrupted = true; // 失败了再试 } } } finally { if (interrupted) Thread.currentThread().interrupt(); }}摘要你可以使用 Java 平台提供的协作中断机制来构建灵活的取消策略。作业可以决定它们是否可以取消,它们希望如何响应中断,如果立即返回会影响应用程序的完整性,它们可以推迟中断以执行特定于任务的清理。即使你想完全忽略代码中断,也要确保在捕获 InterruptedException 并且不重新抛出代码时恢复中断状态 ,以便调用它的代码能够发现中断。 ...

December 22, 2018 · 2 min · jiezi

Java中断异常 InterruptedException 的正确处理方式

你看到这篇文件可能是因为你已经调用了一个抛出 InterruptedException 异常的方法,并且需要以某种方式处理它。首先,需要了解为一个方法为啥会 throws InterruptedException, 是这个方法抛出中断异常作为方法签名的一部分以及调用正在调用的方法的可能结果。因此,首先要接受一个事实,InterruptedException 是这个方法调用的完全有效的结果。现在,如果你正在调用的方法抛出此类异常,你的方法应该怎么做?可以通过考虑以下问题找出答案:你正在实现的方法是否有意义抛出异常 InterruptedException?换句话说,InterruptedException 异常是否是调用你的方法是一个明智的结果?如果是,那么 throws InterruptedException 应当成为你的方法签名,你应该让异常传播(即不捕获该异常的话)。示例: 你的方法等待来自网络的值以完成计算并返回结果。如果阻塞网络调用抛出 InterruptedException方法无法以正常方式完成计算。你让 InterruptedException 传播。int computeSum(Server server) throws InterruptedException { // Any InterruptedException thrown below is propagated int a = server.getValueA(); int b = server.getValueB(); return a + b;}如果不是,那么你不应该声明你的方法 throws InterruptedException, 你应该(必须!)捕获异常。在这种情况下,现在要记住两件事:有人打断了你的线程。这个人可能急于取消操作,优雅地终止程序,或者其他什么。你应该对那个人保持礼貌并且不用再费力地从你的方法中返回。即使你的方法可以设法让出现 InterruptedException 异常时, 即在线程被中断的情况下, 产生合理的返回值,线程被中断过这件事仍然很重要。特别是,调用方法的代码可能会对执行方法期间是否发生中断感兴趣。您应该通过设置中断标志来记录发生中断的事实:即需要在 catch 里调用Thread.currentThread().interrupt() .示例: 用户要求打印两个值的总和。如果无法计算总和,则打印“无法计算总和”(并且比由于一个导致程序因堆栈跟踪而崩溃要好得多 InterruptedException)。换句话说,用这个方法声明这个方法是没有意义的throws InterruptedException。void printSum(Server server) { try { int sum = computeSum(server); System.out.println(“Sum: " + sum); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // set interrupt flag System.out.println(“Failed to compute sum”); }}到目前为止,应该清楚的是,做这件事throw new RuntimeException(e)是一个坏主意。这对调用者来说不太礼貌。您可以发明一个新的运行时异常,但根本原因(某人希望线程停止执行)可能会丢失。另一个例子:实施 Runnable正如您可能已经发现的那样,签名 Runnable.run 不允许重新抛出 InterruptedExceptions。好吧,你声明了实现 Runnable 接口,这意味着你已声明处理可能的中断异常问题。选择不同的接口,例如Callable则可以抛出中断异常(V call() throws Exception),或者按照说的上面的第二种方法。还有一个:调用 Thread.sleep你正在尝试读取文件,规范说你应该尝试10次,间隔1秒。调用 Thread.sleep(1000)。所以,你需要处理 InterruptedException。对于一种方法 tryToReadFile 来说,如果说“如果我被打断了,我无法完成尝试阅读文件的行为”这一方法非常有意义。换句话说,它对抛出的方法很有意义InterruptedExceptions。String tryToReadFile(File f) throws InterruptedException { for (int i = 0; i < 10; i++) { if (f.exists()) return readFile(f); Thread.sleep(1000); } return null;}作者:Andreas Lundblad,理论计算机科学博士。在Oracle开发Java平台(javac,javadoc,sjavac)工作了三年。安德烈亚斯是StackOverflow的Java标签的前10贡献者. ...

December 9, 2018 · 1 min · jiezi