Ch12.5-3 : Exception

[Exception]

錯誤是個開發程式時必經的過程,如何讓錯誤的訊息,更容易讓開發者知道,程式到底錯誤在哪裡,我找了一些參考資料後,尋找到fastClasspathScanner這個免費開源專案,只要設定好要尋找專案的package就可以找出專案中相關的類別,利用這個特性,我們就可以把錯誤訊息的StackTraceElement進行比較,找出只屬於我們專案內的錯誤,減少不要相關錯誤訊息印出,加速我們找到問題的根源,那我們就開始實作吧!


[相關程式路徑]

build.sbt
app
 └ controllers
   └ AcidController.java      <-- ACID主要controller
 └ test
   └ db
     └ Test_FooLocalDAO.java  <-- 測試程式
 └pojo
   └ error
     └ ErrorLogInfo.java      <-- 錯誤紀錄資訊
       RelatedClass.java      <-- 錯誤相關類別
 └ error
   └ HelperException.java     <-- 例外發生時候,利用Helper去尋找相關的程式錯誤
 └ utils
   └ tool
     └ Utils_FastScan.java    <--- 根據我們定義的package,去尋找相關的類別

[相關程式]

build.sbt

首先需要先新增fast-classpath-scanner到我們的build.sbt,這樣我們專案就可以使用這個專案的相關程式了。

libraryDependencies ++= Seq(
  ...
  // fast-classpath-scanner
  "io.github.lukehutch" % "fast-classpath-scanner" % "2.0.9",
  ...
)

utils.tool.Utils_FastScan.java

這隻類別主要前面的String陣列是目前該專案的package的名稱,放在前面全域變數的宣告。getProjectClasses這個方法會根據classPackages去找出專案相關的類別。最後getProjectClassMethodMapping除了找到專案的類別之外,會去把每一隻類別的方法,組成一個Map物件來存放,這樣我們就可以利用類別名稱,再去比對方法,就可以找到相關類別的方法。

package utils.tool;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;

public enum Utils_FastScan {

  un;

  /**
   * Add your project top level package 
   */
  final static String[] classPackages 
                        = {"annotation" , "aop" , "common" , "error" , "filters" , "modules" ,
                           "pojo" , "services" , "utils" , "views" , "test"};


  /**
   * According classPackages to find our classes
   */
  public List<String> getProjectClasses() {
    List<String> classNames = new ArrayList<String>();
    FastClasspathScanner fastScaner = new FastClasspathScanner(classPackages);
    classNames.addAll(fastScaner.scan().getNamesOfAllClasses());
    return classNames;
  }


  /**
   * According class to Map our class the Method
   */
  public Map<String , HashMap<String , String>> getProjectClassMethodMapping(){
    Map<String , HashMap<String , String>> classMaps = new HashMap<String , HashMap<String , String>>();
    List<String> classNames = getProjectClasses();

    for(String className : classNames){
      Method[] classMethods;
      try {
        classMethods = Class.forName(className).getMethods();
        HashMap<String , String> methodMaps = new HashMap<String , String>();
        for(int index = 0 ; index < classMethods.length ; index++){
          methodMaps.put(classMethods[index].getName(), classMethods[index].getName());
        }
        classMaps.put(className, methodMaps);
      } catch (SecurityException e) {
      } catch (ClassNotFoundException e) {
      }
    }
    return classMaps;
  }


}

app.pojo.error.RelatedClass.java

這個類別是用來紀錄相關類別被呼叫的方法,以及例外發生時,相關程式丟出錯誤的行數。

package pojo.error;

public class RelatedClass {

  /** 類別名稱 */
  private String className;

  /** 類別方法名稱 */
  private String method;

  /** 行數 */
  private String lineNumber;

  public String getClassName() {
    return className;
  }

  public void setClassName(String className) {
    this.className = className;
  }

  public String getMethod() {
    return method;
  }

  public void setMethod(String method) {
    this.method = method;
  }

  public String getLineNumber() {
    return lineNumber;
  }

  public void setLineNumber(String lineNumber) {
    this.lineNumber = lineNumber;
  }


}

app.pojo.error.ErrorLogInfo

這個類別是當程式發生例外的時候,要儲存的資訊。

package pojo.error;

