001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.dbcp2.cpdsadapter; 019 020import java.io.PrintWriter; 021import java.io.Serializable; 022import java.sql.DriverManager; 023import java.sql.SQLException; 024import java.sql.SQLFeatureNotSupportedException; 025import java.util.Hashtable; 026import java.util.Properties; 027import java.util.logging.Logger; 028 029import javax.naming.Context; 030import javax.naming.Name; 031import javax.naming.NamingException; 032import javax.naming.RefAddr; 033import javax.naming.Reference; 034import javax.naming.Referenceable; 035import javax.naming.StringRefAddr; 036import javax.naming.spi.ObjectFactory; 037import javax.sql.ConnectionPoolDataSource; 038import javax.sql.PooledConnection; 039 040import org.apache.commons.dbcp2.PoolablePreparedStatement; 041import org.apache.commons.pool2.KeyedObjectPool; 042import org.apache.commons.pool2.impl.BaseObjectPoolConfig; 043import org.apache.commons.pool2.impl.GenericKeyedObjectPool; 044import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig; 045 046/** 047 * <p> 048 * An adapter for jdbc drivers that do not include an implementation 049 * of {@link javax.sql.ConnectionPoolDataSource}, but still include a 050 * {@link java.sql.DriverManager} implementation. 051 * <code>ConnectionPoolDataSource</code>s are not used within general 052 * applications. They are used by <code>DataSource</code> implementations 053 * that pool <code>Connection</code>s, such as 054 * {@link org.apache.commons.dbcp2.datasources.SharedPoolDataSource}. A J2EE 055 * container will normally provide some method of initializing the 056 * <code>ConnectionPoolDataSource</code> whose attributes are presented 057 * as bean getters/setters and then deploying it via JNDI. It is then 058 * available as a source of physical connections to the database, when 059 * the pooling <code>DataSource</code> needs to create a new 060 * physical connection. 061 * </p> 062 * 063 * <p> 064 * Although normally used within a JNDI environment, the DriverAdapterCPDS 065 * can be instantiated and initialized as any bean and then attached 066 * directly to a pooling <code>DataSource</code>. 067 * <code>Jdbc2PoolDataSource</code> can use the 068 * <code>ConnectionPoolDataSource</code> with or without the use of JNDI. 069 * </p> 070 * 071 * <p> 072 * The DriverAdapterCPDS also provides <code>PreparedStatement</code> pooling 073 * which is not generally available in jbdc2 074 * <code>ConnectionPoolDataSource</code> implementation, but is 075 * addressed within the jdbc3 specification. The <code>PreparedStatement</code> 076 * pool in DriverAdapterCPDS has been in the dbcp package for some time, but 077 * it has not undergone extensive testing in the configuration used here. 078 * It should be considered experimental and can be toggled with the 079 * poolPreparedStatements attribute. 080 * </p> 081 * 082 * <p> 083 * The <a href="package-summary.html">package documentation</a> contains an 084 * example using catalina and JNDI. The <a 085 * href="../datasources/package-summary.html">datasources package documentation</a> 086 * shows how to use <code>DriverAdapterCPDS</code> as a source for 087 * <code>Jdbc2PoolDataSource</code> without the use of JNDI. 088 * </p> 089 * 090 * @author John D. McNally 091 * @version $Id: DriverAdapterCPDS.java 1658644 2015-02-10 08:59:07Z tn $ 092 * @since 2.0 093 */ 094public class DriverAdapterCPDS 095 implements ConnectionPoolDataSource, Referenceable, Serializable, 096 ObjectFactory { 097 098 private static final long serialVersionUID = -4820523787212147844L; 099 100 101 private static final String GET_CONNECTION_CALLED 102 = "A PooledConnection was already requested from this source, " 103 + "further initialization is not allowed."; 104 105 /** Description */ 106 private String description; 107 /** Password */ 108 private String password; 109 /** Url name */ 110 private String url; 111 /** User name */ 112 private String user; 113 /** Driver class name */ 114 private String driver; 115 116 /** Login TimeOut in seconds */ 117 private int loginTimeout; 118 /** Log stream. NOT USED */ 119 private transient PrintWriter logWriter = null; 120 121 // PreparedStatement pool properties 122 private boolean poolPreparedStatements; 123 private int maxIdle = 10; 124 private long _timeBetweenEvictionRunsMillis = 125 BaseObjectPoolConfig.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS; 126 private int _numTestsPerEvictionRun = -1; 127 private int _minEvictableIdleTimeMillis = -1; 128 private int _maxPreparedStatements = -1; 129 130 /** Whether or not getConnection has been called */ 131 private volatile boolean getConnectionCalled = false; 132 133 /** Connection properties passed to JDBC Driver */ 134 private Properties connectionProperties = null; 135 136 static { 137 // Attempt to prevent deadlocks - see DBCP - 272 138 DriverManager.getDrivers(); 139 } 140 141 /** 142 * Controls access to the underlying connection 143 */ 144 private boolean accessToUnderlyingConnectionAllowed = false; 145 146 /** 147 * Default no-arg constructor for Serialization 148 */ 149 public DriverAdapterCPDS() { 150 } 151 152 /** 153 * Attempt to establish a database connection using the default 154 * user and password. 155 */ 156 @Override 157 public PooledConnection getPooledConnection() throws SQLException { 158 return getPooledConnection(getUser(), getPassword()); 159 } 160 161 /** 162 * Attempt to establish a database connection. 163 * @param username name to be used for the connection 164 * @param pass password to be used fur the connection 165 */ 166 @Override 167 public PooledConnection getPooledConnection(String username, String pass) 168 throws SQLException { 169 getConnectionCalled = true; 170 PooledConnectionImpl pci = null; 171 // Workaround for buggy WebLogic 5.1 classloader - ignore the 172 // exception upon first invocation. 173 try { 174 if (connectionProperties != null) { 175 connectionProperties.put("user", username); 176 connectionProperties.put("password", pass); 177 pci = new PooledConnectionImpl(DriverManager.getConnection( 178 getUrl(), connectionProperties)); 179 } else { 180 pci = new PooledConnectionImpl(DriverManager.getConnection( 181 getUrl(), username, pass)); 182 } 183 pci.setAccessToUnderlyingConnectionAllowed(isAccessToUnderlyingConnectionAllowed()); 184 } 185 catch (ClassCircularityError e) 186 { 187 if (connectionProperties != null) { 188 pci = new PooledConnectionImpl(DriverManager.getConnection( 189 getUrl(), connectionProperties)); 190 } else { 191 pci = new PooledConnectionImpl(DriverManager.getConnection( 192 getUrl(), username, pass)); 193 } 194 pci.setAccessToUnderlyingConnectionAllowed(isAccessToUnderlyingConnectionAllowed()); 195 } 196 KeyedObjectPool<PStmtKeyCPDS, PoolablePreparedStatement<PStmtKeyCPDS>> stmtPool = null; 197 if (isPoolPreparedStatements()) { 198 GenericKeyedObjectPoolConfig config = new GenericKeyedObjectPoolConfig(); 199 config.setMaxTotalPerKey(Integer.MAX_VALUE); 200 config.setBlockWhenExhausted(false); 201 config.setMaxWaitMillis(0); 202 config.setMaxIdlePerKey(getMaxIdle()); 203 if (getMaxPreparedStatements() <= 0) 204 { 205 // since there is no limit, create a prepared statement pool with an eviction thread 206 // evictor settings are the same as the connection pool settings. 207 config.setTimeBetweenEvictionRunsMillis(getTimeBetweenEvictionRunsMillis()); 208 config.setNumTestsPerEvictionRun(getNumTestsPerEvictionRun()); 209 config.setMinEvictableIdleTimeMillis(getMinEvictableIdleTimeMillis()); 210 } 211 else 212 { 213 // since there is limit, create a prepared statement pool without an eviction thread 214 // pool has LRU functionality so when the limit is reached, 15% of the pool is cleared. 215 // see org.apache.commons.pool2.impl.GenericKeyedObjectPool.clearOldest method 216 config.setMaxTotal(getMaxPreparedStatements()); 217 config.setTimeBetweenEvictionRunsMillis(-1); 218 config.setNumTestsPerEvictionRun(0); 219 config.setMinEvictableIdleTimeMillis(0); 220 } 221 stmtPool = new GenericKeyedObjectPool<>(pci, config); 222 pci.setStatementPool(stmtPool); 223 } 224 return pci; 225 } 226 227 @Override 228 public Logger getParentLogger() throws SQLFeatureNotSupportedException { 229 throw new SQLFeatureNotSupportedException(); 230 } 231 232 // ---------------------------------------------------------------------- 233 // Referenceable implementation 234 235 /** 236 * <CODE>Referenceable</CODE> implementation. 237 */ 238 @Override 239 public Reference getReference() throws NamingException { 240 // this class implements its own factory 241 String factory = getClass().getName(); 242 243 Reference ref = new Reference(getClass().getName(), factory, null); 244 245 ref.add(new StringRefAddr("description", getDescription())); 246 ref.add(new StringRefAddr("driver", getDriver())); 247 ref.add(new StringRefAddr("loginTimeout", 248 String.valueOf(getLoginTimeout()))); 249 ref.add(new StringRefAddr("password", getPassword())); 250 ref.add(new StringRefAddr("user", getUser())); 251 ref.add(new StringRefAddr("url", getUrl())); 252 253 ref.add(new StringRefAddr("poolPreparedStatements", 254 String.valueOf(isPoolPreparedStatements()))); 255 ref.add(new StringRefAddr("maxIdle", 256 String.valueOf(getMaxIdle()))); 257 ref.add(new StringRefAddr("timeBetweenEvictionRunsMillis", 258 String.valueOf(getTimeBetweenEvictionRunsMillis()))); 259 ref.add(new StringRefAddr("numTestsPerEvictionRun", 260 String.valueOf(getNumTestsPerEvictionRun()))); 261 ref.add(new StringRefAddr("minEvictableIdleTimeMillis", 262 String.valueOf(getMinEvictableIdleTimeMillis()))); 263 ref.add(new StringRefAddr("maxPreparedStatements", 264 String.valueOf(getMaxPreparedStatements()))); 265 266 return ref; 267 } 268 269 270 // ---------------------------------------------------------------------- 271 // ObjectFactory implementation 272 273 /** 274 * implements ObjectFactory to create an instance of this class 275 */ 276 @Override 277 public Object getObjectInstance(Object refObj, Name name, 278 Context context, Hashtable<?,?> env) 279 throws Exception { 280 // The spec says to return null if we can't create an instance 281 // of the reference 282 DriverAdapterCPDS cpds = null; 283 if (refObj instanceof Reference) { 284 Reference ref = (Reference)refObj; 285 if (ref.getClassName().equals(getClass().getName())) { 286 RefAddr ra = ref.get("description"); 287 if (ra != null && ra.getContent() != null) { 288 setDescription(ra.getContent().toString()); 289 } 290 291 ra = ref.get("driver"); 292 if (ra != null && ra.getContent() != null) { 293 setDriver(ra.getContent().toString()); 294 } 295 ra = ref.get("url"); 296 if (ra != null && ra.getContent() != null) { 297 setUrl(ra.getContent().toString()); 298 } 299 ra = ref.get("user"); 300 if (ra != null && ra.getContent() != null) { 301 setUser(ra.getContent().toString()); 302 } 303 ra = ref.get("password"); 304 if (ra != null && ra.getContent() != null) { 305 setPassword(ra.getContent().toString()); 306 } 307 308 ra = ref.get("poolPreparedStatements"); 309 if (ra != null && ra.getContent() != null) { 310 setPoolPreparedStatements(Boolean.valueOf( 311 ra.getContent().toString()).booleanValue()); 312 } 313 ra = ref.get("maxIdle"); 314 if (ra != null && ra.getContent() != null) { 315 setMaxIdle(Integer.parseInt(ra.getContent().toString())); 316 } 317 318 ra = ref.get("timeBetweenEvictionRunsMillis"); 319 if (ra != null && ra.getContent() != null) { 320 setTimeBetweenEvictionRunsMillis( 321 Integer.parseInt(ra.getContent().toString())); 322 } 323 324 ra = ref.get("numTestsPerEvictionRun"); 325 if (ra != null && ra.getContent() != null) { 326 setNumTestsPerEvictionRun( 327 Integer.parseInt(ra.getContent().toString())); 328 } 329 330 ra = ref.get("minEvictableIdleTimeMillis"); 331 if (ra != null && ra.getContent() != null) { 332 setMinEvictableIdleTimeMillis( 333 Integer.parseInt(ra.getContent().toString())); 334 } 335 ra = ref.get("maxPreparedStatements"); 336 if (ra != null && ra.getContent() != null) { 337 setMaxPreparedStatements( 338 Integer.parseInt(ra.getContent().toString())); 339 } 340 341 ra = ref.get("accessToUnderlyingConnectionAllowed"); 342 if (ra != null && ra.getContent() != null) { 343 setAccessToUnderlyingConnectionAllowed( 344 Boolean.valueOf(ra.getContent().toString()).booleanValue()); 345 } 346 347 cpds = this; 348 } 349 } 350 return cpds; 351 } 352 353 /** 354 * Throws an IllegalStateException, if a PooledConnection has already 355 * been requested. 356 */ 357 private void assertInitializationAllowed() throws IllegalStateException { 358 if (getConnectionCalled) { 359 throw new IllegalStateException(GET_CONNECTION_CALLED); 360 } 361 } 362 363 // ---------------------------------------------------------------------- 364 // Properties 365 366 /** 367 * Get the connection properties passed to the JDBC driver. 368 * 369 * @return the JDBC connection properties used when creating connections. 370 */ 371 public Properties getConnectionProperties() { 372 return connectionProperties; 373 } 374 375 /** 376 * <p>Set the connection properties passed to the JDBC driver.</p> 377 * 378 * <p>If <code>props</code> contains "user" and/or "password" 379 * properties, the corresponding instance properties are set. If these 380 * properties are not present, they are filled in using 381 * {@link #getUser()}, {@link #getPassword()} when {@link #getPooledConnection()} 382 * is called, or using the actual parameters to the method call when 383 * {@link #getPooledConnection(String, String)} is called. Calls to 384 * {@link #setUser(String)} or {@link #setPassword(String)} overwrite the values 385 * of these properties if <code>connectionProperties</code> is not null.</p> 386 * 387 * @param props Connection properties to use when creating new connections. 388 * @throws IllegalStateException if {@link #getPooledConnection()} has been called 389 */ 390 public void setConnectionProperties(Properties props) { 391 assertInitializationAllowed(); 392 connectionProperties = props; 393 if (connectionProperties.containsKey("user")) { 394 setUser(connectionProperties.getProperty("user")); 395 } 396 if (connectionProperties.containsKey("password")) { 397 setPassword(connectionProperties.getProperty("password")); 398 } 399 } 400 401 /** 402 * Get the value of description. This property is here for use by 403 * the code which will deploy this datasource. It is not used 404 * internally. 405 * 406 * @return value of description, may be null. 407 * @see #setDescription(String) 408 */ 409 public String getDescription() { 410 return description; 411 } 412 413 /** 414 * Set the value of description. This property is here for use by 415 * the code which will deploy this datasource. It is not used 416 * internally. 417 * 418 * @param v Value to assign to description. 419 */ 420 public void setDescription(String v) { 421 this.description = v; 422 } 423 424 /** 425 * Get the value of password for the default user. 426 * @return value of password. 427 */ 428 public String getPassword() { 429 return password; 430 } 431 432 /** 433 * Set the value of password for the default user. 434 * @param v Value to assign to password. 435 * @throws IllegalStateException if {@link #getPooledConnection()} has been called 436 */ 437 public void setPassword(String v) { 438 assertInitializationAllowed(); 439 this.password = v; 440 if (connectionProperties != null) { 441 connectionProperties.setProperty("password", v); 442 } 443 } 444 445 /** 446 * Get the value of url used to locate the database for this datasource. 447 * @return value of url. 448 */ 449 public String getUrl() { 450 return url; 451 } 452 453 /** 454 * Set the value of url used to locate the database for this datasource. 455 * @param v Value to assign to url. 456 * @throws IllegalStateException if {@link #getPooledConnection()} has been called 457 */ 458 public void setUrl(String v) { 459 assertInitializationAllowed(); 460 this.url = v; 461 } 462 463 /** 464 * Get the value of default user (login or username). 465 * @return value of user. 466 */ 467 public String getUser() { 468 return user; 469 } 470 471 /** 472 * Set the value of default user (login or username). 473 * @param v Value to assign to user. 474 * @throws IllegalStateException if {@link #getPooledConnection()} has been called 475 */ 476 public void setUser(String v) { 477 assertInitializationAllowed(); 478 this.user = v; 479 if (connectionProperties != null) { 480 connectionProperties.setProperty("user", v); 481 } 482 } 483 484 /** 485 * Get the driver classname. 486 * @return value of driver. 487 */ 488 public String getDriver() { 489 return driver; 490 } 491 492 /** 493 * Set the driver classname. Setting the driver classname cause the 494 * driver to be registered with the DriverManager. 495 * @param v Value to assign to driver. 496 * @throws IllegalStateException if {@link #getPooledConnection()} has been called 497 */ 498 public void setDriver(String v) throws ClassNotFoundException { 499 assertInitializationAllowed(); 500 this.driver = v; 501 // make sure driver is registered 502 Class.forName(v); 503 } 504 505 /** 506 * Gets the maximum time in seconds that this data source can wait 507 * while attempting to connect to a database. NOT USED. 508 */ 509 @Override 510 public int getLoginTimeout() { 511 return loginTimeout; 512 } 513 514 /** 515 * Get the log writer for this data source. NOT USED. 516 */ 517 @Override 518 public PrintWriter getLogWriter() { 519 return logWriter; 520 } 521 522 /** 523 * Sets the maximum time in seconds that this data source will wait 524 * while attempting to connect to a database. NOT USED. 525 */ 526 @Override 527 public void setLoginTimeout(int seconds) { 528 loginTimeout = seconds; 529 } 530 531 /** 532 * Set the log writer for this data source. NOT USED. 533 */ 534 @Override 535 public void setLogWriter(PrintWriter out) { 536 logWriter = out; 537 } 538 539 540 // ------------------------------------------------------------------ 541 // PreparedStatement pool properties 542 543 544 /** 545 * Flag to toggle the pooling of <code>PreparedStatement</code>s 546 * @return value of poolPreparedStatements. 547 */ 548 public boolean isPoolPreparedStatements() { 549 return poolPreparedStatements; 550 } 551 552 /** 553 * Flag to toggle the pooling of <code>PreparedStatement</code>s 554 * @param v true to pool statements. 555 * @throws IllegalStateException if {@link #getPooledConnection()} has been called 556 */ 557 public void setPoolPreparedStatements(boolean v) { 558 assertInitializationAllowed(); 559 this.poolPreparedStatements = v; 560 } 561 562 /** 563 * The maximum number of statements that can remain idle in the 564 * pool, without extra ones being released, or negative for no limit. 565 * @return the value of maxIdle 566 */ 567 public int getMaxIdle() { 568 return this.maxIdle; 569 } 570 571 /** 572 * The maximum number of statements that can remain idle in the 573 * pool, without extra ones being released, or negative for no limit. 574 * 575 * @param maxIdle The maximum number of statements that can remain idle 576 * @throws IllegalStateException if {@link #getPooledConnection()} has been called 577 */ 578 public void setMaxIdle(int maxIdle) { 579 assertInitializationAllowed(); 580 this.maxIdle = maxIdle; 581 } 582 583 /** 584 * Returns the number of milliseconds to sleep between runs of the 585 * idle object evictor thread. 586 * When non-positive, no idle object evictor thread will be 587 * run. 588 * @return the value of the evictor thread timer 589 * @see #setTimeBetweenEvictionRunsMillis(long) 590 */ 591 public long getTimeBetweenEvictionRunsMillis() { 592 return _timeBetweenEvictionRunsMillis; 593 } 594 595 /** 596 * Sets the number of milliseconds to sleep between runs of the 597 * idle object evictor thread. 598 * When non-positive, no idle object evictor thread will be 599 * run. 600 * @param timeBetweenEvictionRunsMillis 601 * @see #getTimeBetweenEvictionRunsMillis() 602 * @throws IllegalStateException if {@link #getPooledConnection()} has been called 603 */ 604 public void setTimeBetweenEvictionRunsMillis( 605 long timeBetweenEvictionRunsMillis) { 606 assertInitializationAllowed(); 607 _timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis; 608 } 609 610 /** 611 * Returns the number of statements to examine during each run of the 612 * idle object evictor thread (if any). 613 * 614 * *see #setNumTestsPerEvictionRun 615 * *see #setTimeBetweenEvictionRunsMillis 616 */ 617 public int getNumTestsPerEvictionRun() { 618 return _numTestsPerEvictionRun; 619 } 620 621 /** 622 * Sets the number of statements to examine during each run of the 623 * idle object evictor thread (if any). 624 * <p> 625 * When a negative value is supplied, <tt>ceil({*link #numIdle})/abs({*link #getNumTestsPerEvictionRun})</tt> 626 * tests will be run. I.e., when the value is <i>-n</i>, roughly one <i>n</i>th of the 627 * idle objects will be tested per run. 628 * 629 * @param numTestsPerEvictionRun number of statements to examine per run 630 * @see #getNumTestsPerEvictionRun() 631 * @see #setTimeBetweenEvictionRunsMillis(long) 632 * @throws IllegalStateException if {@link #getPooledConnection()} has been called 633 */ 634 public void setNumTestsPerEvictionRun(int numTestsPerEvictionRun) { 635 assertInitializationAllowed(); 636 _numTestsPerEvictionRun = numTestsPerEvictionRun; 637 } 638 639 /** 640 * Returns the minimum amount of time a statement may sit idle in the pool 641 * before it is eligible for eviction by the idle object evictor 642 * (if any). 643 * 644 * *see #setMinEvictableIdleTimeMillis 645 * *see #setTimeBetweenEvictionRunsMillis 646 */ 647 public int getMinEvictableIdleTimeMillis() { 648 return _minEvictableIdleTimeMillis; 649 } 650 651 /** 652 * Sets the minimum amount of time a statement may sit idle in the pool 653 * before it is eligible for eviction by the idle object evictor 654 * (if any). 655 * When non-positive, no objects will be evicted from the pool 656 * due to idle time alone. 657 * @param minEvictableIdleTimeMillis minimum time to set (in ms) 658 * @see #getMinEvictableIdleTimeMillis() 659 * @see #setTimeBetweenEvictionRunsMillis(long) 660 * @throws IllegalStateException if {@link #getPooledConnection()} has been called 661 */ 662 public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) { 663 assertInitializationAllowed(); 664 _minEvictableIdleTimeMillis = minEvictableIdleTimeMillis; 665 } 666 667 /** 668 * Returns the value of the accessToUnderlyingConnectionAllowed property. 669 * 670 * @return true if access to the underlying is allowed, false otherwise. 671 */ 672 public synchronized boolean isAccessToUnderlyingConnectionAllowed() { 673 return this.accessToUnderlyingConnectionAllowed; 674 } 675 676 /** 677 * Sets the value of the accessToUnderlyingConnectionAllowed property. 678 * It controls if the PoolGuard allows access to the underlying connection. 679 * (Default: false) 680 * 681 * @param allow Access to the underlying connection is granted when true. 682 */ 683 public synchronized void setAccessToUnderlyingConnectionAllowed(boolean allow) { 684 this.accessToUnderlyingConnectionAllowed = allow; 685 } 686 687 /** 688 * Returns the maximum number of prepared statements. 689 * 690 * @return maxPrepartedStatements value 691 */ 692 public int getMaxPreparedStatements() 693 { 694 return _maxPreparedStatements; 695 } 696 697 /** 698 * Sets the maximum number of prepared statements. 699 * @param maxPreparedStatements the new maximum number of prepared 700 * statements 701 */ 702 public void setMaxPreparedStatements(int maxPreparedStatements) 703 { 704 _maxPreparedStatements = maxPreparedStatements; 705 } 706}