代码中异常和日志规范

1,203 阅读22分钟

本规约制订目的:规范异常的处理,规范和丰富日志,达到现场可以通过日志获取关键信息,快速定位问题的目的.

主要参考 Effetive Java(第三版) 阿里规约(嵩山版) JAVA源码 网上资料

异常规约

1.【强制】 不要捕获Java类库中定义的继承⾃RuntimeException的运⾏时异常类,如:IndexOutOfBoundsException NullPointerException,这类异常由程序员进行预检查来规避,保证程序健壮性

正例
if(obj != null) {...}
反例
try { obj.method() } catch(NullPointerException e){…}

2.【强制】 异常不要⽤来做流程控制,条件控制,因为异常的处理效率⽐条件分⽀低

3.【强制】 对⼤段代码进⾏try-catch,这是不负责任的表现,catch时请分清稳定代码非稳定代码稳定代码指的是⽆论如何不会出错的代码,对于非稳定代码,不要直接抛出或catch Exception,RuntimeException,Throwable,Error对待这些类要像对待抽象类一样,因为无法可靠测试这些异常,他们是一个方法可能抛出所有异常的父类,对非稳定代码catch时,要尽可能的进⾏区分异常类型,再做对应的异常处理,由于已知了确定发生异常的类型,可以更快定位错误.

  try {
                  
   if (StringUtils.isNotBlank(insertDataMap.get("CREATETIME")))
   {  
   heNanEmployee.setCreateTime(simpleDateFormat.parse(insertDataMap.get("CREATETIME")));
                    }
                } catch (ParseException e) {
                    log.error("SFTP落地员工时间转换出错_" + e.getMessage(), e);
                    throw new ServiceException("SFTP落地员工时间转换出错");
                }
   public Object clone() {
        HashMap<K,V> result;
        try {
            result = (HashMap<K,V>)super.clone();
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
        result.reinitialize();
        result.putMapEntries(this, false);
        return result;
    }

4.【建议】 建议:为异常建立文档,准确记录下抛出每个异常的条件

5.【强制】 优先使用标准的异常:Java类库提供了一系列基本的未受检异常,满足了绝大多少API异常抛出需求,使代码易于使用,增加可读性,定义时区分unchecked / checked 异常,避免直接使⽤RuntimeException抛出,更不允许抛出Exception或者 Throwable,应使⽤有业务含义的⾃定义异常。推荐业界或者集团已定义过的⾃定义异常,如:DaoException / ServiceException等。

6. 【强制】 不要忽略异常,捕获异常是为了处理它,不要捕获了却什么都不处理⽽抛弃之,如果不想处理它,请将该异常抛给它的调⽤者。最顶层的业务使⽤者,必须处理异常,将其转化为⽤户可以理解的内容。

7.【强制】 事务场景中,抛出异常被catch后,如果需要回滚,⼀定要⼿动回滚事务。

8.【强制】 finally块必须对资源对象、流对象进⾏关闭,有异常也要做try-catch。

说明:如果JDK7,可以使⽤try-with-resources⽅法。

9.【强制】 不能在finally块中使⽤return,finally块中的return返回后⽅法结束执⾏,不会再执⾏try块中的return语句

10.【强制】 捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的⽗类。

11.【推荐】 ⽅法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。调⽤⽅需要进⾏null判断防⽌NPE问题。

说明:本规约明确防⽌NPE是调⽤者的责任。即使被调⽤⽅法返回空集合或者空对象,对调⽤者来说,也并⾮⾼枕⽆忧,必须考 虑到远程调⽤失败,运⾏时异常等场景返回null的情况,就算是为其他方法重构的一个子方法,不能因为不会产生NPE就不去判断非空,因为可能被其他方法重用.

11.【推荐】 防⽌NPE,是程序员的基本修养,注意NPE产⽣的场景

12.【推荐】 在代码中使⽤“抛异常”还是“返回错误码”,对于公司外的http/api开放接⼝必须使⽤“错误码”;⽽应⽤内部推荐异常抛出;跨应⽤间调⽤优先考虑使⽤Result⽅式,封装isSuccess、“错误码”、“错误简短信息”。 说明:关于远程调用⽅法返回⽅式使⽤Result⽅式的理由: 1)中间件平台基本上使⽤ResultDO来封装,由于中间件的普及,本身就有标准的引导含义。 2)使⽤抛异常返回⽅式,调⽤⽅如果没有捕获到就会产⽣运⾏时错误。 3)如果不加栈信息,只是new⾃定义异常,加⼊⾃⼰的理解的error message,对于调⽤端解决问题的帮助不会太多。如果加了 栈信息,在频繁调⽤出错的情况下,数据序列化和传输的性能损耗也是问题。

13.【参考】 避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。 说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性⽅ 法,或者抽象公共类,甚⾄是共⽤模块。

14.【参考】 使失败保持原子性,当对象抛出异常时,通常我们期望这个对象仍然保持在一种定义良好的可用状态之中,即使对象保持在被调用前的状态

日志规约

1.【强制】 应⽤中不可直接使⽤⽇志系统(Log4j、Logback)中的API,⽽应依赖使⽤⽇志框架(SLF4JJCL--Jakarta Commons Logging)中的API,使用门面模式的日志框架有利于维护,且使各个类的日志处理方式统一,⽇志⽂件推荐⾄少保存15天(我们配置保留30天),且配置additivity=false,防止日志重复打印,导致服务器空间不足 这一点我们现在用的是主流的Slf4j+logback,配合lambok的注解注入log对象,是目前效率高,占用资源少的方案

2.【推荐】 应⽤中的扩展⽇志(如监控、访问⽇志,错误日志等)命名⽅式:appName_logType_logName.log

logType:⽇志类型,推荐分类有stats/desc/monitor/visit/error等

logName:⽇志描述,这种命名的好处:通过⽂件名就可知道⽇志⽂件属于什么应⽤,什么类型,什么⽬的,也有利于归类查找。

正例:asset_manage应⽤中单独监控ES落地情况,如:asset_manage_monitor_eslocate.log

说明:推荐对⽇志进⾏分类,错误⽇志和业务⽇志尽量分开存放,便于开发⼈员查看,也便于通过⽇志对系统进⾏及时监控,这一点我们没做到,需要去一天的全量日志中通过搜索定位

3. 【强制】 在异常的失败信息中包含失败捕获信息,即加入对该异常有贡献的所有参数和值,异常信息应该包括两类信息:案发现场信息异常堆栈信息,如果不处理,那么往上抛,对trace/debug/info级别的⽇志输出,必须使⽤条件输出形式或者使⽤占位符的⽅式,这里我们日常开发中使用占位符

4.【强制】 ⽣产环境禁⽌直接使⽤System.outSystem.err输出⽇志,或使⽤e.printStackTrace()打印异常堆栈,这样只会打印到tomcat控制台,不方面集中管理,不使用e.printStackTrace()原因是因为printStackTrace()还是通过System.err输出到tomcat控制台

5.【强制】 如使用非Spring管理的连接,如ES客户端,JDBC,SFTP,在建联前,断连后,记录info级别日志,并添加重要信息.但是注意不要携带密码信息

log.info("ftp://{}:{},user:{},开始进行连接...",host,port,username);
...
log.info("SFTP已连接到{}",host);

6. 【推荐】 可以使⽤warn⽇志级别来记录⽤户输⼊参数错误的情况,避免⽤户投诉时,⽆所适从。注意⽇志输出的级别,error级别只记录系统逻辑出错、异常、或者重要的错误信息。如⾮必要,请不要用户场景打出过多error级别日志,避免频繁报警

7.【推荐】 谨慎地记录⽇志。⽣产环境禁⽌输出debug⽇志;有选择地输出info⽇志;如果使⽤warn来记录刚上线时的业务⾏为信息,⼀定要注意⽇志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察⽇志。 说明:⼤量地输出⽆效⽇志,不利于系统性能提升,也不利于快速定位错误点。纪录⽇志时请思考:这些⽇志真的有⼈看吗?看到这条⽇志你能做什么?能不能给问题排查带来好处?

8.【参考】 使用自定义日志切面@TopLog注解,此注解target为method,在方法上标注后为方法引入自定义切面,执行切面中的doAround() 例子如下:我们可以为方法添加tag:标记这是什么方法,方法执行前后的message,切面会自动获取,执行方法名,入参,返回值的toString方法,以及方法执行时间,方便查问题,切面日志输出级别为info,在生产中可以调到error级别关闭日志输出