import java.util.List;

public class ErrorLogInfo {

  /** 錯誤原因 */
  private String casue;

  /** 錯誤訊息 */
  private String message;

  /** 錯誤訊息 */
  private String localMessage;

  /** 發生例外的類別,所擁有的inputs */
  private List<Object> inputs;

  /** 發生例外後,我們專案相關的類別 */
  private List<RelatedClass> relatedClasses;

  public String getCasue() {
    return casue;
  }

  public void setCasue(String casue) {
    this.casue = casue;
  }

  public String getMessage() {
    return message;
  }

  public void setMessage(String message) {
    this.message = message;
  }

  public String getLocalMessage() {
    return localMessage;
  }

  public void setLocalMessage(String localMessage) {
    this.localMessage = localMessage;
  }

  public List<Object> getInputs() {
    return inputs;
  }

  public void setInputs(List<Object> inputs) {
    this.inputs = inputs;
  }

  public List<RelatedClass> getRelatedClasses() {
    return relatedClasses;
  }

  public void setRelatedClasses(List<RelatedClass> relatedClasses) {
    this.relatedClasses = relatedClasses;
  }


}

app.error.HelperException.java

這個Helper,第一個參數主要接收Exception 的錯誤物件,第二個傳入的參數是可以0~N的物件,主要會傳入參數是發生例外的類別時,如果有傳入參數的話,我們可以接受這些參數,來幫助我們辨別是否例外跟我們傳入的參數或物件有關係。

package error;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import play.libs.Json;
import pojo.error.ErrorLogInfo;
import pojo.error.RelatedClass;
import utils.tool.Utils_FastScan;

public enum HelperException {

  un;

  /**
   * <pre>
   * Check 1 : 確認是否有 Exception
   * Check 2 : 印出相關errorMessage
   * Check 3 : 若有input,印出相關input的參數
   * Check 4 : 印出相關Class method , line number error
   * </pre> 
   */
  public String genException(Exception e , Object... inputs) {
    if(e == null){
      return "";
    }

    ErrorLogInfo errorLogInfo = new ErrorLogInfo();
    errorLogInfo.setCasue(e.getCause()!=null ? e.getCause().toString() : "null");
    errorLogInfo.setMessage(e.getMessage());
    errorLogInfo.setLocalMessage(e.getLocalizedMessage());
    errorLogInfo.setInputs(Arrays.asList(inputs));

    Map<String , HashMap<String , String>> classMaps = Utils_FastScan.un.getProjectClassMethodMapping();
    List<RelatedClass> relatedClasses = new ArrayList<RelatedClass>();
    for (StackTraceElement ele : e.getStackTrace()) {
      if(classMaps != null && 
         classMaps.get(ele.getClassName()) != null && 
         classMaps.get(ele.getClassName()).get(ele.getMethodName()) != null ){
        RelatedClass realtedClass = new RelatedClass();
        realtedClass.setClassName(ele.getClassName());
        realtedClass.setMethod(ele.getMethodName());
        realtedClass.setLineNumber(Integer.toString(ele.getLineNumber()));
        relatedClasses.add(realtedClass);
      }
    }
    errorLogInfo.setRelatedClasses(relatedClasses);

    play.Logger.error(Json.toJson(errorLogInfo).toString());
    return Json.toJson(errorLogInfo).toString();
  }



}

app.test.db.Test_FooLocalDAO.java

針對我們寫好的HelperException,來寫case3的測試案例。

package test.db;

import org.apache.ibatis.session.SqlSessionManager;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import com.google.inject.Guice;
import com.google.inject.Injector;

import error.HelperException;
import pojo.web.signup.request.SignupRequest;
import services.WebService;

public class Test_FooLocalDAO extends AbstractFooDAO{

  static Test_FooLocalDAO dao;

  @BeforeClass
  public static void before(){
    play.Logger.info("Test_FooLocalDAO , test case start");
    Injector injector = Guice.createInjector(new modules.MyBatisModule());
    dao = new Test_FooLocalDAO();
    dao.sqlSessionManager = injector.getInstance(SqlSessionManager.class);
    dao.webService = injector.getInstance(WebService.class);
    dao.fooServiceImpl = injector.getInstance(FooServiceImpl.class);
  }

 ...

