分享

如何正确使用Java异常处理机制

 python_lover 2020-01-09

如何正确使用Java异常处理机制

 

个人博客地址

本文的目标并不是介绍Java异常处理机制相关概念和语法,如果你有这方面的需求,请参考我的“ Java异常 官方文档翻译系列”文章。本文的目标是如何正确使用Java异常处理机制。

 

第一节 异常处理概述

在理想境界中,程序永远不会出现问题,用户输入的数据永远是正确的,逻辑没有任何问题 ,选择打开的文件也一定是存在的,内存永远是够用的……反正没有任何问题!但是一旦出现这些问题,如果处理不好,程序就不能正常运行了,用户就有可能再也不使用这个程序了。

要处理异常,必先知道异常发生的原因;要知道异常发生的原因,必先知道异常发生的场景。你的程序可能和任何其他实体交互的时候,都可能发生异常。Java程序中,都是利用方法(Method)和其他实体进行交互。所以异常的发生、抛出、声明和处理都是在方法内。下图是Java程序可能和其他实体交互的概要图:

图1:你的方法与其他实体交互概要图

如图1所示,你写的方法和外部实体交互大概可以分为五类:

  1. 和资源(Resource)交互,见图⑤处。这里资源的范围很广,比如进程外部的数据库,文件,SOA服务,其他各种中间件;进程内的类,方法,线程……都算是资源。
  2. 给进程内的其他方法(User Method)提供服务,见图②处。
  3. 依赖进程内的其他方法(Server Method),见图③处。包括Java平台提供的方法和其他第三方供应方提供的方法。
  4. 和系统环境交互,见图⑧处。系统环境可能是直接环境——JVM,也可能是间接环境——操作系统或硬件等。
  5. 给外部实体提供服务,见图①处。这种外部实体一般会通过容器(或其他类似的机制)和你的方法进行交互。所以,可以归为②,不予探讨。

Java方法和每一类实体进行交互时,都可能发生异常。

当和资源交互时,常常会因为资源不可用而发生异常,比如发生找不到文件、数据库连接错误、找不到类、找不到方法……等等状况。有可能是直接产生的,见图⑤处;有可能是间接产生的,比如图⑥处发生异常,Server Method把异常抛给Your Method,图③处就间接发生了异常。一般来说,你写的方法间接发生这类异常的可能性比直接发生要大得多,因为直接产生这类异常的方法在Java平台中已经提供了。对于这类异常,通常有以下几个特点:

  • 问题来自外部,你的方法本身的正常流程逻辑没问题。
  • 这类异常通常是暂时的,过段时间就可用了(经过资源维护者的彻夜奋战……)。最终用户通常也可以接受暂时的等待或采取替补方案。
  • 你的程序的其他功能还可以用。

这时,你的方法应该这样处理:

  • 返回到一种安全状态,并能够让用户执行一些其他的命令(比如支付宝支付失败时返回一个弹出框,说明余额不足等原因,让用户重新选择其他支付渠道);或者
  • 允许用户保存所有操作的结果,并以适当的方式终止程序(比如保存填了一半的表单)。

然后,你应该协调各方,促进资源恢复可用,消除异常。

当给用户方法(User Method )提供服务时,用户可能会传入一些不合法的数据(或者其他不恰当的使用方法),进而对程序的正常流程造成破坏。你的方法应该检查每一个输入数据,如果发现不合法的数据,马上阻止执行流程,并通知用户方法。

当调用服务方法(Server Method )时,有可能会发生两类异常。一类是你的使用方法不正确,导致服务中止;一类是服务方法出了异常,然后传递给你的方法。如果是第一种异常,你应该检查并修改你的方法逻辑,消除BUG。对于第二类异常,你要么写一个处理器处理,要么继续传递给上层方法。

当和系统环境交互时,有可能因为JVM参数设置不当,有可能因为程序产生了大量不必要的对象,也有可能因为硬故障(操作系统或硬件出了问题),导致整个程序不可用。当这类异常发生时,最终用户没法选择其他替代方案,操作到一半的数据会全部丢失。你的方法对这类异常一般没什么办法,既不能通过修改主流程逻辑来消除,也不能通过增加异常处理器来处理。所以通常你的方法对这类异常不需要做任何处理。但是你必须检查进程内的所有程序和系统环境是否正常,然后协调各方,修改BUG或恢复环境。