@TopLog(tag = "员工单个查询方法",beforeExecuteMessage = "执行单个员工查询",afterExecuteMessage = "员工查询完成")
    @ApiOperation(value = "Restful基本接口--查看一条数据详情,参数主键ID")
    @GetMapping("/{id}")
    public Result detail(@PathVariable String id) {
        Employee assetEmployee = employeeService.findById(id);
        processRelationSearch(assetEmployee);
        return ResultGenerator.genSuccessResult(assetEmployee);
    }
    
    
2020-08-11 14:23:02.345 [http-nio-22120-exec-8] INFO  c.c.topsec.ti.assetmanage.aspect.ExecuteTimeAspect - 执行单个员工查询
2020-08-11 14:23:02.353 [http-nio-22120-exec-8] INFO  c.c.topsec.ti.assetmanage.aspect.ExecuteTimeAspect - 员工查询完成
2020-08-11 14:23:02.353 [http-nio-22120-exec-8] INFO  c.c.topsec.ti.assetmanage.aspect.ExecuteTimeAspect - Tag:员工单个查询方法,执行方法:cn.com.topsec.ti.assetmanage.controller.EmployeeController.detail(123),关键对象信息:{"code":200,"message":"SUCCESS"},执行时间:7ms


线上排查

如果有线上排查问题的机会,即可以连接到生产环境,除了日志,我们还有大量的Java自带工具和第三方工具支持我们查找问题,如我们之前介绍过的SpringBootAdmin+Acuator直接查看各微服务thread dumpheap dump 在服务器上,我们使用

jps -l |grep xxxx 查看Java进程PID

使用 jstat -gcutil pid 查看JVM运行时内存区域状况,jmap 生成heap dump,jstack生成thread dump等等,如果能远程,还可以使用jvisualvm图形化的查看,如上次定时任务导致程序异常关闭的问题,就是通过jvisualvm进行排查,发现定时任务创建了大量I\O线程却未关闭,导致达到线程数上限程序异常退出,进而定位到问题解决.

Arthas

是阿里巴巴开源的 Java 诊断工具

  1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  5. 是否有一个全局视角来查看系统的运行状况?
  6. 有什么办法可以监控到JVM的实时运行状态?
  7. 怎么快速定位应用的热点,生成火焰图?
  • 可以通过dashboard查看全局监控信息

  • thread 命令查看线程信息,查看是否线程过多,线程死锁

  • jad对类进行反编译,查看服务器上代码是否正确(涉及到代码不展示图片)
jad  cn.com.topsec.ti.assetmanage.controller.AccountController list
  • sc 类名查看JVM已经加载的类

  • stack 查看方法完整调用堆栈信息(需要外部访问触发)

  • Trace 观察方法执行的时候哪个子调用比较慢:(需要外部触发)

  • Monitor监控某个特殊方法的调用统计数据,包括总调用次数,平均rt,成功率等信息,每隔5秒输出一次。

除此之外,Arthas还包含有很多工具,我们可以结合线上诊断工具与标准化的日志.更快速的定位问题,解决问题.

知识补充

JAVA异常的结构

Java中异常继承于的Throwable类,包括受检查的异常(checked Excepiton),运行时异常(RuntimeExcepiton),以及Error

checked exception: 编译器要求对其进行显式的捕获或抛出,例如:IOException,SQLException,反射时的NoSuchFieldException,NoSuchMethodException,不在方法签名上抛出或进行try-catch,无法通过编译

unchecked exception: 一般发生在运行期,编译器不要求对其进行显示的捕获或抛出,例如NullPointerException,ClassCastException

Error: 代表"错误",多为比较严重的错误,他们都是不需要也不应该被捕获的结构,如果程序抛出error,如stackoverflowError,OOM就是不可恢复的情况,继续执行有害无益

那么编译器是如何区分受检异常和非受检异常的?

我们看到Class.java中源码Method()方法

   @CallerSensitive
    public Method getMethod(String name, Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
        checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
        Method method = getMethod0(name, parameterTypes, true);
        if (method == null) {
            throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
        }
        return method;
    }