  @Test
  public void case3(){
    play.Logger.info("----------------------------------------------");
    play.Logger.info("Case 3 Test Annation Transactional rollback with HelperException - start");
    SignupRequest request = new SignupRequest();
    try{
      request.setEmail("333@star.com.tw");
      request.setUsername("333");
      request.setPassword("333");
      dao.testErrorWithAnnotationTransation(request);
    } catch (Exception e) {
      HelperException.un.genException(e , request );
    }
    play.Logger.info("Test Annation Transactional rollback - end");
    play.Logger.info("----------------------------------------------");
  }

  ...

  @AfterClass
  public static void after(){
    play.Logger.info("Test_FooLocalDAO , test case finish");
  }

}

app.controllers.AcidController.java

測試程式完成之後,我們就可以把這段程式,寫到controller測試是否可以印出相關例外程式了。

package controllers;

import javax.inject.Inject;

import error.HelperException;
import play.mvc.Controller;
import play.mvc.Result;
import pojo.web.signup.request.SignupRequest;
import test.db.FooDAO;


/**
 * ACID testing
 * 
 * TEST CASE : @link {Test_FooLocalDAO}
 */
public class AcidController extends Controller {

  @Inject
  FooDAO fooDao ;

  public Result annotationACIDWithHelperException(){
    String tewat = "";
    SignupRequest request = new SignupRequest();
    try{
      request.setEmail("333@star.com.tw");
      request.setUsername("333");
      request.setPassword("333");
      fooDao.testErrorWithAnnotationTransation(request);
    } catch (Exception e) {
      e.printStackTrace();
      tewat = HelperException.un.genException(e,request);
    }
    return ok("annotationACID With HelperException rollback Testing " +
              "\n Error info = " + tewat);
  }

}

conf.routes

寫下對應的網址與呼叫的controller

# http://127.0.0.1:9000/acid/annotationACIDWithHelperException
GET        /acid/annotationACIDWithHelperException    controllers.AcidController.annotationACIDWithHelperException()

Offline CASE1:

當錯誤發生時,會根據FastClasspathScannerException的比較之後,會把相關結果放到EerrorLogInfo,這樣就可以大幅度減少例外訊息時,更為精準的提示使用者發生的錯誤訊息。

[info] application - Test_FooLocalDAO , test case start
[info] application - ----------------------------------------------
[info] application - Case 3 Test Annation Transactional rollback with HelperException - start
[error] application - {"casue":"java.lang.ArithmeticException: / by zero","message":"insert database error",
"localMessage":"insert database error","inputs":[{"email":"333@star.com.tw","username":"333","password":"333",
"retypePassword":null}],"relatedClasses":[{"className":"test.db.AbstractFooDAO","method":"testErrorWithAnnotationTransation"
,"lineNumber":"46"},{"className":"test.db.Test_FooLocalDAO","method":"case3","lineNumber":"70"}]}
[info] application - Test Annation Transactional rollback - end
[info] application - ----------------------------------------------
[info] application - Test_FooLocalDAO , test case finish

Online CASE1:

測試之後,我們就可以更容易釐清錯誤發生時,相關的程式是哪些,更容易讓我們去找出錯誤的問題點。

Test URL : http://127.0.0.1:9000/acid/annotationACIDWithHelperException

annotationACID With HelperException rollback Testing 
 Error info = {"casue":"java.lang.ArithmeticException: / by zero","message":"insert database error",
 "localMessage":"insert database error","inputs":[{"email":"333@star.com.tw","username":"333","password":"333",
 "retypePassword":null}],"relatedClasses":[{"className":"test.db.AbstractFooDAO",
 "method":"testErrorWithAnnotationTransation","lineNumber":"46"}]}

[Final]

這個小節學習使用別人寫好的專案,來去整理與設計一個錯誤訊息提示的功能,可以大幅降低看例外發生時的錯誤訊息。加快錯誤時可以找到合適的修正方式。


[Reference]

1.can-you-find-all-classes-in-a-package-using-reflection

http://stackoverflow.com/questions/520328/can-you-find-all-classes-in-a-package-using-reflection

2.fast-classpath-scanner

https://github.com/lukehutch/fast-classpath-scanner/wiki

results matching ""

    No results matching ""