Java異常體系設計的目的在於經過使用少許代碼,實現大型、健壯、可靠程序。
異常處理是Java中惟一正式的錯誤報告機制。異常處理機制最大的好處就是下降錯誤代碼處理的複雜程度。
若是不使用異常,那麼就必須在調用點檢查特定的錯誤,並在程序的不少地方去處理它;若是使用異常,那麼就沒必要在方法調用處進行檢查,由於異常機制將保證可以捕獲這個錯誤。所以只須要在一個地方處理錯誤,這種方式不只節省代碼,並且把「描述正確執行過程作什麼事」和「出了問題怎麼辦」相分離。java
異常處理的一個重要原則就是「只有在你知道如何處理的狀況下才捕獲異常」,實際上異常處理的一個重要目標就是把錯誤處理的代碼與錯誤發生地點相分離。這使你能在一段代碼中專一於要作的事,至於異常處理,則放在另外一端代碼中。這樣,主幹代碼就不會與錯誤處理邏輯混在一塊兒,更容易理解和維護。經過容許一個異常處理程序處理多個異常點,使得異常處理代碼集中於一處。程序員
「異常情形」,指阻止當前方法或做用域繼續執行的問題。異常最重要的方面之一就是若是發生問題,他將不容許程序沿着正常路徑繼續執行,而是將控制權轉交給異常處理程序,強制處理出現的問題,並恢復穩定狀態。
與程序正常處理不一樣,當拋出異常時,有幾件事會隨之發生:sql
Java異常處理:數據庫
try { // 正常處理流程,正確執行過程作什麼事 Path path = Paths.get("var", "error"); List<String> lines = Files.readAllLines(path, Charset.defaultCharset()); System.out.println(lines); } catch (IOException e) { // 異常處理流程,出了問題怎麼辦 e.printStackTrace(); }finally { // 清理資源 }
異常與其餘對象同樣,使用new關鍵字在堆上建立異常對象,也伴隨着存儲空間的分配和構造函數的調用。
標準異常會有幾個構造函數:編程
public class Exception extends Throwable { static final long serialVersionUID = -3387516993124229948L; public Exception() { super(); } public Exception(String message) { super(message); } public Exception(String message, Throwable cause) { super(message, cause); } public Exception(Throwable cause) { super(cause); } }
將異常對象的引用傳遞給throw,從效果上看,它就像從方法中「返回」同樣,能夠將異常處理當作一種不一樣的返回機制。不一樣的是return返回到方法調用點,throw返回到異常處理程序。
private String throwException(){ //return "Test"; throw new RuntimeException("Test Exception"); }
「監控區域」是一段可能產生異常的代碼,而且後面跟着處理這些異常的代碼。能夠簡單的理解爲try塊就是監控區域。
若是在方法內部拋出異常(或調用其餘方法出現異常),這個方法將在拋出異常的點結束,若是不但願方法結束,那麼須要在方法內設置一個特殊的塊來捕獲異常。json
private String tryException(){ try { // 監控區域 return throwException(); }catch (Exception e){ // 異常處理區域 } return ""; }
拋出的異常必須在某處獲得處理,這個點就是異常處理程序。針對每一個要捕獲的異常,準備相應的處理程序。異常處理程序緊跟着try塊,以關鍵字catch表示。
每一個catch子句,看起來就像是接收一個且只接收一個特殊異常類型的方法。當異常發生後,異常處理機制會搜尋參數與異常類型匹配的第一個異常處理器,而後進入catch子句執行,此時認爲異常獲得了處理。一旦catch子句結束,則處理程序的查找過程結束。安全
在查找異常處理器時,並不要求拋出的異常與異常處理器所聲明的異常徹底匹配。派生類的對象也能夠匹配基類的處理器。網絡
private String tryException(){ try { // 監控區域 return throwException(); }catch (RuntimeException e){ // 處理 RuntimeException 狀況 } catch (Exception e){ // 處理 Exception 狀況 } return ""; }
順序,異常處理機制會搜索第一個匹配的異常處理器,所以catch語句的順序相當重要,一般將具體類型前置,通用類型後置。app
對於一些代碼,可能會但願不管try塊中是否拋出異常,他們都會執行。爲了達到效果,能夠在異常處理後面加上finally子句。
對於沒有垃圾回收和析構函數自動調用機制的語言來講,finally很是重要。它是程序員可以保證在任何狀況下,內存總能獲得釋放。但在Java中存在垃圾回收機制,內存釋放再也不是個問題。當要把除內存外的資源恢復到他們的初始化狀態時,就須要使用finally子句。常見的資源包括:網絡連接、文件句柄、顯示鎖等。框架
private String tryException(){ try { // 監控區域 return throwException(); }catch (RuntimeException e){ // 處理 RuntimeException 狀況 } catch (Exception e){ // 處理 Exception 狀況 }finally { // 對 網絡連接、文件句柄、鎖等資源進行處理 } return ""; }
Java鼓勵將方法可能會拋出的異常告知使用該方法的客戶端。這種作法,使得調用者能知道在代碼中能夠獲取全部的異常。
異常說明在方法聲明中使用附加的關鍵字throws,後面接一個全部潛在異常類型列表,因此方法簽名變成:
// 方法異常說明 private List<String> readFromFile(String filePath) throws IOException { Path path = Paths.get(filePath); return Files.readAllLines(path, Charset.defaultCharset()); }
代碼必須和異常說明保存一致。若是方法裏面的代碼產生了異常卻沒有被處理,編譯器會報錯,要麼處理這個異常,要麼在異常說明列表中添加這個異常類型。
固然,能夠在方法簽名中聲明異常,實際上並不拋出。這樣能夠爲異常佔個位置,之後能夠拋出該異常而不用修改調用代碼。
被檢查異常,這種在編譯階段被強制檢查的異常成爲被檢查異常。備註: 被檢查異常,能夠經過反射機制獲取異常列表。
當覆蓋方法時,只能拋出在基類方法的異常列表中列出的異常,這意味着當基類使用的代碼應用到其派生類對象的時候,程序同樣能正常工做。
儘管在繼承過程當中,編譯器會對異常說明作強制要求,但異常說明並非方法類型的一部分,方法類型由方法名和參數類型組成。所以不能基於異常說明來重載方法。
對於方法重寫時子類方法中的異常列表,要求要寬鬆得多。
具體代碼以下:
// 父類接口 public interface FileReader { List<String> readFromFile(String filePath) throws IOException; } class FileReader1 implements FileReader{ // 子類方法異常列表與父類徹底一致 @Override public List<String> readFromFile(String filePath) throws IOException { return null; } } class FileReader2 implements FileReader{ // 子類方法拋出父類方法異常的子異常 @Override public List<String> readFromFile(String filePath) throws FileNotFoundException { return null; } } class FileReader3 implements FileReader{ // 子類方法沒有拋出異常 @Override public List<String> readFromFile(String filePath){ return null; } }
Java 的方法重載,只涉及方法名和參數列表。方法返回值和異常列表都做爲方法重載的依據。
public List<String> readFromFile(String path) throws IOException{ return null; } /** 編譯不過 public List<String> readFromFile(String path) throws FileNotFoundException{ return null; } */
異常限制對構造函數不起做用,子類構造函數可以拋出任意異常,而沒必要理會基類構造函數所拋出的異常。但,由於基類構造函數必須以某種形式被調用,派生類構造函數的異常說明一定包含基類構造函數的異常說明。
構造器會把對象設置爲安全的初始化狀態,若是有別的工做,好比打開一個文件,這樣的動做只有在對象使用完畢而且用戶調用了清理方法才能清理。若是在構造函數中拋出異常,這些清理動做就不能正常工做,所以在編寫構造器時要格外注意。
class Parent{ Parent() throws IOException{ } } class Child extends Parent{ Child() throws IOException { super(); //這次拋出異常 } /** Child() throws IOException { // super 必須是第一個語句,沒法對異常進行捕獲 try { super(); //這次拋出異常 }catch (Exception e){ } } */ }
在編譯時被強制檢查的異常稱爲"受檢查的異常"。即在方法的聲明中聲明的異常。
受檢查異常要求方法調用者必須對異常進行處理。從某種角度來講,受檢查異常違反了 Java 異常處理的初衷。
private List<String> readFromFile(String filePath) throws IOException { Path path = Paths.get(filePath); return Files.readAllLines(path, Charset.defaultCharset()); }
在調用 readFromFile 方法時,沒法忽略對 IOException 的處理。
通常狀況下,面對受檢查異常,咱們一般這樣處理:
private void printFile2(String filePath){ try { List<String> lines = readFromFile(filePath); lines.forEach(System.out::println); }catch (IOException e){ // 使用異常鏈,將受檢查異常轉化爲運行時異常 throw new RuntimeException(e); } }
JDBC 接口中存在大量的受檢查異常,在操做數據庫時,會出現大量的try catch 樣板代碼,使核心邏輯埋葬在代碼海中。
爲此,Spring 對其進行優化,具體優化措施主要有:
jdbcTempalte 代碼片斷以下:
public <T> T execute(StatementCallback<T> action) throws DataAccessException { Assert.notNull(action, "Callback object must not be null"); Connection con = DataSourceUtils.getConnection(obtainDataSource()); Statement stmt = null; try { stmt = con.createStatement(); applyStatementSettings(stmt); T result = action.doInStatement(stmt); handleWarnings(stmt); return result; } catch (SQLException ex) { // Release Connection early, to avoid potential connection pool deadlock // in the case when the exception translator hasn't been initialized yet. String sql = getSql(action); JdbcUtils.closeStatement(stmt); stmt = null; DataSourceUtils.releaseConnection(con, getDataSource()); con = null; // 完成受檢查異常到運行時異常的轉化 throw translateException("StatementCallback", sql, ex); } finally { JdbcUtils.closeStatement(stmt); DataSourceUtils.releaseConnection(con, getDataSource()); } }
沒必要侷限於Java提供的異常類型。咱們能夠自定義異常類來表示程序中可能會遇到的特定問題。
要自定義異常類,必須從已有異常類繼承,最好的方式是選擇意思相近的異常類繼承。
// 業務異常 class BizException extends RuntimeException{ public BizException() { super(); } public BizException(String message) { super(message); } public BizException(String message, Throwable cause) { super(message, cause); } public BizException(Throwable cause) { super(cause); } }
異常通常是用名稱表明發生的問題,而且異常的名稱應該能夠望文知意。
異常自己也是類,存在一個完整的繼承體系。
Throwable被用來表示任何能夠做爲異常被拋出的類。
Throwable對象能夠分爲倆種類型(從Throwable繼承而來):
throwable主要是對異常棧進行維護,核心方法以下:
方法 | 含義 |
---|---|
printStackTrace | 打印調用棧信息,輸出到標準錯誤輸出(System.error) |
printStackTrace(PrintStream) | 指定Stream打印調用棧信息 |
printStackTrace(PrintWriter) | 指定Print打印調用棧信息 |
getStackTrace() | 獲取調用棧序列信息 |
fillInStackTrace() | 記錄棧幀的當前狀態 |
異常棧記錄了"把你帶到異常拋出點"的方法調用序列,是問題排查的主要信息之一。
public static void main(String... arg){ try { // 正常處理流程,正確執行過程作什麼事 Path path = Paths.get("var", "error"); List<String> lines = Files.readAllLines(path, Charset.defaultCharset()); System.out.println(lines); } catch (IOException e) { // 異常處理流程,出了問題怎麼辦 e.printStackTrace(); } }
運行程序,得到結果,異常棧以下:
java.nio.file.NoSuchFileException: var/error at sun.nio.fs.UnixException.translateToIOException(UnixException.java:86) at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:102) at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:107) at sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:214) at java.nio.file.Files.newByteChannel(Files.java:361) at java.nio.file.Files.newByteChannel(Files.java:407) at java.nio.file.spi.FileSystemProvider.newInputStream(FileSystemProvider.java:384) at java.nio.file.Files.newInputStream(Files.java:152) at java.nio.file.Files.newBufferedReader(Files.java:2781) at java.nio.file.Files.readAllLines(Files.java:3199) at com.geekhalo.exception.Demo.main(Demo.java:15) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:483) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Exception是與編程有關的全部異常類的基類。
方法 | 含義 |
---|---|
getMessage | 獲取詳細信息 |
getLocaliedMessage | 獲取本地語言表示的詳細信息 |
從RuntimeException派生出來的異常成爲「不受檢查異常」。這種異常會自動被Java虛擬機拋出,因此沒必要在方法的異常說明中列出來。
private void throwRuntimeException(){ throw new RuntimeException(); }
RuntimeException 及其子類 無需在方法中進行聲明。
完成自定義異常後,下一個關鍵點即是如何處理異常。
異常處理程序的目的就是處理所發生的異常。所以,第一個異常處理策略即是,處理異常,進行異常恢復。
private void recoveryException(String filePath){ try { List<String> lines = readFromFile(filePath); lines.forEach(System.out::println); }catch (IOException e){ // 打印日誌,從異常中恢復程序 LOGGER.error("failed to read from file {}", filePath, e); } } private List<String> readFromFile(String filePath) throws IOException { Path path = Paths.get(filePath); return Files.readAllLines(path, Charset.defaultCharset()); }
當你沒法獲得足夠信息,從而對異常進行恢復時。能夠把剛剛捕獲的異常從新拋出。在catch子句中已經得到了對當前異常對象的引用,能夠直接將其拋出。
private void printFile(String filePath) throws IOException{ try { List<String> lines = readFromFile(filePath); lines.forEach(System.out::println); }catch (IOException e){ // 從新拋出異常 throw e; } } // 方法異常說明 private List<String> readFromFile(String filePath) throws IOException { Path path = Paths.get(filePath); return Files.readAllLines(path, Charset.defaultCharset()); }
重拋異常會把異常拋給上一級調用,同一try後的catch子句被忽略。若是隻是把當前異常拋出,那麼printStackTrace顯示的是原來異常拋出點的調用鏈信息,而非從新拋出點的信息。若是想要更新調用信息,能夠調用fillInStackTrace方法,返回另外一個Throwable對象,它將當前調用棧信息填入原來的異常對象。
若是想要在捕獲一個異常後拋出另外一個新異常,並但願把原始異常信息保留下來,這成爲異常連。
Throwable的子類在構造器中均可以接受一個cause對象,用於表示原始異常,這樣把原始異常傳遞給新異常,使得當前位置建立並拋出的新異常,經過異常鏈追蹤到異常最初發生的位置。
private void printFile2(String filePath){ try { List<String> lines = readFromFile(filePath); lines.forEach(System.out::println); }catch (IOException e){ // 異常鏈 throw new BizException(e); } } // 方法異常說明 private List<String> readFromFile(String filePath) throws IOException { Path path = Paths.get(filePath); return Files.readAllLines(path, Charset.defaultCharset()); }
Throwable子類中,只有Error、Exception、RuntimeException在構造函數中提供了cause參數,若是要把其餘異常鏈起來,可使用initCause方法。
異常是框架設計不可遺漏的點。
框架中的異常處理,一樣遵循固定的操做流程:
Spring MVC 是最多見的 Web 框架,上手簡單,開發迅速。
遵循正常流程與異常處理分離策略。研發人員只需關心正常邏輯,由框架對異常流程進行統一處理。那應該怎麼操做呢?
首先,須要定義本身的業務異常。
public abstract class BusinessException extends RuntimeException{ /** * 異常處理碼 */ private final int code; /** * 異常消息 */ private final String msg; private final String timestamp = String.valueOf(System.currentTimeMillis()); protected BusinessException(int code, String msg){ this.code = code; this.msg = msg; } protected BusinessException(int code, String msg, Exception e) { super(e); this.code = code; this.msg = msg; } }
可使用 HandlerExceptionResolver 擴展,對異常進行定製。
RestHandlerExceptionResolver 對 Rest 請求的服務異常進行處理。將異常統一轉化爲 JSON 返回給用戶。
@Component public class RestHandlerExceptionResolver implements HandlerExceptionResolver { private static final Logger LOGGER = LoggerFactory.getLogger(RestHandlerExceptionResolver.class); @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 是 Rest 請求 而且可以處理 if(isRestRequest(handler) && isAcceptException(ex)){ // 將異常傳化爲 RestResVo 對象 RestResVo<Void> restResVo = RestResVo.error((BusinessException)ex); try { // 以 Json 格式進行寫回 response.getWriter().println(JSON.toJSONString(restResVo)); }catch (Exception e){ LOGGER.error("failed to write json {}", restResVo, e); } // empty ModelAndView說明已經處理 return new ModelAndView(); } return null; } private boolean isRestRequest(Object handler) { if (handler instanceof HandlerMethod){ HandlerMethod handlerMethod = (HandlerMethod) handler; return AnnotationUtils.findAnnotation(handlerMethod.getMethod(), ResponseBody.class) !=null || AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), ResponseBody.class) != null; } return false; } private boolean isAcceptException(Exception ex) { return ex instanceof BusinessException; } }
PageHandlerExceptionResolver 對頁面請求的異常進行處理。將異常統一轉發到 error 視圖。
@Component public class PageHandlerExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 是頁面請求而且可以處理當前異常 if(isPageRequest(handler) && isAcceptException(ex)){ // 返回 error 視圖 ModelAndView mv = new ModelAndView("error"); mv.addObject("error", ex); return mv; } return null; } private boolean isPageRequest(Object handler) { if (handler instanceof HandlerMethod){ HandlerMethod handlerMethod = (HandlerMethod) handler; return AnnotationUtils.findAnnotation(handlerMethod.getMethod(), ResponseBody.class) == null && AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), ResponseBody.class) == null; } return true; } private boolean isAcceptException(Exception ex) { return ex instanceof BusinessException; } }
在使用 Spring Cloud 進行微服務時,若是 Server 端發生異常,客戶端會收到一個 5xx 錯誤,從而中斷當前正常請求邏輯。但,異常中所含有的業務信息也一併丟失了,如何最大限度的保持異常信息呢?
首先,仍舊是定義本身的業務異常類。
@Data public class CodeBasedException extends RuntimeException { private Integer code; private String msg; private Object data; public CodeBasedException(){ super(); } public CodeBasedException(String msg) { super(msg); this.msg = msg; } public CodeBasedException(Integer code, String msg, Object data) { this.code = code; this.msg = msg; this.data = data; } public CodeBasedException(String message, Integer code, String msg, Object data) { super(message); this.code = code; this.msg = msg; this.data = data; } }
在 Server 端,捕獲業務異常,並將信息經過 Header 進行寫回。
HandlerInterceptorBasedExceptionBinder 在業務處理完成後,捕獲 CodeBasedException 異常,並將異常信息經過 Response 對象回寫到 Header 中。
public class HandlerInterceptorBasedExceptionBinder implements HandlerInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(HandlerInterceptorBasedExceptionBinder.class); @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { if (ex == null){ return; } if (ex instanceof CodeBasedException){ CodeBasedException codeBasedException = (CodeBasedException) ex; response.addHeader(SoaConstants.HEADER_ERROR_CODE, String.valueOf(codeBasedException.getCode())); response.addHeader(SoaConstants.HEADER_ERROR_MSG, encode(codeBasedException.getMsg())); response.addHeader(SoaConstants.HEADER_ERROR_EXCEPTION_MSG, encode(codeBasedException.getMessage())); return; } response.setHeader(SoaConstants.HEADER_ERROR_CODE, "500"); response.setHeader(SoaConstants.HEADER_ERROR_MSG, encode(ex.getMessage())); response.setHeader(SoaConstants.HEADER_ERROR_EXCEPTION_MSG, encode(String.valueOf(ex.getStackTrace()))); LOGGER.error("failed to handle request.", ex); } }
若是是 Spring Boot 項目,咱們須要完成 HandlerInterceptorBasedExceptionBinder 的註冊。
@Configuration public class SoaWebMvcConfigurer implements WebMvcConfigurer{ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new HandlerInterceptorBasedExceptionBinder()).addPathPatterns("/**"); } }
客戶端在獲取請求結果後,從 Header 中提取異常信息,並從新組裝並拋出異常。
FeignErrorDecoderBasedExceptionConverter 從 Header 中提取異常信息,並從新組裝並拋出 SoaRemoteCallException。
public class FeignErrorDecoderBasedExceptionConverter implements ErrorDecoder { private static final Logger LOGGER = LoggerFactory.getLogger(FeignErrorDecoderBasedExceptionConverter.class); public FeignErrorDecoderBasedExceptionConverter() { } @Override public Exception decode(String methodKey, Response response) { Map<String, Collection<String>> headers = response.headers(); report(methodKey, response); return checkException(headers); } private void report(String methodKey, Response response) { String message = format("status %s reading %s", response.status(), methodKey); try { if (response.body() != null) { String body = Util.toString(response.body().asReader()); message += "; content:\n" + body; } } catch (IOException ignored) { // NOPMD } LOGGER.error("status {}, message {}", response.status(), message); } private Exception checkException(Map<String, Collection<String>> headers) { String code = getValue(headers, SoaConstants.HEADER_ERROR_CODE); String msg = HeaderValueUtils.decode(getValue(headers, SoaConstants.HEADER_ERROR_MSG)); String exceptionMsg = HeaderValueUtils.decode(getValue(headers, SoaConstants.HEADER_ERROR_EXCEPTION_MSG)); Integer errorCode = NumberUtils.isNumber(code) ? Integer.valueOf(code) : -1; return new SoaRemoteCallException(exceptionMsg, errorCode, msg, ""); } private String getValue(Map<String, Collection<String>> headers, String key) { Collection<String> values = headers.get(key); if (values != null && values.size() == 1){ return values.iterator().next(); } LOGGER.debug("failed to find value of {} in header {}", key, headers); return null; } }
最後,須要完成 FeignErrorDecoderBasedExceptionConverter 的註冊。
@Bean public FeignErrorDecoderBasedExceptionConverter exceptionCheckFeignDecoder(){ return new FeignErrorDecoderBasedExceptionConverter(); }
異常使用方面,經過須要兩步操做: