动态代理在WEB与JDBC开发中的应用
WEB案例
目前有一个2005年开始,基于Struts1的Web项目A,其验证部分依赖于主站的SSO(单点登录)。在请求站点A的时候,用户会被强制带去做SSO验证,通过身份验证后后,主站会自动地把请求转发至A站点,并在request header中添加了用于保存登录用户ID的新属性SM_USER,然后A站点根据用户ID提供相应的服务。由于该项目是一个既存项目,所以其中残余大量像下面一样的测试代码。
- String user_id = request.getHeader("XX_USER");
- if (user_id == null) {
- user_id = "my_hard_coded_user_id";
- }
- UserProfile userProfile = new BizDao().getUserProfile(user_id);
其根本原因是在本地测试的时候无法于生产环境的SSO对接,只有把代码提交至公共的DEV服务器、UAT服务器或PROD服务器后,才能享受SSO为A站所提供的XX_USER数据,所以程序员在无法取得header数据的情况下,直接简单粗暴地对本地环境进行了硬编码处理。在只有几个人的小团队,这样的处理可能看起来也无所谓,但长年下来经手的人数也相当可观了,很多人都习惯使用自己的ID做测试,所以在SVN的历史版本中,硬编码的ID从A改到B、改到C、改到D……
问题分析
每个人都根据自己的喜好选择了使用自己的ID或是他人的ID,那么是否有一种办法可以统一接口一劳永逸呢?可能最容易想到的就是HttpServletRequest.setHeader,可惜HttpServletRequest并没有这样的API,为啥?个人猜测可能是因为request源于客户端,服务器端应该保持请求的原始性、纯净性和无毒性吧,而HttpServletResponse(http://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletResponse.html)则是在应用程序的控制之下,所以程序员可以对其任意践踏,setHeader、addHeader以及getHeader、getHeaders、getHeaderNames。既然没有API可用,直接进行包装代理吧,在Google的帮助下我们可以找到现成的方案来对request中的header进行重新定制,重点是HttpServletRequestWrapper类的实现。
http://vangjee.wordpress.com/2009/02/25/how-to-modify-request-headers-in-a-j2ee-web-application/
其实这个方案是在我实现动态代理方案之后找到的,两者思路几乎是一样的,都是对原始request进行包装代理,重新实现getHeader方法。
解决方案
不废话,直接上代码!
- private static class RequestInvocationHandler implements InvocationHandler {
- private HttpServletRequest wrappedRequest;
- public RequestInvocationHandler(HttpServletRequest r) {
- wrappedRequest = r;
- }
- public static String dummyData = "my_hard_coded_id";
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- if ("getHeader".equals(method.getName()) && args.length == 1 && "SM_USER".equals(args[0])) {
- return dummyData;
- }
- return method.invoke(wrappedRequest, args);
- }
- public static HttpServletRequest createRequestWapper(HttpServletRequest r) {
- if (null != r.getParameter("u")) {
- dummyData = r.getParameter("u");
- }
- return (HttpServletRequest)(Proxy.newProxyInstance(HttpServletRequest.class.getClassLoader(),
- new Class[] {HttpServletRequest.class},
- new RequestInvocationHandler(r)));
- }
- }
上面代码实现了对Request对象的代理,所有针对Request对象的调用都需经过invoke方法。在invoke方法中,我们可以针对不同的方法签名进行更为细粒度的控制。比如在WEB案例中提到的问题,可以专门针对getHeader方法进行重新定制,本来Request Header中并无该数据,但我们可以硬生生地“造”出数据。另外除了可以“造”出测试数据外,还可以通过请求中携带的参数“u”动态地进行数据修改,这样就实现了用户切换的功能。
既然代理类的问题解决了,下面该谈谈应在何时何地植入代理对象了。何时何地可以理解为切入时机,其实最初的是在org.apache.struts.action.RequestProcessor中植入代理对象的,但是在使用过程中发现该实现有一个弊端,那就是在非Struts请求时无法使用代理对象,比如直接访问JSP文件或是其它Servlet所提供的服务路径。这时自然而然地想到Filter,而使用Filter也是一痛,加入一个全新的Filter吧,有点破坏的整体设计的感觉,而放入其它Filter之中的话,看起来又不伦不类。但好歹是测试用,这两个方案可以折衷选择其一,具体实现如下。
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
- request = RequestInvocationHandler.createRequestWapper((HttpServletRequest)request);
- chain.doFilter(request, response);
- }
整体工作流程如下,用户首先发出请求,然后request对象在filter中被替换成代理对象,替换后的request被传入doFilter方法,之后不论是Servlet、Struts还是JSP,它们用到的都是我们定制的Request对象。
- +---------+ +---------+
- | | | |
- Request | | +-----> | Servlet |
- --------> | Filter | | |
- | +----+ | | Struts |
- | Request | | |
- Response | is | | |
- lt;--------+ | wrapped | | |
- | here | <-----+ | |
- | | | |
- +---------+ +---------+
后记
当然,如果想要修改request对象不止这一种方法,比如正统的方式是定义一个HttpServletRequestWrapper来重新定义request,具体案例请参考http://vangjee.wordpress.com/2009/02/25/how-to-modify-request-headers-in-a-j2ee-web-application/
JDBC案例
背景描述
我们先看一下项目所面临问题以及期望解决方案。在作者所接触的这个项目中,直接使用原始JDBC技术,Java.sql.PreparedStatement和java.sql.ResultSet几乎占领了数据访问层,没有半点OR Mapping的迹象,看起来是不是很悲催?命啊~在项目开发至一半的时候,突然发现要对日文字符进行支持,而在之前一直使用英文进行测试。好了,问题来了,一个编码为8859_1的数据库怎么来接收Java中的UTF-8呢?并且很多CRUD功能已经完成,每个实现中涉及的字段数目相当之大,难道针对每个字段逐一调整不成?
问题分析
针对上面描述的情况,我们先做一个简单的分析。目前所面临的问题可以归为两种类型,第一类就是乱码的问题,这个可以通过重新编码得到解决;第二类则是面临大量的代码实现,如果对所有内容进行调整的话,很多功能就得重新测试,成本较高,但确实可行。
针对乱码问题,从实现的角度考虑,可以分别针对输入数据和输出数据进行编码转换。是在使用PreparedStatement的时候,对所有调用setString的地方进行编码转换;而对于数据读取的情况,可以对ResultSet.getString所返回的结果字符串进行编码转换。下面以UTF-8转8859_1为例子,实现编码转换(逆向转换只需交换编码名称的位置即可):
- new String(new String("abc").getByte("UTF-8"), "8859_1")
如果以这种方式对每处PreparedStatement.setString和ResultSet.getString的调用都进行编码转换操作的话,工作量不仅繁重,而且不利于代码的维护与移植。从API上看,其实我们可以针对setString和getString进行“HOOK”,使用代理模式比较适合,动态代理最为适宜。
解决方案
编码问题解决了,利用动态代理统一地对setString和getString接口进行自制,让所有的调用都在我们的控制之下还何愁不能统一江山!
- public static ResultSet createResultSetJPWrapper(ResultSet rs) {
- return (ResultSet)(Proxy.newProxyInstance(ResultSet.class.getClassLoader(),
- new Class[] {ResultSet.class},
- new ResultSetJPWrapper(rs)));
- }
- private static class ResultSetJPWrapper implements InvocationHandler {
- private ResultSet wrappedResultSet;
- public ResultSetJPWrapper(ResultSet rs) {
- wrappedResultSet = rs;
- }
- public Object invoke(Object proxy, Method method, Object[] args)
- throws Throwable {
- Object result = method.invoke(wrappedResultSet, args);
- if ("getString".equals(method.getName()) && null != result) {
- result = CommonUtils.convertCharset((String)result, ISAConstants.CHARSET_8859_1, ISAConstants.CHARSET_UTF_8);
- }
- return result;
- }
- }
- public static PreparedStatement createPreparedStatementJPWrapper(PreparedStatement pstmt) {
- return (PreparedStatement)(Proxy.newProxyInstance(PreparedStatement.class.getClassLoader(),
- new Class[] {PreparedStatement.class},
- new PreparedStatementJPWrapper(pstmt)));
- }
- private static class PreparedStatementJPWrapper implements InvocationHandler {
- private PreparedStatement wrappedStatement;
- public PreparedStatementJPWrapper(PreparedStatement pstmt) {
- wrappedStatement = pstmt;
- }
- public Object invoke(Object proxy, Method method, Object[] args)
- throws Throwable {
- if ("setString".equals(method.getName()) && args.length == 2 && null != args[1] && String.class.equals(args[1].getClass())) {
- args[1] = CommonUtils.convertCharset((String)args[1], ISAConstants.CHARSET_UTF_8, ISAConstants.CHARSET_8859_1);
- }
- return method.invoke(wrappedStatement, args);
- }
- }
接下来就是在需要使用PreparedStatement和ResultSet的地方进行重构。
- // 重构前
- PreparedStatement stmt = conn.prepareStatement(sql);
- // 重构后
- PreparedStatement stmt = DBUtils.createPreparedStatementJPWrapper(conn.prepareStatement(sql));
- // 重构前
- ResultSet rs = stmt.executeQuery();
- // 重构后
- ResultSet rs = DBUtils.createResultSetJPWrapper(stmt.executeQuery());
重构后不论是stmt还是rs,setString和getString都会被我们定义的动态代理所劫持,在数据输入前或数据输出后重新编码,而这一切对于开发人员来说一切都是透明的,并且不会对现有逻辑造成污染。
后记
这里谈论的只是动态代理应用案例,并不意味着必须使用动态代理是该案例中解决问题的唯一方案。其实动态代理在这里不一定是最佳的实现方案,如果可以的话最好对数据库进行重新设置修改默认编码,或者是在应用层统一编码,但这些往往都不适用于一个经过十多年、几十人、且风格各异且“烂”的发臭的项目了。其实实际的数据库VARCHAR类型中存放的字符串编码五花八门,EUC-JP/SHIFTJIS/UTF-8/8859_1,并且这些编码可能同时存在于一张表,要了命了!
动态代理在WEB与JDBC开发中的应用的更多相关文章
- Spring AOP动态代理实现,解决Spring Boot中无法正常启用JDK动态代理的问题
Spring AOP底层的动态代理实现有两种方式:一种是JDK动态代理,另一种是CGLib动态代理. JDK动态代理 JDK 1.3版本以后提供了动态代理,允许开发者在运行期创建接口的代理实例,而且只 ...
- Web(Jsp+ Servlet)开发中如何解决中文乱码问题
1.中文乱码的成因 编码的字符集和解码的字符集不一致. 2.web开发过程中可能出现的乱码的位置及解决方案 ①request乱码 在向服务器传递数据时,所传递的中文有可能出现乱码. post请求(协议 ...
- JAVAEE——Mybatis第一天:入门、jdbc存在的问题、架构介绍、入门程序、Dao的开发方法、接口的动态代理方式、SqlMapConfig.xml文件说明
1. 学习计划 第一天: 1.Mybatis的介绍 2.Mybatis的入门 a) 使用jdbc操作数据库存在的问题 b) Mybatis的架构 c) Mybatis的入门程序 3.Dao的开发方法 ...
- java中Proxy(代理与动态代理)
转自: https://blog.csdn.net/pangqiandou/article/details/52964066 一.代理的概念 动态代理技术是整个java技术中最重要的一个技术,它是学习 ...
- java中静态代理,动态代理知识的补充
文章转载自:http://blog.csdn.net/jialinqiang/article/details/8950989 一.Java动态代理 相对于静态代理的代理类在编译时生成(.class文件 ...
- (转)轻松学,Java 中的代理模式及动态代理
背景:讲到反射机制,肯定会想到动态代理. 轻松学,Java 中的代理模式及动态代理 代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强.值得注意的是,代理类和被代理类应该 ...
- 【Mybatis】-- Mapper动态代理开发注意事项
1.1. Mapper动态代理方式 1.1.1. 开发规范 Mapper接口开发方法只需要程序员编写Mapper接口(相当于Dao接口),由Mybatis框架根据接口定义创建接口的动态代理对象,代理对 ...
- Web前端开发中的MCRV模式(转)
作者: izujian 来源: baiduux 摘要:针对前端开发中基于ajax的复杂页面开发所面临的代码规模大,难以组织和维护,代码复用性.扩展性和适应性差等问题,本文尝试以MVC思想为 基础,结 ...
- Mybatis框架基础入门(三)--Mapper动态代理方式开发
使用MyBatis开发Dao,通常有两个方法,即原始Dao开发方法和Mapper动态代理开发方法. 原始Dao开发方法需要程序员编写Dao接口和Dao实现类,此方式开发Dao,存在以下问题: Dao方 ...
随机推荐
- 运行shell脚本报错 '\357\273\277': command not found 解决的方法
1,删除BOM,在vi以下运行以下的命令就可以 :set nobomb 2,原因: 所谓BOM,全称是Byte Order Mark.它是一个Unicode字符,通常出如今文本的开头,用来标识字节序( ...
- 基于OAS设计可扩展OpenAPI
前言 随着互联网行业的兴起,开发模式已逐步转换为微服务自治:小团队开发微服务,然后通过Restful接口相互调用.开发者们越来越渴望能够使用一种“官话”进行流畅的沟通,甚至实现多种编程语言系统的自动化 ...
- 【bzoj1798】[Ahoi2009]Seq 维护序列seq
大意:一个数组,三个操作,第一种是区间[a,b]每个数乘乘,第二种是区间[a,b]每个数加c,第三种是查询[a,b]区间的和并对p取摸. 两种操作就不能简单的只往下传标记.每次传乘法标记时,要把加法标 ...
- python generator iterator和iterable object
1 iterable object list.dict.set.tuple.file(在每行上iterate)等都是iterable object,但是它们不是iterator.但是它们可以转换成it ...
- centos7 tengine2.1.2 编译安装 防火墙设置
安装 pcre 和 openssl yum -y install pcre pcre-devel yum -y install openssl openssl-devel wget http://te ...
- [RK3288][Android6.0] Display驱动初始化流程小结【转】
本文转载自:http://blog.csdn.net/kris_fei/article/details/52584903 Platform: RK3288OS: Android 6.0Kernel: ...
- expandableListview的默认箭头箭头怎样移到右边
1 . ExpandableListView布局:<ExpandableListView android:id="@+id/bbs_category_expandable_lis ...
- P2465 [SDOI2008]山贼集团 dp
这个题是一道树形dp+状压dp二合一,先预处理每种组合会有什么额外的费用,然后在树上dp就行了. 题干: 题目描述 某山贼集团在绿荫村拥有强大的势力,整个绿荫村由N个连通的小村落组成,并且保证对于每两 ...
- mysql将查询结果导出csv文件的方法into outfile
例句: select * from table_name into outfile '/tmp/tmp.csv' fields terminated by ','; 详解: ① into outf ...
- luogu3942将军令
https://www.zybuluo.com/ysner/note/1302132 题面 在大小为\(n\)的树上选择尽量少的点,使得所有未选择的点距离选择了的点小于等于\(k\). \(n\leq ...