Java的异常都是发生在方法内,所以研究Java异常,要以你设计的方法为中心。我们以“你的方法 ”为中心,总结一下处理办法:当服务方法告诉“你的方法 ”的主流程逻辑有问题时,就要及时修复BUG来消除异常;当用户方法非法使用“你的方法”时,应该直接中止主流程,并通知用户方法,强迫用户方法使用正确的方式,防止问题蔓延;当服务方法传递一个异常给“你的方法”时,你要判断“你的方法”是否合适处理这个异常,如果不合适,传递给上层方法,如果合适,写一个异常处理器处理这个异常。当系统环境出了问题,“你的方法”什么也做不了。

刚才以“你的方法”为中心,总结了在“你的方法”内部的处理办法。现在以“你”为中心,总结一下方法外部的处理方法:当资源不可用的时候,你应该协调各方,恢复资源;当发生系统故障时,你应该协调各方,恢复系统。

现在,已经基本分析清楚了异常发生的原因,以及相应的应对方法。下一节正式介绍Java异常处理机制。

 

第二节 Java异常处理类

Java把异常当做是破坏正常流程的一个事件,当事件发生后,就会触发处理机制。

Java有一套独立的异常处理机制,在遇到异常时,方法并不返回任何值(返回值属于正常流程),而是抛出一个封装了错误信息的对象。下图是Java异常处理机制类层级结构图:

图2:Java异常处理机制类层级结构图

 

 

2.1 Throwable

所有的异常对象都派生于Throwable类的一个实例。

2.1.1 Throwable有五种构造方法:

Throwable()

创建一个无详细信息的Throwable

Throwable(String message)

创建一个有详细信息的Throwable

Throwable(String message, Throwable cause)

创建一个有详细信息和发生原因的Throwable

protected Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)

创建一个有详细信息和发生原因的Throwable,并确定是否可以suppression,是否可以writable stack trace。jdk7开始才有。

Throwable(Throwable cause)

创建一个有发生原因的Throwable

备注:

  • suppression:被压抑的异常。想了解更多信息,请参看我的译文“try-with-resources语句”。
  • strack trace:堆栈跟踪。是一个方法调用过程列表,它包含了程序执行过程中方法调用的具体位置。

2.1.2 Throwable的所有成员方法:

public final void addSuppressed(Throwable exception)

把指定的异常加入当前异常的suppressed异常列表,这样就可以把这个异常传递下去。这个方法是线程安全的,通常被try-with-resources语句调用(可以说是专为这种新语句设计的)。jdk7开始才有。

如果enableSuppression为false,这个方法无效

public Throwable fillInStackTrace()

填充这个执行堆栈跟踪,这个方法把当前线程的堆栈帧的当前状态记录到了这个Throwable对象信息里面。并返回当前Throwable实例。构造器方法都会首先调用这个方法。

如果writableStackTrace为false,这个方法无效。

public Throwable getCause()

获取cause Throwable信息。其实就是获取底层的异常信息。对应于initCause方法。

public String getLocalizedMessage()

对于当前Throwable,创建一个本地化描述。供子类重写。如果子类没有重写这个方法,这个方法返回和getMessage()一样。

public String getMessage()

返回当前Throwable的详细信息。

public StackTraceElement[] getStackTrace()

获取堆栈跟踪信息,可以通过程序遍历StackTraceElement对象,获取个性化的信息。StackTraceElement的toString方法可以返回标准的堆栈跟踪信息。

public final Throwable[] getSuppressed() 

对应于addSuppressed方法,获取suppressed异常列表。这个方法是线程安全的,通常被try-with-resources语句调用(可以说是专为这种新语句设计的)。jdk7开始才有。如果没有,就会返回一个空数组。

public Throwable initCause(Throwable cause)

设置引起当前Throwable被抛出的Throwable。只能设置一次cause Throwable,通常在构造方法就设置好了,或者在创建Throwable实例以后马上调用本方法。

 

try {
    lowLevelOp();
} catch (LowLevelException le) {
    throw (HighLevelException)
          new HighLevelException().initCause(le); // Legacy constructor

}

 

public void printStackTrace()

