Java基礎加強總結之動態代理(Proxy)
一、代理的概念
動態代理技術是整個java技術中最重要的一個技術,它是學習java框架的基礎,不會動態代理技術,那麼在學習Spring這些框架時是學不明白的。
動態代理技術就是用來產生一個物件的代理物件的。在開發中為什麼需要為一個物件產生代理物件呢?
舉一個現實生活中的例子:歌星或者明星都有一個自己的經紀人,這個經紀人就是他們的代理人,當我們需要找明星表演時,不能直接找到該明星,只能是找明星的代理人。比如劉德華在現實生活中非常有名,會唱歌,會跳舞,會拍戲,劉德華在沒有出名之前,我們可以直接找他唱歌,跳舞,拍戲,劉德華出名之後,他乾的第一件事就是找一個經紀人,這個經紀人就是劉德華的代理人(代理),當我們需要找劉德華表演時,不能直接找到劉德華了(劉德華說,你找我代理人商談具體事宜吧!),只能是找劉德華的代理人,因此劉德華這個代理人存在的價值就是攔截我們對劉德華的直接訪問!
這個現實中的例子和我們在開發中是一樣的,我們在開發中之所以要產生一個物件的代理物件,主要用於攔截對真實業務物件的訪問。那麼代理物件應該具有什麼方法呢?代理物件應該具有和目標物件相同的方法
所以在這裡明確代理物件的兩個概念:
1、代理物件存在的價值主要用於攔截對真實業務物件的訪問。
2、代理物件應該具有和目標物件(真實業務物件)相同的方法。劉德華(真實業務物件)會唱歌,會跳舞,會拍戲,我們現在不能直接找他唱歌,跳舞,拍戲了,只能找他的代理人(代理物件)唱歌,跳舞,拍戲,一個人要想成為劉德華的代理人,那麼他必須具有和劉德華一樣的行為(會唱歌,會跳舞,會拍戲),劉德華有什麼方法,他(代理人)就要有什麼方法,我們找劉德華的代理人唱歌,跳舞,拍戲,但是代理人不是真的懂得唱歌,跳舞,拍戲的,真正懂得唱歌,跳舞,拍戲的是劉德華,在現實中的例子就是我們要找劉德華唱歌,跳舞,拍戲,那麼只能先找他的經紀人,交錢給他的經紀人,然後經紀人再讓劉德華去唱歌,跳舞,拍戲。
二、java中的代理
2.1、"java.lang.reflect.Proxy"類介紹
現在要生成某一個物件的代理物件,這個代理物件通常也要編寫一個類來生成,所以首先要編寫用於生成代理物件的類。在java中如何用程式去生成一個物件的代理物件呢,java在JDK1.5之後提供了一個"java.lang.reflect.Proxy"類,通過"Proxy"類提供的一個newProxyInstance方法用來建立一個物件的代理物件,如下所示:
1 static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
newProxyInstance方法用來返回一個代理物件,這個方法總共有3個引數,ClassLoader loader用來指明生成代理物件使用哪個類裝載器,Class<?>[] interfaces用來指明生成哪個物件的代理物件,通過介面指定,InvocationHandler h用來指明產生的這個代理物件要做什麼事情。所以我們只需要呼叫newProxyInstance方法就可以得到某一個物件的代理物件了。
2.2、編寫生成代理物件的類
在java中規定,要想產生一個物件的代理物件,那麼這個物件必須要有一個介面,所以我們第一步就是設計這個物件的介面,在介面中定義這個物件所具有的行為(方法)
1、定義物件的行為介面
1 package cn.gacl.proxy; 2 3 /** 4 * @ClassName: Person 5 * @Description: 定義物件的行為 6 * @author: 孤傲蒼狼 7 * @date: 2014-9-14 下午9:44:22 8 * 9 */ 10 public interface Person { 11 12 /** 13 * @Method: sing 14 * @Description: 唱歌 15 * @Anthor:孤傲蒼狼 16 * 17 * @param name 18 * @return 19 */ 20 String sing(String name); 21 /** 22 * @Method: sing 23 * @Description: 跳舞 24 * @Anthor:孤傲蒼狼 25 * 26 * @param name 27 * @return 28 */ 29 String dance(String name); 30 }
2、定義目標業務物件類
1 package cn.gacl.proxy; 2 3 /** 4 * @ClassName: LiuDeHua 5 * @Description: 劉德華實現Person介面,那麼劉德華會唱歌和跳舞了 6 * @author: 孤傲蒼狼 7 * @date: 2014-9-14 下午9:22:24 8 * 9 */ 10 public class LiuDeHua implements Person { 11 12 public String sing(String name){ 13 System.out.println("劉德華唱"+name+"歌!!"); 14 return "歌唱完了,謝謝大家!"; 15 } 16 17 public String dance(String name){ 18 System.out.println("劉德華跳"+name+"舞!!"); 19 return "舞跳完了,多謝各位觀眾!"; 20 } 21 }
3、建立生成代理物件的代理類
1 package cn.gacl.proxy; 2 3 import java.lang.reflect.InvocationHandler; 4 import java.lang.reflect.Method; 5 import java.lang.reflect.Proxy; 6 7 /** 8 * @ClassName: LiuDeHuaProxy 9 * @Description: 這個代理類負責生成劉德華的代理人 10 * @author: 孤傲蒼狼 11 * @date: 2014-9-14 下午9:50:02 12 * 13 */ 14 public class LiuDeHuaProxy { 15 16 //設計一個類變數記住代理類要代理的目標物件 17 private Person ldh = new LiuDeHua(); 18 19 /** 20 * 設計一個方法生成代理物件 21 * @Method: getProxy 22 * @Description: 這個方法返回劉德華的代理物件:Person person = LiuDeHuaProxy.getProxy();//得到一個代理物件 23 * @Anthor:孤傲蒼狼 24 * 25 * @return 某個物件的代理物件 26 */ 27 public Person getProxy() { 28 //使用Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)返回某個物件的代理物件 29 return (Person) Proxy.newProxyInstance(LiuDeHuaProxy.class 30 .getClassLoader(), ldh.getClass().getInterfaces(), 31 new InvocationHandler() { 32 /** 33 * InvocationHandler介面只定義了一個invoke方法,因此對於這樣的介面,我們不用單獨去定義一個類來實現該介面, 34 * 而是直接使用一個匿名內部類來實現該介面,new InvocationHandler() {}就是針對InvocationHandler介面的匿名實現類 35 */ 36 /** 37 * 在invoke方法編碼指定返回的代理物件乾的工作 38 * proxy : 把代理物件自己傳遞進來 39 * method:把代理物件當前呼叫的方法傳遞進來 40 * args:把方法引數傳遞進來 41 * 42 * 當呼叫代理物件的person.sing("冰雨");或者 person.dance("江南style");方法時, 43 * 實際上執行的都是invoke方法裡面的程式碼, 44 * 因此我們可以在invoke方法中使用method.getName()就可以知道當前呼叫的是代理物件的哪個方法 45 */ 46 @Override 47 public Object invoke(Object proxy, Method method, 48 Object[] args) throws Throwable { 49 //如果呼叫的是代理物件的sing方法 50 if (method.getName().equals("sing")) { 51 System.out.println("我是他的經紀人,要找他唱歌得先給十萬塊錢!!"); 52 //已經給錢了,經紀人自己不會唱歌,就只能找劉德華去唱歌! 53 return method.invoke(ldh, args); //代理物件呼叫真實目標物件的sing方法去處理使用者請求 54 } 55 //如果呼叫的是代理物件的dance方法 56 if (method.getName().equals("dance")) { 57 System.out.println("我是他的經紀人,要找他跳舞得先給二十萬塊錢!!"); 58 //已經給錢了,經紀人自己不會唱歌,就只能找劉德華去跳舞! 59 return method.invoke(ldh, args);//代理物件呼叫真實目標物件的dance方法去處理使用者請求 60 } 61 62 return null; 63 } 64 }); 65 } 66 }
測試程式碼:
1 package cn.gacl.proxy; 2 3 public class ProxyTest { 4 5 public static void main(String[] args) { 6 7 LiuDeHuaProxy proxy = new LiuDeHuaProxy(); 8 //獲得代理物件 9 Person p = proxy.getProxy(); 10 //呼叫代理物件的sing方法 11 String retValue = p.sing("冰雨"); 12 System.out.println(retValue); 13 //呼叫代理物件的dance方法 14 String value = p.dance("江南style"); 15 System.out.println(value); 16 } 17 }
執行結果如下:
Proxy類負責建立代理物件時,如果指定了handler(處理器),那麼不管使用者呼叫代理物件的什麼方法,該方法都是呼叫處理器的invoke方法。
由於invoke方法被呼叫需要三個引數:代理物件、方法、方法的引數,因此不管代理物件哪個方法呼叫處理器的invoke方法,都必須把自己所在的物件、自己(呼叫invoke方法的方法)、方法的引數傳遞進來。
三、動態代理應用
在動態代理技術裡,由於不管使用者呼叫代理物件的什麼方法,都是呼叫開發人員編寫的處理器的invoke方法(這相當於invoke方法攔截到了代理物件的方法呼叫)。並且,開發人員通過invoke方法的引數,還可以在攔截的同時,知道使用者呼叫的是什麼方法,因此利用這兩個特性,就可以實現一些特殊需求,例如:攔截使用者的訪問請求,以檢查使用者是否有訪問許可權、動態為某個物件新增額外的功能。
3.1、在字元過濾器中使用動態代理解決中文亂碼
在平時的JavaWeb專案開發中,我們一般會寫一個CharacterEncodingFilter(字元過濾器)來解決整個JavaWeb應用的中文亂碼問題,如下所示:
1 package me.gacl.web.filter; 2 3 import java.io.IOException; 4 5 import javax.servlet.Filter; 6 import javax.servlet.FilterChain; 7 import javax.servlet.FilterConfig; 8 import javax.servlet.ServletException; 9 import javax.servlet.ServletRequest; 10 import javax.servlet.ServletResponse; 11 12 /** 13 * @ClassName: CharacterEncodingFilter 14 * @Description: 解決中文亂碼的字元過濾器 15 * @author: 孤傲蒼狼 16 * @date: 2014-9-14 下午10:38:12 17 * 18 */ 19 public class CharacterEncodingFilter implements Filter { 20 21 @Override 22 public void init(FilterConfig filterConfig) throws ServletException { 23 24 } 25 26 @Override 27 public void doFilter(ServletRequest request, ServletResponse response, 28 FilterChain chain) throws IOException, ServletException { 29 //解決以Post方式提交的中文亂碼問題 30 request.setCharacterEncoding("UTF-8"); 31 response.setCharacterEncoding("UTF-8"); 32 response.setContentType("text/html;charset=UTF-8"); 33 chain.doFilter(request, response); 34 } 35 36 @Override 37 public void destroy() { 38 39 } 40 }
但是這種寫法是沒有辦法解決以get方式提交中文引數時的亂碼問題的,我們可以用如下的程式碼來證明上述的解決中文亂碼過濾器只對以post方式提交中文引數時有效,而對於以get方式提交中文引數時無效
jsp測試頁面如下:
1 <%@ page language="java" pageEncoding="UTF-8"%> 2 <%--引入jstl標籤庫 --%> 3 <%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> 4 <!DOCTYPE HTML> 5 <html> 6 <head> 7 <title>使用字元過濾器解決解決get、post請求方式下的中文亂碼問題</title> 8 </head> 9 <body> 10 <%--使用c:url標籤構建url,構建好的url儲存在servletDemo1變數中--%> 11 <c:url value="/servlet/ServletDemo1" scope="page" var="servletDemo1"> 12 <%--構建的url的附帶的中文引數 ,引數名是:username,值是:孤傲蒼狼--%> 13 <c:param name="username" value="孤傲蒼狼"></c:param> 14 </c:url> 15 <%--使用get的方式訪問 --%> 16 <a href="${servletDemo1}">超連結(get方式請求)</a> 17 <hr/> 18 <%--使用post方式提交表單 --%> 19 <form action="${pageContext.request.contextPath}/servlet/ServletDemo1" method="post"> 20 使用者名稱:<input type="text" name="username" value="孤傲蒼狼" /> 21 <input type="submit" value="post方式提交"> 22 </form> 23 24 </body> 25 </html>處理請求的ServletDemo1程式碼如下:
1 package me.gacl.web.controller; 2 3 import java.io.IOException; 4 import java.io.PrintWriter; 5 6 import javax.servlet.ServletException; 7 import javax.servlet.http.HttpServlet; 8 import javax.servlet.http.HttpServletRequest; 9 import javax.servlet.http.HttpServletResponse; 10 11 public class ServletDemo1 extends HttpServlet { 12 13 public void doGet(HttpServletRequest request, HttpServletResponse response) 14 throws ServletException, IOException { 15 // 接收引數 16 String username = request.getParameter("username"); 17 // 獲取請求方式 18 String method = request.getMethod(); 19 // 獲取輸出流 20 PrintWriter out = response.getWriter(); 21 out.write("請求的方式:" + method); 22 out.write("<br/>"); 23 out.write("接收到的引數:" + username); 24 } 25 26 public void doPost(HttpServletRequest request, HttpServletResponse response) 27 throws ServletException, IOException { 28 doGet(request, response); 29 } 30 }
在web.xml中註冊上述的CharacterEncodingFilter和ServletDemo1
1 <filter> 2 <filter-name>CharacterEncodingFilter</filter-name> 3 <filter-class>me.gacl.web.filter.CharacterEncodingFilter</filter-class> 4 </filter> 5 6 <filter-mapping> 7 <filter-name>CharacterEncodingFilter</filter-name> 8 <url-pattern>/*</url-pattern> 9 </filter-mapping> 10 11 <servlet> 12 <servlet-name>ServletDemo1</servlet-name> 13 <servlet-class>me.gacl.web.controller.ServletDemo1</servlet-class> 14 </servlet> 15 16 <servlet-mapping> 17 <servlet-name>ServletDemo1</servlet-name> 18 <url-pattern>/servlet/ServletDemo1</url-pattern> 19 </servlet-mapping>
測試結果如下所示:
從執行結果可以看出,上述的過濾器的確是不能解決以get方式提交中文引數的亂碼問題,下面使用動態代理技術改造上述的過濾器,使之能夠解決以get方式提交中文引數的亂碼問題,改造後的過濾器程式碼如下:
1 package me.gacl.web.filter; 2 3 import java.io.IOException; 4 import java.lang.reflect.InvocationHandler; 5 import java.lang.reflect.Method; 6 import java.lang.reflect.Proxy; 7 8 import javax.servlet.Filter; 9 import javax.servlet.FilterChain; 10 import javax.servlet.FilterConfig; 11 import javax.servlet.ServletException; 12 import javax.servlet.ServletRequest; 13 import javax.servlet.ServletResponse; 14 import javax.servlet.http.HttpServletRequest; 15 import javax.servlet.http.HttpServletResponse; 16 17 /** 18 * @ClassName: CharacterEncodingFilter 19 * @Description: 解決中文亂碼的字元過濾器 20 * @author: 孤傲蒼狼 21 * @date: 2014-9-14 下午10:38:12 22 * 23 */ 24 public class CharacterEncodingFilter implements Filter { 25 26 @Override 27 public void init(FilterConfig filterConfig) throws ServletException { 28 29 } 30 31 @Override 32 public void doFilter(ServletRequest req, ServletResponse resp, 33 FilterChain chain) throws IOException, ServletException { 34 35 final HttpServletRequest request = (HttpServletRequest) req; 36 HttpServletResponse response = (HttpServletResponse) resp; 37 //解決以Post方式提交的中文亂碼問題 38 request.setCharacterEncoding("UTF-8"); 39 response.setCharacterEncoding("UTF-8"); 40 response.setContentType("text/html;charset=UTF-8"); 41 //獲取獲取HttpServletRequest物件的代理物件 42 ServletRequest requestProxy = getHttpServletRequestProxy(request); 43 /** 44 * 傳入代理物件requestProxy給doFilter方法, 45 * 這樣使用者在使用request物件時實際上使用的是HttpServletRequest物件的代理物件requestProxy 46 */ 47 chain.doFilter(requestProxy, response); 48 } 49 50 51 /** 52 * @Method: getHttpServletRequestProxy 53 * @Description: 獲取HttpServletRequest物件的代理物件 54 * @Anthor:孤傲蒼狼 55 * 56 * @param request 57 * @return HttpServletRequest物件的代理物件 58 */ 59 private ServletRequest getHttpServletRequestProxy(final HttpServletRequest request){ 60 ServletRequest proxy = (ServletRequest) Proxy.newProxyInstance( 61 CharacterEncodingFilter.class.getClassLoader(), 62 request.getClass().getInterfaces(), 63 new InvocationHandler(){ 64 @Override 65 public Object invoke(Object proxy, Method method, Object[] args) 66 throws Throwable { 67 //如果請求方式是get並且呼叫的是getParameter方法 68 if (request.getMethod().equalsIgnoreCase("get") && method.getName().equals("getParameter")) { 69 //呼叫getParameter方法獲取引數的值 70 String value = (String) method.invoke(request, args); 71 if(value==null){ 72 return null; 73 } 74 //解決以get方式提交的中文亂碼問題 75 return new String(value.getBytes("iso8859-1"),"UTF-8"); 76 }else { 77 //直接呼叫相應的方法進行處理 78 return method.invoke(request, args); 79 } 80 } 81 }); 82 //返回HttpServletRequest物件的代理物件 83 return proxy; 84 } 85 86 @Override 87 public void destroy() { 88 89 } 90 }
我們在過濾器中使用動態代理技術生成一個HttpServletRequest物件的代理物件requestProxy,然後把代理物件requestProxy進行chain.doFilter(requestProxy, response)傳遞給使用者使用,這樣使用者實際上使用的就是HttpServletRequest物件的代理物件requestProxy。然而這一過程對於使用者來說是透明的,使用者是不知道自己使用的HttpServletRequest物件是一個代理物件requestProxy,由於代理物件requestProxy和目標物件HttpServletRequest具有相同的方法,當用戶呼叫getParameter方法接收中文引數時,實際上呼叫的就是代理物件requestProxy的invoke方法,因此我們就可以在invoke方法中就判斷當前的請求方式以及使用者正在呼叫的方法,如果判斷當前的請求方式是get方式並且使用者正在呼叫的是getParameter方法,那麼我們就可以手動處理get方式提交中文引數的中文亂碼問題了。
測試結果如下所示:
3.2、在字元過濾器中使用動態代理壓縮伺服器響應的內容後再輸出到客戶端
壓縮過濾器的程式碼如下:
1 package me.gacl.web.filter; 2 3 import java.io.ByteArrayOutputStream; 4 import java.io.IOException; 5 import java.io.OutputStreamWriter; 6 import java.io.PrintWriter; 7 import java.lang.reflect.InvocationHandler; 8 import java.lang.reflect.Method; 9 import java.lang.reflect.Proxy; 10 import java.util.zip.GZIPOutputStream; 11 12 import javax.servlet.Filter; 13 import javax.servlet.FilterChain; 14 import javax.servlet.FilterConfig; 15 import javax.servlet.ServletException; 16 import javax.servlet.ServletOutputStream; 17 import javax.servlet.ServletRequest; 18 import javax.servlet.ServletResponse; 19 import javax.servlet.http.HttpServletRequest; 20 import javax.servlet.http.HttpServletResponse; 21 22 /** 23 * @ClassName: GzipFilter 24 * @Description: 壓縮過濾器,將web應用中的文字都經過壓縮後再輸出到瀏覽器 25 * @author: 孤傲蒼狼 26 * @date: 2014-9-15 下午9:35:36 27 * 28 */ 29 public class GzipFilter implements Filter { 30 31 @Override 32 public void init(FilterConfig filterConfig) throws ServletException { 33 34 } 35 36 @Override 37 public void doFilter(ServletRequest req, ServletResponse resp, 38 FilterChain chain) throws IOException, ServletException { 39 40 final HttpServletRequest request = (HttpServletRequest) req; 41 final HttpServletResponse response = (HttpServletResponse) resp; 42 final ByteArrayOutputStream bout = new ByteArrayOutputStream(); 43 final PrintWriter pw = new PrintWriter(new OutputStreamWriter(bout,"UTF-8")); 44 45 chain.doFilter(request, getHttpServletResponseProxy(response, bout, pw)); 46 pw.close(); 47 //拿到目標資源的輸出 48 byte result[] = bout.toByteArray(); 49 System.out.println("原始大小:" + result.length); 50 51 ByteArrayOutputStream bout2 = new ByteArrayOutputStream(); 52 GZIPOutputStream gout = new GZIPOutputStream(bout2); 53 gout.write(result); 54 gout.close(); 55 56 //拿到目標資源輸出的壓縮資料 57 byte gzip[] = bout2.toByteArray(); 58 System.out.println("壓縮大小:" + gzip.length); 59 60 response.setHeader("content-encoding", "gzip"); 61 response.setContentLength(gzip.length); 62 response.getOutputStream().write(gzip); 63 } 64 65 /** 66 * @Method: getHttpServletResponseProxy 67 * @Description: 獲取HttpServletResponse物件的代理物件 68 * @Anthor:孤傲蒼狼 69 * 70 * @param response 71 * @param bout 72 * @param pw 73 * @return HttpServletResponse物件的代理物件 74 */ 75 private ServletResponse getHttpServletResponseProxy( 76 final HttpServletResponse response, 77 final ByteArrayOutputStream bout, 78 final PrintWriter pw) { 79 80 return (ServletResponse) Proxy.newProxyInstance(GzipFilter.class.getClassLoader(), 81 response.getClass().getInterfaces(), 82 new InvocationHandler(){ 83 @Override 84 public Object invoke(Object proxy, Method method, Object[] args) 85 throws Throwable { 86 if(method.getName().equals("getWriter")){ 87 return pw; 88 }else if(method.getName().equals("getOutputStream")){ 89 return new MyServletOutputStream(bout); 90 }else{ 91 return method.invoke(response, args); 92 } 93 } 94 }); 95 } 96 97 @Override 98 public void destroy() { 99 100 } 101 102 class MyServletOutputStream extends ServletOutputStream{ 103 104 private ByteArrayOutputStream bout = null; 105 public MyServletOutputStream(ByteArrayOutputStream bout){ 106 this.bout = bout; 107 } 108 @Override 109 public void write(int b) throws IOException { 110 bout.write(b); 111 } 112 113 } 114 }
在web.xml中註冊上述的GzipFilter
1 <filter> 2 <description>配置壓縮過濾器</description> 3 <filter-name>GzipFilter</filter-name> 4 <filter-class>me.gacl.web.filter.GzipFilter</filter-class> 5 </filter> 6 7 <!--jsp檔案的輸出的內容都經過壓縮過濾器壓縮後才輸出 --> 8 <filter-mapping> 9 <filter-name>GzipFilter</filter-name> 10 <url-pattern>*.jsp</url-pattern> 11 <!-- 配置過濾器的攔截方式--> 12 <!-- 對於在Servlet中通過 13 request.getRequestDispatcher("jsp頁面路徑").forward(request, response) 14 方式訪問的Jsp頁面的要進行攔截 --> 15 <dispatcher>FORWARD</dispatcher> 16 <!--對於直接以URL方式訪問的jsp頁面進行攔截,過濾器的攔截方式預設就是REQUEST--> 17 <dispatcher>REQUEST</dispatcher> 18 </filter-mapping> 19 <!--js檔案的輸出的內容都經過壓縮過濾器壓縮後才輸出 --> 20 <filter-mapping> 21 <filter-name>GzipFilter</filter-name> 22 <url-pattern>*.js</url-pattern> 23 </filter-mapping> 24 <!--css檔案的輸出的內容都經過壓縮過濾器壓縮後才輸出 --> 25 <filter-mapping> 26 <filter-name>GzipFilter</filter-name> 27 <url-pattern>*.css</url-pattern> 28 </filter-mapping> 29 <!--html檔案的輸出的內容都經過壓縮過濾器壓縮後才輸出 --> 30 <filter-mapping> 31 <filter-name>GzipFilter</filter-name> 32 <url-pattern>*.html</url-pattern> 33 </filter-mapping>
GzipFilter過濾器會將*.jsp,*.js,*.css,*.html這些檔案裡面的文字內容都經過壓縮後再輸出到客戶端顯示。