一個同事將公司的開發框架基於最新的Spring、Tomcat、Java版本作了部分修改,拿來開發運行之後,發現一個奇怪的空指針異常。
還原一下當時的場景,代碼大概如下,所有的Servlet繼承自BaseServlet。以DefaultServlet為例,當有DefaultServlet請求到達時,會映射到一個ServletProxy的servlet,然後再轉發至DefaultServlet。在轉發之前已經調用了DefaultServlet的setLogger方法,假設轉發到doGet方法,doGet方法先調用了response(req,res)又轉到了execute方法,execute方法打印一行log,至此都沒有問題。接下來doGet方法也打印一行log"@@@@@@doGet",在這裡就報了空指針異常。logger為null,這就奇怪了,怎麼會為null呢。
public abstract class BaseServlet extends HttpServlet { protected Log logger; @Override public final void doGet( HttpServletRequest req, HttpServletResponse res ) throws IOException { response( req, res ); logger.debug( "@@@@@@doGet"); } @Override public final void doPost( HttpServletRequest req, HttpServletResponse res ) throws IOException { response( req, res ); logger.debug( "@@@@@@doPost"); } private void response( HttpServletRequest req, HttpServletResponse res ) throws IOException { String result = ""; try { result = execute( req, res ); } finally { //返回結果 } } public void setLogger( Log logger ) { this.logger = logger; } public abstract String execute( HttpServletRequest req, HttpServletResponse res ) throws IOException; } public class DefaultServlet extends BaseServlet { @Override public String execute(HttpServletRequest req, HttpServletResponse res) throws IOException { logger.info("@@@@@@@DefaultServlet"); } }
倒騰了半天也找不出個原因,最後與原來的框架比較了一下,發現doGet與doPost被加上了final修飾符。我去,這樣一想就有點頭緒了,因為在Spring配置文件中配置了動態代理做切面。動態代理又是用的CGLib。
下面順便說一下CGLib的大概原理(有部分猜測的成分,錯誤之處請指正),假設有個類A,如果用CGLib做動態代理,將會在字節碼的層面上動態生成一個類B並加載。模擬代碼如下,B繼承自A同時又有對A的引用,B類所有可重寫的方法都要重寫並調用A類型target的同名方法,當然在調用target方法之前可以做調用前操作和調用後操作,這才是代理的用途,這裡就省略掉了。
在main函數裡new了一個B類的實例,並調用了setName方法,實際上執行的是target的setName方法,設置的是target的字段name,B實例的字段name仍然為空。調用notFinalMethod方法也是調用target的方法並能把target的字段name打印出來。但是finalMethod方法由於有final修飾符,所以不能在B中重寫,當調用finalMethod方法時,就只能乖乖地調用B本身的finalMethod方法而不能調用target的finalMethod方法,這時由於B實例的name為空,所以打印出來的值也就為空了。
public class CGLibSimulate { public static void main(String[] args) { A a=new B(); a.setName("aa"); a.notFinalMethod(); a.finalMethod(); } public static class A { protected String name = null; public final void finalMethod() { System.out.println(name); } public void notFinalMethod() { System.out.println(name); } public void setName(String name){ this.name=name; } } public static class B extends A { private A target=new A(); @Override public void notFinalMethod() { target.notFinalMethod(); } @Override public void setName(String name){ target.setName(name); } } }
理解了這個應該就理解了上邊空指針問題的原因了吧,希望能幫到遇到此問題的人。