把这个Throwable和它的堆栈跟踪信息打印到标准的错误字节流里面

把信息注入流以后,可以继续处理,分析并存储到不同的介质中:

 

Throwable t = new Throwable();

ByteArrayOutStream out = new ByteArrayOutStream();

t.printStackTrace();

String desc = out.toString();

//这个例子其实是public void printStackTrace(PrintStream s)的,放在一处解释

 

public void printStackTrace(PrintStream s)

把这个Throwable和它的堆栈跟踪信息打印到指定的打印字节流里面

 

同上

public void printStackTrace(PrintWriter s)

把这个Throwable和它的堆栈跟踪信息打印到指定的打印字符流里面

 

同上

public void setStackTrace(StackTraceElement[] stackTrace)

手动设置堆栈跟踪信息。这个方法是给RPC框架或其他先进系统设计的,允许客户端覆盖默认的由fillInStackTrace()生成的默认堆栈跟踪信息,如果这个Throwable是从一个序列化字节流读取而来的话。

如果writableStackTrace为false,这个方法无效。

public String toString()

返回当前Throwable的简短描述。

备注:

  • 所有派生于Throwable类的异常类,基本都没有这些成员方法,也就是说所有的异常类都只是一个标记,记录发生了什么类型的异常(通过标记,编译期和JVM做不同的处理),所有实质性的行为Throwable都具备了。
  • 综上,在一个Throwable里面可以获取什么信息?
    • 获取堆栈跟踪信息(源代码中哪个类,哪个方法,第几行出现了问题……从当前代码到最底层的代码调用链都可以查出来)
    • 获取引发当前Throwable的Throwable。追踪获取底层的异常信息。
    • 获取被压抑了,没抛出来的其他Throwable。一次只能抛出一个异常,如果发生了多个异常,其他异常就不会被抛出,这时可以通过加入suppressed异常列表来解决(JDK7以后才有)。
    • 获取基本的详细描述信息

从图2可以看出,Throwable类只有两个直接继承者:Error和Exception。然后Exception又分为RuntimeException和Checked Exception。

2.2 Error

在Java中, 由系统环境问题引起的异常,一般都继承于Error类。

对于Error类:

  • 一般开发者不要自定义Error子类,因为它代表系统级别的错误。与一般的程序无关。
  • 在Java异常处理机制中,Error不强制捕获或声明,也就是不强制处理。因为程序本身对此类错误无能为力。一般情况下我们只要把堆栈跟踪信息记录下来就行。

下列是Java平台中直接继承于Error的错误类型:

AnnotationFormatError, AssertionError, AWTError, CoderMalfunctionError, FactoryConfigurationError, FactoryConfigurationError, IOError, LinkageError, ServiceConfigurationError, ThreadDeath, TransformerFactoryConfigurationError, VirtualMachineError

2.3 Exception

在Java中,除了系统环境问题引起的异常,一般都继承于Exception类。Exception分为RuntimeException和Checked Exception。Checked Exception必须要捕获或声明。而RuntimeException不强制。

对于Exception类:

  • 如果你创建了一个异常类型,直接继承于Exception,那么这个异常类型将属于检查异常(Checked Exception)。

2.4 RuntimeException

在Java中,由于接口方法使用不当造成的异常,一般属于RuntimeException,也就是运行时异常。

对于RuntimeException:

  • 如果你调用服务方法的方式不正确,你应该马上修改代码,避免发生RuntimeException
  • 如果是用户方法调用你的方法的方式不正确,你应该立刻抛出RuntimeException,强制让使用者修正代码或改变使用方式,防止问题蔓延
  • 一般情况下,不要捕获或声明RuntimeException。因为问题在于你的程序本身有问题,如果你用异常流程处理了,反而让正常流程问题一直存在

下列是Java平台中直接继承于RuntimeException的运行时异常:

