赞
踩
在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。
例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。
定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。模板方法模式是一种基于继承的代码复用技术,它是一种类行为型模式。
模板方法(Template Method)模式包含以下主要角色:
负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
定义了算法的骨架,按某种顺序调用其包含的基本方法。
是实现算法各个步骤的方法,是模板方法的组成部分。
基本方法又可以分为三种:
一个抽象方法由抽象类声明、由其具体子类实现。
b)具体方法(Concrete Method)
一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。
实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
抽象类:com.zengqingfa.designpattern.template.common.AbstractTemplate
package com.zengqingfa.designpattern.template.common; public abstract class AbstractTemplate { /** * 模板方法:final修饰,防止重写 */ public final void templateMethod() { //调用子类的方法 abstractMethod(); //父类实现的方法 commonMethod(); } /** * 抽象方法:由子类去实现 */ protected abstract void abstractMethod(); /** * 基本方法(抽象类已经实现,子类拥有的共同代码) */ private final void commonMethod() { //业务相关的代码 System.out.println("模板类中的方法:AbstractTemplate#commonMethod"); } }
实现类:
public class ConcreteTemplate1 extends AbstractTemplate { @Override protected void abstractMethod() { System.out.println("子类ConcreteTemplate1实现了abstractMethod方法"); } } public class ConcreteTemplate2 extends AbstractTemplate { @Override protected void abstractMethod() { System.out.println("子类ConcreteTemplate2实现了abstractMethod方法"); } }
测试:
public class Client { public static void main(String[] args) { AbstractTemplate template1 = new ConcreteTemplate1(); template1.templateMethod(); AbstractTemplate template2 = new ConcreteTemplate2(); template2.templateMethod(); //匿名内部类的形式 AbstractTemplate template3 = new AbstractTemplate() { @Override protected void abstractMethod() { System.out.println("template3 abstractMethod"); } }; template3.templateMethod(); } }
结果:
子类ConcreteTemplate1实现了abstractMethod方法
模板类中的方法:AbstractTemplate#commonMethod
子类ConcreteTemplate2实现了abstractMethod方法
模板类中的方法:AbstractTemplate#commonMethod
template3 abstractMethod
模板类中的方法:AbstractTemplate#commonMethod
抽象类:com.zengqingfa.designpattern.template.hook.AbstractTemplate
package com.zengqingfa.designpattern.template.hook; /** * * @fileName: AbstractTemplate * @author: zengqf3 * @date: 2021-4-9 15:10 * @description: */ public abstract class AbstractTemplate { /** * 模板方法:final修饰,防止重写 */ public final void templateMethod() { //调用子类的方法 abstractMethod(); if (hook()) { doHookMethod(); } //父类实现的方法 commonMethod(); } /** * 抽象方法:由子类去实现 */ protected abstract void abstractMethod(); /** * 基本方法(抽象类已经实现,子类拥有的共同代码) */ private final void commonMethod() { //业务相关的代码 System.out.println("模板类中的方法:AbstractTemplate#commonMethod"); } /** * 钩子方法(空方法):子类根据实际情况是否重写, * 默认钩子方法 *一个钩子方法常常由抽象类给出一个空实现作为此方法的默认实现。这种空的钩子方法叫做“Do Nothing Hook”。 *显然,这种默认钩子方法在缺省适配模式里面已经见过了,一个缺省适配模式讲的是一个类为一个接口提供一个默认的空实现, *从而使得缺省适配类的子类不必像实现接口那样必须给出所有方法的实现,因为通常一个具体类并不需要所有的方法。 * * 命名规则 * 钩子方法的名字应当以do开始,这是熟悉设计模式的Java开发人员的标准做法。在上面的例子中, * 钩子方法hookMethod()应当以do开头;在HttpServlet类中,也遵从这一命名规则,如doGet()、doPost()等方法。 */ protected void doHookMethod() { } /** * 默认返回true * @return */ protected boolean hook() { return false; } }
实现类:
public class ConcreteTemplate1 extends AbstractTemplate { @Override protected void abstractMethod() { System.out.println("子类ConcreteTemplate1实现了abstractMethod方法"); } /** * 钩子方法重写 */ @Override protected void doHookMethod() { System.out.println("执行ConcreteTemplate1的钩子方法doHookMethod"); } @Override protected boolean hook() { return true; } } public class ConcreteTemplate2 extends AbstractTemplate { @Override protected void abstractMethod() { System.out.println("子类ConcreteTemplate2实现了abstractMethod方法"); } }
测试:
public class Client {
public static void main(String[] args) {
AbstractTemplate template1 = new ConcreteTemplate1();
template1.templateMethod();
System.out.println("====================");
AbstractTemplate template2 = new ConcreteTemplate2();
template2.templateMethod();
}
}
结果:
子类ConcreteTemplate1实现了abstractMethod方法
执行ConcreteTemplate1的钩子方法doHookMethod
模板类中的方法:AbstractTemplate#commonMethod
====================
子类ConcreteTemplate2实现了abstractMethod方法
模板类中的方法:AbstractTemplate#commonMethod
回调接口:
public interface ICallBack {
void callBack();
}
模板类:
class Template { /** * 模板方法:final修饰,防止重写 */ public final void templateMethod(ICallBack callBack) { callBack.callBack(); //父类实现的方法 commonMethod(); } /** * 基本方法(抽象类已经实现,子类拥有的共同代码) */ private final void commonMethod() { //业务相关的代码 System.out.println("模板类中的方法:AbstractTemplate#commonMethod"); } }
回调接口实现:
public class ConcreteCallBack1 implements ICallBack { @Override public void callBack() { System.out.println("子类ConcreteTemplate1实现了callBack方法"); } } public class ConcreteCallBack2 implements ICallBack { @Override public void callBack() { System.out.println("子类ConcreteTemplate2实现了callBack方法"); } }
测试:
public class Client { public static void main(String[] args) { Template template = new Template(); template.templateMethod(new ConcreteCallBack1()); System.out.println("====================="); template.templateMethod(new ConcreteCallBack2()); System.out.println("====================="); //匿名内部类 template.templateMethod(new ICallBack() { @Override public void callBack() { System.out.println("匿名内部类实现了方法"); } }); }
结果:
子类ConcreteTemplate1实现了callBack方法
模板类中的方法:AbstractTemplate#commonMethod
=====================
子类ConcreteTemplate2实现了callBack方法
模板类中的方法:AbstractTemplate#commonMethod
=====================
匿名内部类实现了方法
模板类中的方法:AbstractTemplate#commonMethod
需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,此时,可结合桥接模式来进行设计。
Servlet(Server Applet)是Java Servlet的简称,用Java编写的服务器端程序,主要功能在于交互式地浏览和修改数据,生成动态Web内容。在每一个 Servlet 都必须要实现 Servlet 接口,GenericServlet 是个通用的、不特定于任何协议的Servlet,它实现了 Servlet 接口,而 HttpServlet 继承于 GenericServlet,实现了 Servlet 接口,为 Servlet 接口提供了处理HTTP协议的通用实现,所以我们定义的 Servlet 只需要继承 HttpServlet 即可。
javax.servlet.http.HttpServlet
public abstract class HttpServlet extends GenericServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // ... } protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // ... } protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // ... } protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // ... } protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // ... } protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // ... } protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // ... } protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); if (method.equals(METHOD_GET)) { long lastModified = getLastModified(req); if (lastModified == -1) { // servlet doesn't support if-modified-since, no reason // to go through further expensive logic doGet(req, resp); } else { long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE); if (ifModifiedSince < lastModified) { // If the servlet mod time is later, call doGet() // Round down to the nearest second for a proper compare // A ifModifiedSince of -1 will always be less maybeSetLastModified(resp, lastModified); doGet(req, resp); } else { resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } } } else if (method.equals(METHOD_HEAD)) { long lastModified = getLastModified(req); maybeSetLastModified(resp, lastModified); doHead(req, resp); } else if (method.equals(METHOD_POST)) { doPost(req, resp); } else if (method.equals(METHOD_PUT)) { doPut(req, resp); } else if (method.equals(METHOD_DELETE)) { doDelete(req, resp); } else if (method.equals(METHOD_OPTIONS)) { doOptions(req,resp); } else if (method.equals(METHOD_TRACE)) { doTrace(req,resp); } else { // // Note that this means NO servlet supports whatever // method was requested, anywhere on this server. // String errMsg = lStrings.getString("http.method_not_implemented"); Object[] errArgs = new Object[1]; errArgs[0] = method; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg); } } // ...省略... }
在 HttpServlet 的 service 方法中,首先获得到请求的方法名,然后根据方法名调用对应的 doXXX 方法,比如说请求方法为GET,那么就去调用 doGet 方法;请求方法为POST,那么就去调用 doPost 方法
HttpServlet 相当于定义了一套处理 HTTP 请求的模板;service 方法为模板方法,定义了处理HTTP请求的基本流程;doXXX 等方法为基本方法,根据请求方法做相应的处理,子类可重写这些方法;HttpServletRequest 中的Method则起到钩子方法的作用.
在开发javaWeb应用时,自定义的Servlet类一般都扩展 HttpServlet 类,譬如我们实现一个输出 Hello World! 的 Servlet 如下
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; // 扩展 HttpServlet 类 public class HelloWorld extends HttpServlet { public void init() throws ServletException { // ... } public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<h1>Hello World!</h1>"); } public void destroy() { // ... } }
Executor 是 Mybatis 的核心接口之一,其中定义了数据库操作的基本方法,该接口的代码如下:
public interface Executor { ResultHandler NO_RESULT_HANDLER = null; int update(MappedStatement ms, Object parameter) throws SQLException; <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException; <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException; List<BatchResult> flushStatements() throws SQLException; void commit(boolean required) throws SQLException; void rollback(boolean required) throws SQLException; CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql); boolean isCached(MappedStatement ms, CacheKey key); void clearLocalCache(); void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType); Transaction getTransaction(); void close(boolean forceRollback); boolean isClosed(); void setExecutorWrapper(Executor executor); }
uml图如下:
BaseExecutor 中主要提供了缓存管理和事务管理的基本功能,继承 BaseExecutor 的子类只需要实现四个基本方法来完成数据库的相关操作即可,这四个方法分别是:doUpdate() 方法、doQuery() 方法、doQueryCursor() 方法、doFlushStatement() 方法,其余功能都在 BaseExecutor 中实现。
BaseExecutor的部分代码如下,其中的 query() 方法首先会创建 CacheKey 对象,并根据 CacheKey 对象查找一级缓存,如果缓存命中则返回缓存中记录的结果对象,如果未命中则查询数据库得到结果集,之后将结果集映射成结果对象并保存到一级缓存中,同时返回结果对象。
public abstract class BaseExecutor implements Executor { // .... @Override public int update(MappedStatement ms, Object parameter) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } clearLocalCache(); return doUpdate(ms, parameter); } @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } @SuppressWarnings("unchecked") @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { queryStack++; list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; } @Override public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); return doQueryCursor(ms, parameter, rowBounds, boundSql); } @Override public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (closed) { throw new ExecutorException("Executor was closed."); } CacheKey cacheKey = new CacheKey(); cacheKey.update(ms.getId()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); cacheKey.update(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); // mimic DefaultParameterHandler logic for (ParameterMapping parameterMapping : parameterMappings) { if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } cacheKey.update(value); } } if (configuration.getEnvironment() != null) { // issue #176 cacheKey.update(configuration.getEnvironment().getId()); } return cacheKey; } @Override public boolean isCached(MappedStatement ms, CacheKey key) { return localCache.getObject(key) != null; } @Override public void commit(boolean required) throws SQLException { if (closed) { throw new ExecutorException("Cannot commit, transaction is already closed"); } clearLocalCache(); flushStatements(); if (required) { transaction.commit(); } } @Override public void rollback(boolean required) throws SQLException { if (!closed) { try { clearLocalCache(); flushStatements(true); } finally { if (required) { transaction.rollback(); } } } } @Override public void clearLocalCache() { if (!closed) { localCache.clear(); localOutputParameterCache.clear(); } } protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException; protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException; protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException; protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException; // .... }
BaseExecutor 的子类有四个分别是 SimpleExecotor、ReuseExecutor、BatchExecutor、ClosedExecutor,由于这里使用了模板方法模式,一级缓存等固定不变的操作都封装到了 BaseExecutor 中,因此子类就不必再关心一级缓存等操作,只需要专注实现4个基本方法的实现即可。
这里对这四个子类的功能做一个简要的介绍:
Spring 提供了很多 Template 类,比如,JdbcTemplate、RedisTemplate、RestTemplate。尽管都叫作 xxxTemplate,但它们并非基于模板模式来实现的,而是基于回调来实现的,确切地说应该是同步回调。而同步回调从应用场景上很像模板模式,所以,在命名上,这些类使用 Template(模板)这个单词作为后缀。
正常情况下使用原生jdbc查询数据库:
package com.zengqingfa.designpattern.template; import java.sql.*; public class JdbcDemo { public User queryUser(long id) { Connection conn = null; Statement stmt = null; try { //1.加载驱动 Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/template", "root", "root"); //2.创建statement类对象,用来执行SQL语句 stmt = conn.createStatement(); //3.ResultSet类,用来存放获取的结果集 String sql = "select * from user where id=" + id; ResultSet resultSet = stmt.executeQuery(sql); String eid = null, ename = null, price = null; while (resultSet.next()) { User user = new User(); user.setId(resultSet.getLong("id")); user.setName(resultSet.getString("name")); user.setTelephone(resultSet.getString("telephone")); return user; } } catch (ClassNotFoundException e) { // TODO: log... } catch (SQLException e) { // TODO: log... } finally { if (conn != null) { try { conn.close(); } catch (SQLException e) { // TODO: log... } } if (stmt != null) { try { stmt.close(); } catch (SQLException e) { // TODO: log... } } } return null; } }
queryUser() 函数包含很多流程性质的代码,跟业务无关,比如,加载驱动、创建数据库连接、创建 statement、关闭连接、关闭 statement、处理异常。针对不同的 SQL 执行请求,这些流程性质的代码是相同的、可以复用的
针对这个问题,Spring 提供了 JdbcTemplate,对 JDBC 进一步封装,来简化数据库编程。使用 JdbcTemplate 查询用户信息,我们只需要编写跟这个业务有关的代码,其中包括,查询用户的 SQL 语句、查询结果与 User 对象之间的映射关系。其他流程性质的代码都封装在了 JdbcTemplate 类中,不需要我们每次都重新编写。
package com.zengqingfa.designpattern.template; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import java.sql.ResultSet; import java.sql.SQLException; public class JdbcTemplateDemo { private JdbcTemplate jdbcTemplate; public User queryUser(long id) { String sql = "select * from user where id=" + id; return jdbcTemplate.query(sql, new UserRowMapper()).get(0); } class UserRowMapper implements RowMapper<User> { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setId(rs.getLong("id")); user.setName(rs.getString("name")); user.setTelephone(rs.getString("telephone")); return user; } } }
JdbcTemplate 通过回调的机制,将不变的执行流程抽离出来,放到模板方法 execute() 中,将可变的部分设计成回调 StatementCallback,由用户来定制。query() 函数是对 execute() 函数的二次封装,让接口用起来更加方便。
org.springframework.jdbc.core.JdbcTemplate
@Override public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException { return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper))); } @Override @Nullable public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException { Assert.notNull(sql, "SQL must not be null"); Assert.notNull(rse, "ResultSetExtractor must not be null"); if (logger.isDebugEnabled()) { logger.debug("Executing SQL query [" + sql + "]"); } /** * Callback to execute the query. */ class QueryStatementCallback implements StatementCallback<T>, SqlProvider { @Override @Nullable public T doInStatement(Statement stmt) throws SQLException { ResultSet rs = null; try { rs = stmt.executeQuery(sql); return rse.extractData(rs); } finally { JdbcUtils.closeResultSet(rs); } } @Override public String getSql() { return sql; } } return execute(new QueryStatementCallback()); } @Override @Nullable 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()); } } @Override @Nullable 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()); } }
lback 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()); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。