发现方法签名上抛出了两个异常NoSuchMethodException,SecurityException,但我们使用时只需要抛出或try-catch捕获NoSuchMethodException即可,所以NoSuchMethodException为受检异常,而SecurityException为非受检异常,如下为API使用代码

  private void testCheckedException() throws NoSuchMethodException {
        Class clazz = JavaIndexOutOfBounds.class;
        clazz.getMethod("test");
    }

这里我们看下Exception.java源码注释,写的很明白

Exception.java
 <p>The class {@code Exception} and any subclasses that are not also
 * subclasses of {@link RuntimeException} are <em>checked
 * exceptions</em>.  Checked exceptions need to be declared in a
 * method or constructor's {@code throws} clause if they can be thrown
 * by the execution of the method or constructor and propagate outside
 * the method or constructor boundary.
 
 RuntimeException.java
 {@code RuntimeException} is the superclass of those
 * exceptions that can be thrown during the normal operation of the
 * Java Virtual Machine.
 
 <p>{@code RuntimeException.} and its subclasses are <em>unchecked
 * exceptions</em>.  Unchecked exceptions do <em>not</em> need to be
 * declared in a method or constructor's {@code throws} clause if they
 * can be thrown by the execution of the method or constructor and
 * propagate outside the method or constructor boundary.

当使用受检查异常(checked exception),源码中会在方法签名中抛出,编译器会强制调用者抛出继续传递/处理该异常,即方法中声明要抛出的每个受检查的异常,都是对API用户的一种潜在暗示,出现该异常是调用这个方法一种可能的结果

剩下两种是未受检且可抛出的,即RuntimeExceptionError,上面我们提到过了,对于Error的OOM,StackOverFlowError,如果发生了,是程序内存分配出现了严重错误或资源不足,没必要继续执行

如果程序抛出RuntimeExcepiton中的NPE,ArrayIndexOutOfBounds说明程序的健壮性和逻辑可能有问题,同样不需要继续执行,如果程序没有捕捉到这样的可抛出结构,就会导致当前线程中断,也就是报错了.控制台/日志会输出当前错误的堆栈信息,如图

这里之所以叫做堆栈信息,是因为Java中方法的执行模型为栈帧,位置为Java虚拟机栈,方法执行时入栈,执行完毕出栈,所以栈顶的就是最后执行的方法,也是出错的直接位置.此时引出我们第一条异常规约

JAVA异常机制的初衷

int[] array = new int[5];
try{
    int i = 0;
    while(true){
        System.out.println(array[i++]);}
    }catch(ArrayIndexOutOfBoundsException e){
    }
}

可能有人会这么写代码,即利用Java的异常机制参与数组的循环,由于Java每次都会检查数组下标越界,所以在某些情况下,这样终止遍历数组是可以实现的,但是比如try代码块中如果调用了其他函数,且其他函数抛出了ArrayIndexOutOfBoundsException,数组就会异常终止,且由于直接忽略了异常,导致极难定位错误

  1. JAVA异常机制的初衷是用于不正常的情形,所以JVM不会对异常进行优化
  2. 代码放在try-catch中阻止了现代JVM实现本来可能执行的某些特定优化 所以会产生效率低,甚至根本不能运行,比如刚才的例子try代码块中调用了某个方法产生了下标越界,这个模式会失效

教训也很简单,异常应该只用于异常的情况下,他们永远不应该用于正常的控制流.此时引出异常规约第二条

为异常建立文档

为异常建立文档,准确记录下抛出每个异常的条件,永远在一个共有方法签名中throws Exception,甚至throws Throwable 这是非常极端的做法,这样的声明不仅没有为程序员提供关于这个方法能够抛出哪些异常的任何指导信息,而且达达妨碍了该方法的使用,因为这样粗粒度的抛出,掩盖了该方法在同样的执行环境下可能抛出的任何异常

记录每个受检异常和非受检异常,我们在方法签名上抛出受检异常,但是对于非受检异常,如NPE等,不要抛出