AnnotationTypeMismatchException, ArithmeticException, ArrayStoreException, BufferOverflowException, BufferUnderflowException, CannotRedoException, CannotUndoException, ClassCastException, CMMException, ConcurrentModificationException,DataBindingException, DOMException, EmptyStackException, EnumConstantNotPresentException, EventException, FileSystemAlreadyExistsException, FileSystemNotFoundException, IllegalArgumentException, IllegalMonitorStateException,IllegalPathStateException, IllegalStateException, IllformedLocaleException, ImagingOpException, IncompleteAnnotationException, IndexOutOfBoundsException, JMRuntimeException, LSException, MalformedParameterizedTypeException, MirroredTypesException,MissingResourceException, NegativeArraySizeException, NoSuchElementException, NoSuchMechanismException, NullPointerException, ProfileDataException, ProviderException, ProviderNotFoundException, RasterFormatException, RejectedExecutionException,SecurityException, SystemException, TypeConstraintException, TypeNotPresentException, UndeclaredThrowableException, UnknownEntityException, UnmodifiableSetException, UnsupportedOperationException, WebServiceException, WrongMethodTypeException

2.5 Checked Exception

在Java中,直接或间接因为“资源”问题引起的异常,一般属于检查异常(Checked Exception) 。检查异常继承于Exception,而不继承于RuntimeException。

对于检查异常:

  • 必须捕获或声明
  • 交给关心这个异常的方法处理
  • 异常处理器应该引导用户接下来怎么办,至少做到安全退出

下列是Java平台中直接继承于Exception的检查异常:

AclNotFoundException, ActivationException, AlreadyBoundException, ApplicationException, AWTException, BackingStoreException, BadAttributeValueExpException, BadBinaryOpValueExpException, BadLocationException, BadStringOperationException,BrokenBarrierException, CertificateException, CloneNotSupportedException, DataFormatException, DatatypeConfigurationException, DestroyFailedException, ExecutionException, ExpandVetoException, FontFormatException, GeneralSecurityException,GSSException, IllegalClassFormatException, InterruptedException, IntrospectionException, InvalidApplicationException, InvalidMidiDataException, InvalidPreferencesFormatException, InvalidTargetObjectTypeException, IOException, JAXBException, JMException,KeySelectorException, LastOwnerException, LineUnavailableException, MarshalException, MidiUnavailableException, MimeTypeParseException, MimeTypeParseException, NamingException, NoninvertibleTransformException, NotBoundException,NotOwnerException, ParseException, ParserConfigurationException, PrinterException, PrintException, PrivilegedActionException, PropertyVetoException, ReflectiveOperationException, RefreshFailedException, RemarshalException,  SAXException,ScriptException, ServerNotActiveException, SOAPException, SQLException, TimeoutException, TooManyListenersException, TransformerException, TransformException, UnmodifiableClassException, UnsupportedAudioFileException, UnsupportedCallbackException,UnsupportedFlavorException, UnsupportedLookAndFeelException, URIReferenceException, URISyntaxException, UserException, XAException, XMLParseException, XMLSignatureException, XMLStreamException, XPathException

2.6 Uncheck Exception

Error和RuntimeException统称为非检查异常。两者的共同点就是都不被强制捕获或声明。实际上两者描述问题的范围完全没有交集。

2.7 总结

所有的功能都在Throwable类里面实现了,子类只需要直接继承或间接继承它,并且加上需要的构造方法就行(一般而言,第一第二个构造方法是必须的,也可以全部加上),而且构造方法通常只需要一行代码:super(...),也就是说只要调用父类的构造方法就行了。Java把异常分为三类(Error,Checked Exception,RuntimeException),只是在语法层面上有不同的标记而已。它们自身拥有的功能一样,运行时系统处理它们的方式也是一样的(你也可以捕获或声明非检查异常),不同的是编译器对它们的区别对待(检查异常必须要在代码里处理,非检查异常就不需要),以及程序员对它们的区别对待(这需要程序员遵循良好的实践原则)。

这三类异常全部覆盖了第一节中所描述的异常发生场景,图1中,④⑤⑥处可能会发生Checked Exception,②③处既可能会发生RuntimeException也可能会发生Checked Exception,⑦⑧⑨处可能会发生Error。①处已经超出了Java异常处理机制的范畴(这属于容器要考虑的问题),通常在数据中加入返回码来通知异常信息。

理解了每一类异常对应的场景,很多人其实已经知道该怎么用了,不必往下看了。

 

第三节 Java异常处理执行流程探究

首先设计两个方法,一个方法可能会抛出RuntimeException,一个方法可能会抛出Checked Exception。

public String runtimeServerMethod(String s) {

  if(s==null) {

     throw new RuntimeException("runtimeServerMethod方法的字符串不能为空");

  }

  return s;

}

 

private BufferedReader bufferedReader;

 

public String checkedServerMethod(String s) throws IOException {

   File file = new File(s);

   Reader reader = new FileReader(file);

   bufferedReader = new BufferedReader(reader);

   String result = bufferedReader.readLine();

   return result;

}

 

3.1 流程一

public void currentMethod() {

  System.out.println("--------------------try-catch-before");

  String result = this.runtimeServerMethod(null);

  System.out.println("--------------------result:"+result);

  System.out.println("--------------------try-catch-after");

}

 

执行结果:

 

--------------------try-catch-before

Exception in thread "main" java.lang.RuntimeException: runtimeServerMethod方法的字符串不能为空

     at com.leaforbook.javaexception.App.runtimeServerMethod(App.java:34)

     at com.leaforbook.javaexception.App.currentMethod(App.java:26)

     at com.leaforbook.javaexception.App.userMethod(App.java:20)

     at com.leaforbook.javaexception.App.main(App.java:16)

分析:

违反了runtimeServerMethod方法的使用规则——入参不能为null,导致产生了一个运行时异常。主流程线程直接中断,后面的代码不再执行。

3.2 流程二

public void currentMethod() {

  System.out.println("--------------------try-catch-before");

  String result = null;

  try {

     result = this.runtimeServerMethod(null);

     System.out.println("--------------------result:"+result);

  } catch (Exception e) {

     System.out.println("--------------------in-catch");

  }

  System.out.println("--------------------try-catch-after");

}

执行结果:

--------------------try-catch-before

--------------------in-catch

--------------------try-catch-after

分析:

RuntimeException也可以捕获处理(这是一种不好的实践),运行时系统并不会区分异常类型。异常发生以后,try代码块后面的代码不再执行,而是跳到catch代码块,线程不中断,执行完整个方法。

3.3 流程三

public void currentMethod() {

   System.out.println("--------------------try-catch-before");

   String result = null;

   result = this.runtimeServerMethod("Conform to the rules");

   System.out.println("--------------------result:"+result);

   System.out.println("--------------------try-catch-after");

}

执行结果:

--------------------try-catch-before

--------------------result:Conform to the rules

--------------------try-catch-after

分析:

当符合服务方法的规则时,就不会抛出运行时异常。方法就可以正常执行完成。

3.4 流程四

public void currentMethod() {

     System.out.println("--------------------try-catch-before");

     String result = null;

     try {

              result = this.checkedServerMethod("");

               System.out.println("--------------------result:"+result);

          } catch (IOException e) {

              System.out.println("--------------------in-catch");

          } finally {

               System.out.println("--------------------in-finally");

              if(bufferedReader!=null) {

                   try {

                        bufferedReader.close();

                   } catch (IOException e) {

                        e.printStackTrace();

                   }

              }

          }

     System.out.println("--------------------try-catch-after");

}

执行结果:

--------------------try-catch-before

--------------------in-catch

--------------------in-finally

--------------------try-catch-after

分析:

当调用了checkedServerMethod方法,并且发生了Checked Exception时,一定要捕获或声明该异常,否则编译不通过。上例中,异常发生后,try代码块后面的代码不再执行,跳到catch代码块,再执行finally代码块(在这里有关闭资源的操作),然后再执行其余部分。

3.5 流程五

public void currentMethod() {

     System.out.println("--------------------try-catch-before");

     String result = null;

     try {

              result = this.checkedServerMethod("");

               System.out.println("--------------------result:"+result);

          } catch (IOException e) {

              System.out.println("--------------------in-catch");

              return;

          } finally {

               System.out.println("--------------------in-finally");

              if(bufferedReader!=null) {

                   try {

                        bufferedReader.close();

                   } catch (IOException e) {

                        e.printStackTrace();

                   }

              }

          }

     System.out.println("--------------------try-catch-after");

 }

执行结果:

--------------------try-catch-before

--------------------in-catch

--------------------in-finally

分析:

和流程四不同之处在于,catch代码块多了一条return语句。执行结果也相应发生了变化,try-catch-finally代码块后面的代码不再执行。而且值得注意的是,finally代码块的代码依然执行了。这就是finally代码块的意义。

3.6 流程六

public void userMethod() {

    try {

        this.currentMethod();

    } catch (IOException e) {

        e.printStackTrace();

    }

}

 

public void currentMethod() throws IOException {

     System.out.println("--------------------try-catch-before");

     String result = null;

     try {

              result = this.checkedServerMethod("");

               System.out.println("--------------------result:"+result);

          } catch (IOException e) {

              System.out.println("--------------------in-catch");

              this.checkedServerMethod("catch");

          } finally {

               System.out.println("--------------------in-finally");

              if(bufferedReader!=null) {

                   try {

                        bufferedReader.close();

                   } catch (IOException e) {

                        e.printStackTrace();

                   }

              }

          }

         System.out.println("--------------------try-catch-after");

}

执行结果:

--------------------try-catch-before

--------------------in-catch

--------------------in-finally

java.io.FileNotFoundException: catch (系统找不到指定的文件。)

     at java.io.FileInputStream.open0(Native Method)

     at java.io.FileInputStream.open(FileInputStream.java:195)

     at java.io.FileInputStream.<init>(FileInputStream.java:138)

     at java.io.FileReader.<init>(FileReader.java:72)

     at com.leaforbook.javaexception.App.checkedServerMethod(App.java:62)

     at com.leaforbook.javaexception.App.currentMethod(App.java:38)

     at com.leaforbook.javaexception.App.userMethod(App.java:23)

     at com.leaforbook.javaexception.App.main(App.java:18)

分析:

和流程五类似,只不过catch代码块不是返回一个正常值,而是抛出一个Checked Exception。

3.7 流程七

public void userMethod() {

  try {

      this.currentMethod();

  } catch (IOException e) {

      e.printStackTrace();

  }

}

public void currentMethod() throws IOException {

     System.out.println("--------------------try-catch-before");

     String result = null;

     try {

              result = this.checkedServerMethod("");

               System.out.println("--------------------result:"+result);

          } catch (IOException e) {

              System.out.println("--------------------in-catch");

          this.checkedServerMethod("catch");

          } finally {

               System.out.println("--------------------in-finally");

              if(bufferedReader!=null) {

                   try {

                        bufferedReader.close();

                   } catch (IOException e) {

                        e.printStackTrace();

                   }

              }

              this.checkedServerMethod("finally");

          }

      System.out.println("--------------------try-catch-after");

}

执行结果:

--------------------try-catch-before

--------------------in-catch

--------------------in-finally

java.io.FileNotFoundException: finally (系统找不到指定的文件。)

     at java.io.FileInputStream.open0(Native Method)

     at java.io.FileInputStream.open(FileInputStream.java:195)

     at java.io.FileInputStream.<init>(FileInputStream.java:138)

     at java.io.FileReader.<init>(FileReader.java:72)

     at com.leaforbook.javaexception.App.checkedServerMethod(App.java:63)

     at com.leaforbook.javaexception.App.currentMethod(App.java:48)

     at com.leaforbook.javaexception.App.userMethod(App.java:23)

     at com.leaforbook.javaexception.App.main(App.java:18)

分析:

和流程六比起来,流程七在finally代码块也抛出了一个异常,最终在userMethod方法里面捕获到的是finally代码块的异常,catch代码块里抛出的异常被压抑了。

3.8 流程八

public void userMethod() {

     try {

         this.currentMethod();

     } catch (IOException e) {

         e.printStackTrace();

     }

}

 

public void currentMethod() throws IOException {

     System.out.println("--------------------try-catch-before");

     String result = null;

     try {

              result = this.checkedServerMethod("");

               System.out.println("--------------------result:"+result);

          }finally {

               System.out.println("--------------------in-finally");

              if(bufferedReader!=null) {

                   try {

                        bufferedReader.close();

                   } catch (IOException e) {

                        e.printStackTrace();

                   }

              }

          }

     System.out.println("--------------------try-catch-after");

 }

执行结果:

--------------------try-catch-before

--------------------in-finally

java.io.FileNotFoundException:

     at java.io.FileInputStream.open0(Native Method)

     at java.io.FileInputStream.open(FileInputStream.java:195)

&

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多