继续使用反射包里Class.java中的getMethod()方法举例,Java源码即规范,我们看到,注释的第一部分是该方法的详细使用说明,有的还有使用例子,接着是参数与返回值,最后一部分就是异常抛出的说明,对于每一种可能抛出异常异常,分别说明出现该异常的条件

  /**
     * Returns a {@code Method} object that reflects the specified public
     * member method of the class or interface represented by this
     * {@code Class} object. The {@code name} parameter is a
     * {@code String} specifying the simple name of the desired method. The
     * {@code parameterTypes} parameter is an array of {@code Class}
     * objects that identify the method's formal parameter types, in declared
     * order. If {@code parameterTypes} is {@code null}, it is
     * treated as if it were an empty array.
     *
     * <p> If the {@code name} is "{@code <init>}" or "{@code <clinit>}" a
     * {@code NoSuchMethodException} is raised. Otherwise, the method to
     * be reflected is determined by the algorithm that follows.  Let C be the
     * class or interface represented by this object:
     * <OL>
     * <LI> C is searched for a <I>matching method</I>, as defined below. If a
     *      matching method is found, it is reflected.</LI>
     * <LI> If no matching method is found by step 1 then:
     *   <OL TYPE="a">
     *   <LI> If C is a class other than {@code Object}, then this algorithm is
     *        invoked recursively on the superclass of C.</LI>
     *   <LI> If C is the class {@code Object}, or if C is an interface, then
     *        the superinterfaces of C (if any) are searched for a matching
     *        method. If any such method is found, it is reflected.</LI>
     *   </OL></LI>
     * </OL>
     *
     * <p> To find a matching method in a class or interface C:&nbsp; If C
     * declares exactly one public method with the specified name and exactly
     * the same formal parameter types, that is the method reflected. If more
     * than one such method is found in C, and one of these methods has a
     * return type that is more specific than any of the others, that method is
     * reflected; otherwise one of the methods is chosen arbitrarily.
     *
     * <p>Note that there may be more than one matching method in a
     * class because while the Java language forbids a class to
     * declare multiple methods with the same signature but different
     * return types, the Java virtual machine does not.  This
     * increased flexibility in the virtual machine can be used to
     * implement various language features.  For example, covariant
     * returns can be implemented with {@linkplain
     * java.lang.reflect.Method#isBridge bridge methods}; the bridge
     * method and the method being overridden would have the same
     * signature but different return types.
     *
     * <p> If this {@code Class} object represents an array type, then this
     * method does not find the {@code clone()} method.
     *
     * <p> Static methods declared in superinterfaces of the class or interface
     * represented by this {@code Class} object are not considered members of
     * the class or interface.
     *
     * @param name the name of the method
     * @param parameterTypes the list of parameters
     * @return the {@code Method} object that matches the specified
     *         {@code name} and {@code parameterTypes}
     * @throws NoSuchMethodException if a matching method is not found
     *         or if the name is "&lt;init&gt;"or "&lt;clinit&gt;".
     * @throws NullPointerException if {@code name} is {@code null}
     * @throws SecurityException
     *         If a security manager, <i>s</i>, is present and
     *         the caller's class loader is not the same as or an
     *         ancestor of the class loader for the current class and
     *         invocation of {@link SecurityManager#checkPackageAccess
     *         s.checkPackageAccess()} denies access to the package
     *         of this class.
     *
     * @jls 8.2 Class Members
     * @jls 8.4 Method Declarations
     * @since JDK1.1
     */
    @CallerSensitive
    public Method getMethod(String name, Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
        checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
        Method method = getMethod0(name, parameterTypes, true);
        if (method == null) {
            throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
        }
        return method;
    }

优先使用标准的异常

最经常被重用的异常类型有,IllegalArgumentException(非法参数)IllegalStateException(非法对象状态),如对象为null,调用其方法就会产生NPE

可以这么说,大部分错误的方法调用都可以归结为非法参数或者非法状态,有一些异常描述了特定情况下的非法参数和非法状态 比如NPE描述了不允许NULL的参数传了NULL,使用NPE而不是IllegalArgumentException

数组下标越界参数中传了越界的值,使用IndexOutOfBound而不是IllegalArgumentException

   /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }



 /**
     * 比较年龄大小IllegalArgumentException
     * @author zhaoxu
     * @param
     * @return
     * @throws
     */
    private static String processMethod(int i,int j){
        if (i<0 || j<0) {
            throw new IllegalArgumentException("年龄不能为负数");
        }else{
            return i<j?"第一个年龄小于第二个":"第一个年龄大于第二个";
        }

    }
 /**
     * 对象IllegalStateException
     * @author zhaoxu
     * @param
     * @return
     * @throws
     */
    private static String processMethod(UserInfo userInfo){
        if (userInfo==null) {
            throw new IllegalStateException("对象为null,未被初始化");
        }else{
            return userInfo.getUserName();
        }

    }
    

回滚事务

Spring使用声明式事务处理,默认情况下,如果被注解的数据库操作方法中发生了unchecked异常,所有的数据库操作将rollback,如果发生的异常是checked异常,默认情况下数据库操作还是会提交的,这里有如下三种事务写法

Positive example 1/**
     * @author caikang
     * @date 2017/04/07
     */
    @Service
    @Transactional(rollbackFor = Exception.class)
    public class UserServiceImpl implements UserService {
        @Override
        public void save(User user) {
            //some code
            //db operation
        }
    }   
Positive example 2/**
     * @author caikang
     * @date 2017/04/07
     */
    @Service
    public class UserServiceImpl implements UserService {
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void save(User user) {
            //some code
            //db operation
        }
    }   
Positive example 3/**
     * @author caikang
     * @date 2017/04/07
     */
    @Service
    public class UserServiceImpl implements UserService {
        @Autowired
        private DataSourceTransactionManager transactionManager;

        @Override
        @Transactional
        public void save(User user) {
            DefaultTransactionDefinition def = new DefaultTransactionDefinition();
            // explicitly setting the transaction name is something that can only be done programmatically
            def.setName("SomeTxName");
            def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

            TransactionStatus status = transactionManager.getTransaction(def);
            try {
                // execute your business logic here
                //db operation
            } catch (Exception ex) {
                transactionManager.rollback(status);
                throw ex;
            }
        }
    }   

流的关闭

这是传统流的使用方式,在try块中创建流,在finally中判断非空后关闭流

    /**
     * 存储序列化对象流
     * @author zhaoxu
     * @param
     * @return
     * @throws
     */
    private static void storeNoResource(String fileName) throws IOException {
        //对象输出流
        UserInfo userZ =  new UserInfo();
        userZ.setUserName("nihao");
        userZ.setUserId("W0001");
        ObjectOutputStream objectOutputStream = null;
        //向输出对象流中,写对象
        try  {
            objectOutputStream= new ObjectOutputStream(new FileOutputStream(fileName))
            objectOutputStream.writeObject(userZ);
        }finally {
            if (objectOutputStream!=null){
                objectOutputStream.close();
            }
        }
    }

在JDK以后可以使用try-with-resources⽅法,当资源继承了AutoCloseable接口,,就可以使用try-with-resources,会在try语句块运行结束时,objectOutputStream 会被自动关闭

  /**
     * 存储序列化对象流
     * @author zhaoxu
     * @param
     * @return
     * @throws
     */
    private static void store(String fileName) throws IOException {
        //对象输出流
        UserInfo userZ =  new UserInfo();
        userZ.setUserName("nihao");
        userZ.setUserId("W0001");
        //向输出对象流中,写对象
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(fileName))) {
            objectOutputStream.writeObject(userZ);
        }
    }

防止NPE

  1. 返回类型为包装类型,自动拆箱(integer.intValue)可能产生npe
public class JavaNPE {
    public static void main(String[] args) {
        System.out.println(getInt());
    }
    
    public static int getInt() {
        Integer integer = null;
        return integer;
    }
}
  1. 数据库查询的结果是空集合还是NULL?我目前使用的通用mapper返回的是空集合,若单个对象查不到则为null,注意数据库查询结果的判定非空
  2. 集合经过判定(list!=null&&!list.isEmpty)之后,即使isNotEmpty,取出的数据元素也可能为null
  3. 远程调用(RPC对象)一律进行NPE判断
  4. Session获取的数据,进行NPE判断
  5. 级联调⽤obj.getA().getB().getC(),⼀连串调⽤,易产⽣NPE 总之,NPE无小事,Java是面向对象的语言,在使用每一个集合,每一个对象之前,都要先思考其会不会出现为NULL的场景,进行NPE判断

不过这么做,步步为营的判断非空防止NPE,虽然保证了程序的健壮,却创建了大量的if-else,使原本简单的逻辑由于大量if语句的参与变得可读性不是很棒,此时可以使用JDK8的Optional来防止NPE,解决链式调用问题

使失败保持原子性

当对象抛出异常时,通常我们期望这个对象仍然保持在一种定义良好的可用状态之中,即使失败是发生在执行某个操作过程的中间,对于受检异常来说更重要,因为调用者期望可以从中恢复

一般而言,失败的方法调用应该使对象保持在被调用前的状态,具有这种属性的方法叫做具有失败原子性

  1. 执行参数前检查参数的有效性 使对象被修改前,先抛出适当的异常,如以下例子Java源码Stack.javapeek()方法,先进行对初始大小的检查,防止对空栈进行操作
    public synchronized E peek() {
        int     len = size();

        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }
  1. 调整计算处理过程的顺序,使任何可能会失败的计算部分都在对象状态被修改之前发生,如对参数的检查只有在执行了部分计算之后才能进行,比如向HashMap中添加类型不正确的元素,在HashMap被修改之前,会报ClassCastException

日志规约之日志输出

首先简单说下我们目前使用的日志框架slf4j+logback 日志框架有很多,如logback,Log4j2每一种日志框架都有自己单独的API,要使用对应的框架就要使用其对应的API,这就增加应用程序代码对于日志框架的耦合性 另外一类是日志系统,主要一套通用的API,用来屏蔽各个日志框架之间的差异的,slf4j就是这样的日志门面就是这样的日志门面,Java简易日志门面(Simple Logging Facade for Java,缩写SLF4J),它其实只是一个门面服务而已,他并不是真正的日志框架,真正的日志的输出相关的实现还是要依赖Log4j、logback等日志框架.这里涉及到一个设计模式(外观(门面)模式),有兴趣可用了解下

在异常的失败信息中包含失败捕获信息,即加入对该异常有贡献的所有参数和值,不能远程debug的情况下,日志就是我们定位问题的唯一来源,如果不添加错误信息和错误位置,获取到的信息量相当有限,且不能快速定位问题 如图,在spring框架下往往方法调用的堆栈都非常深,我们拿到日志只能看见堆栈信息,无法快速定位 且堆栈信息只有结合源码debug才有用,只能说拿到这样的日志让人一头雾水,无法定位

所以,异常类型的toString方法应该尽可能的返回更多有关失败原因的信息,这一点特别重要

即异常堆栈信息应该捕获到失败的原因,异常的细节信息应该包含对该异常有贡献的所有参数和对象的值,比如一个ArrayIndexOutOfBoundsException应该包含上界,下界和越界index值,实际的index可能小于下界,或者大于上界,或者是个无效值,或者甚至是数组内部错误,下界大于上界,每一种情况都代表了不同的问题,如果我们输出了详细的错误信息,就可以更快定位到真正的错误,但是注意千万不要在堆栈信息中输出密码秘钥等信息

并且我们要区分顶层接口,即用户层次的错误消息异常细节消息的区别,如资产删除时,提示用户该设备'交换机001'仍关联有IP,请先删除IP,这对于用户是可以理解的,但是对排查问题没什么帮助,异常的字符串主要是让程序员和运维工程师分析定位失败原因,因此信息的全面性很重要

  /**
     * 数组下标越界抛出有用信息
     * @author zhaoxu
     * @param
     * @return
     * @throws
     */
    private static void testArrayException(int[] arr,int index) {
        if (index<0){
            throw  new IllegalArgumentException("数组下标不能小于0");
        }
        if (index>arr.length-1) {
            int upperBound = arr.length-1;
            int lowerBound = 0;
            throw new ArrayIndexOutOfBoundsException("array下界为"+lowerBound+",array上界为"
                    +upperBound+",越界下标为"+index);
        }else {
            System.out.println(arr[index]);
        }
        

我们目前使用的SLF4J+logback日志解决方案中,日志级别有如下,优先级顺序由上到下依次降低

例如,如果日志级别是warn,这段日志不会打印,但是会执行字符串拼接,如果symbol是对象,会执行toString方法,浪费了系统资源

logger.debug("Processing trade with id: " + id + " symbol: " + symbol); 

应该使用占位符的方式

logger.info("Processing trade with id: {} and symbol : {} ", id, symbol);

错误日志的写法,输出的POJO类必须重写toString⽅法,否则只输出此对象的hashCode值(地址值),没啥参考意义。

logger.error("inputParams:{} and errorMessage:{}", 各类参数或者对象 toString(), e.getMessage(), e);