001: package com.jamonapi.proxy;
002:
003: import java.lang.reflect.Method;
004: import java.util.Date;
005: import java.sql.*;
006: import java.util.*;
007:
008: import com.jamonapi.*;
009: import com.jamonapi.utils.*;
010:
011: /**
012: * Class that implements JDBC specific proxied monitoring. The following are monitored by this class 1) All SQL statements executed.
013: * For Statements argument values are replaced by '?' and for CallableStatements and PreparedStatement the original query with the '?'
014: * is used (select * from table where key=?), 2) The first keyword of the SQL command (i.e. select/update/delete/insert/create/...
015: * 3) String matches. Developers pass in these strings. This is useful for tracking table accesses, 4) SQL Details. This keeps track
016: * of the most recent N queries.
017: *
018: * In addition the standard MonProxy stats are tracked (Interface method calls, and Exception details).
019: * *
020: * */
021:
022: class JDBCMonProxy extends MonProxy {
023:
024: // The queries associated with the PreparedStatments are kept here. PreparedStatements have no way to get the query out.
025: // A weakHashMap() is used as you can't guarantee when a PreparedStatement closes, and there has to be some way of cleaning up
026: // stale objects in the map. WeakHashMaps() automatically remove any Object that has no other reference.
027: private static Map statementsMap = Collections
028: .synchronizedMap(new WeakHashMap());
029: private static Long DEFAULT_SQL_TIME = new Long(-99);// number for when the query hasn't finished executing yet.
030: private static int ARGS_SQL_STATEMENT = 0;// The sql is always the first argument to methods like executeQuery(sql,...);
031: // the following numbers correspond to this header
032: //String[] sqlHeader={"ID", "StartTime", "Executiontime", "StatementReuse", "SQL", "ExceptionStackTrace", "MethodName", };
033: private static int SQL_EXECUTION_TIME_IND = 2;
034: private static int SQL_EXCEPTION_IND = 5;
035:
036: JDBCMonProxy(Object monitoredObj, Params params) {
037: super (monitoredObj, params);
038: }
039:
040: /** Method that monitors method invocations of the proxied interface. This method is not explicitly called.
041: * The Proxy class automatically calls it.
042: *
043: *
044: */
045: public Object invoke(Object proxy, Method method, Object[] args)
046: throws Throwable {
047: BasicTimingMonitor mon = null;
048: Object[] row = null;
049: SQLDeArgMon sqlMon = null;
050: // These values need to be checked at beginning and end of routine, so need to ensure they don't change during this method call
051: // hence the local versions of them.
052: boolean isSQLSummaryEnabled = params.isSQLSummaryEnabled
053: && params.isEnabled;
054: boolean isSQLDetailEnabled = params.isSQLDetailEnabled
055: && params.isEnabled;
056: boolean executingQuery = isExecuteQueryMethod(method.getName()); // determine if a query is being executed.
057:
058: // Because this monitor string is created every time I use a StringBuffer as it should be more effecient.
059: // I didn't do this in the exception part of the code as that shouldn't be called as often.
060: try {
061:
062: // If enabled and the method that is being called executes a query then monitor the sql executed. Note that 'Statement' 'execute'
063: // methods will have sql passed to them, and and PreparedStatement and CallableStatements will not.
064: // When needed PrepatedStatement/CallableStatements will get their sql from the WeakHashMap from when the PreparedStatement was created.
065: if (executingQuery
066: && (isSQLSummaryEnabled || isSQLDetailEnabled)) {
067: String actualSQL = null;
068: // Start timing the query and add a row to the recently executed query buffer
069: mon = new BasicTimingMonitor();
070: mon.start();
071:
072: int statementReuseCounter = 0;//createStatement will always have a value of 0 as they can't be reused. PreparedStaement is tracked
073:
074: if (isStatementObject(args)) {// a query associated with a Statement is being executed.
075: actualSQL = getSQL(args[ARGS_SQL_STATEMENT]);//get the sql being executed
076: sqlMon = new SQLDeArgMon("Statement", actualSQL,
077: params.matchStrings);// parses sql and puts ? marks associated with args, and sets up monitors.
078:
079: } else { // a query associated with a PreparedStatement/CallableStatement is being executed
080: sqlMon = (SQLDeArgMon) statementsMap
081: .get(getMonitoredObject()); // get existing object associated with this PreparedStatement/CallableStatement
082: statementReuseCounter = sqlMon.incrementCounter();// increment the reuse counter.
083: if (isSQLSummaryEnabled)
084: MonitorFactory.add(
085: "MonProxy-SQL-PreparedStatement Reuse",
086: "count", 2 * statementReuseCounter);// to get the average to be accurate multiply by 2
087:
088: actualSQL = getSQL(sqlMon.getSQL());// in the case of a preparedStatement the actual sql includes question marks.
089:
090: }
091:
092: // create a monitor for: select * from table where name=?
093: if (isSQLSummaryEnabled)
094: sqlMon.start();
095:
096: if (isSQLDetailEnabled) {
097: row = new Object[] { new Long(++params.sqlID),
098: new Date(), DEFAULT_SQL_TIME,
099: new Integer(statementReuseCounter),
100: actualSQL, "", method.toString(), };
101: params.sqlBuffer.addRow(row);
102: }
103: } // end if enabled
104:
105: // Invoke the underlying method
106: Object returnValue = super .invoke(proxy, method, args);
107:
108: // If sql monitoring is not enabled or if the proxy is already there then don't wrap a proxy. For example Statements can
109: // return the underlying Connection which will probably already be Monitored. Note for jdbc if the Object returns another jdbc Object
110: // type that should be monitored it will also be monitored.
111: if (!(params.isEnabled) || returnValue instanceof MonProxy)
112: return returnValue;
113: else if ((isSQLSummaryEnabled || isSQLDetailEnabled)
114: && returnsPreparedStatement(method.getName())
115: && isPreparedStatement(returnValue)) { // not sure if the returnsPreparedStatement is needed or not
116: // Associate sql such as 'select * from table where name=?' with the PreparedStatement in the WeakHashMap so when later executeQuery() is
117: // issued against the PreparedStatement we can count stats of the preparedStatements reuses. PreparedStatements have no way of getting out
118: // what sql is associated. WeakHashMap will not leave the memory hanging when the PreparedStatement goes out of scope.
119: String actualSQL = getSQL(args[ARGS_SQL_STATEMENT]);
120: statementsMap.put(returnValue, new SQLDeArgMon(
121: "PreparedStatement", actualSQL,
122: params.matchStrings));
123: return MonProxyFactory
124: .monitor((PreparedStatement) returnValue);
125: } else if ((isSQLSummaryEnabled || isSQLDetailEnabled)
126: && (monitorResultSet(returnValue) || monitorOtherJDBC(returnValue)))
127: return monitorJDBC(returnValue);
128: else
129: return returnValue;
130:
131: } catch (Throwable e) {
132:
133: // If there is a stack trace it will be part of the SQL details row. Note a user reported a null pointer exception being thrown,
134: // so i added the 'row!=null' check.
135: if (executingQuery && isSQLDetailEnabled && row != null)
136: row[SQL_EXCEPTION_IND] = Misc.getExceptionTrace(e);
137:
138: throw e;
139: } finally {
140:
141: // mon != null means either or both of the sql detail monitoring or sql summary monitoring is enabled.
142: if (mon != null && executingQuery) {
143: long executionTime = mon.stop();
144:
145: if (isSQLDetailEnabled && row != null)
146: row[SQL_EXECUTION_TIME_IND] = new Long(
147: executionTime);
148:
149: if (isSQLSummaryEnabled)
150: sqlMon.add(executionTime).stop();
151: }
152:
153: }
154: }
155:
156: private boolean isExecuteQueryMethod(String methodName) {
157: return "executeQuery".equals(methodName)
158: || "executeUpdate".equals(methodName)
159: || "execute".equals(methodName);
160:
161: }
162:
163: private boolean returnsPreparedStatement(String methodName) {
164: return "prepareStatement".equals(methodName)
165: || "prepareCall".equals(methodName);
166: }
167:
168: private boolean isStatementObject(Object[] args) {
169: return (args != null && args.length >= 1);
170: }
171:
172: private boolean monitorOtherJDBC(Object value) {
173: return (value instanceof Statement || value instanceof Connection);
174: }
175:
176: private boolean monitorResultSet(Object value) {
177: return (value instanceof ResultSet && params.isResultSetEnabled && params.isInterfaceEnabled);
178: }
179:
180: private Object monitorJDBC(Object returnValue) {
181: if (returnValue instanceof ResultSet)
182: return MonProxyFactory.monitor((ResultSet) returnValue);
183: else if (returnValue instanceof Statement)
184: return MonProxyFactory.monitor((Statement) returnValue);
185: else if (returnValue instanceof Connection)
186: return MonProxyFactory.monitor((Connection) returnValue);
187: else
188: return returnValue;
189: }
190:
191: // This will return true for CallableStatements too.
192: private boolean isPreparedStatement(Object value) {
193: return value instanceof PreparedStatement;
194: }
195:
196: private String getSQL(Object sql) {
197: return (sql == null) ? "null sql" : sql.toString();
198: }
199:
200: /**
201: * This class associates queries with the PreparedStatement and tracks accesses to it. It also removes values from a Statements sql and
202: * replaces it with '?'. For example: select * from table where key='steve', becomes select * from table where key=?
203: * @author steve souza
204: *
205: */
206: private static class SQLDeArgMon {
207:
208: private int accessCounter = 0;
209: private String sql;
210:
211: private int monRows;
212: private static final int LABEL_IND = 0;
213: private MonitorComposite monitors = null;
214: private String[][] data = null;
215:
216: //The constructor creates monitors for 1) the first keyword (i.e. select/delete/...), 2) the sql for the PreparedStatement/Statement
217: // 3) Any keyword matches passed in (such as table names)
218:
219: SQLDeArgMon(String statementType, String sql, List matchStrings) {
220:
221: SQLDeArger sqld = new SQLDeArger(sql, matchStrings);
222: this .sql = sqld.getParsedSQL();
223: data = sqld.getAll(); // contains 3 cols: summary sql dearged, detail sql, ms. (units)
224: monRows = data.length;
225:
226: for (int i = 0; i < monRows; i++) {
227: StringBuffer label = new StringBuffer("MonProxy-SQL-");
228: // note this loop matches one in SQLDeArger.getAll and positions are important
229: // The constructor must be changed if this method changes - kind of ugly...
230: // The following labels will appear in the jamon report.
231: if (i == 0) //All
232: data[i][LABEL_IND] = label.append("Type: ").append(
233: data[i][LABEL_IND]).toString();
234: else if (i == 1) // SQL Type - i.e. select, delete, update etc.
235: data[i][LABEL_IND] = label.append("Type: ").append(
236: data[i][LABEL_IND]).toString();
237: else if (i == 2) // Parsed SQL (Statement, or PreparedStatement then - select * from table where key=?
238: data[i][LABEL_IND] = label.append(statementType)
239: .append(": ").append(data[i][LABEL_IND])
240: .toString();
241: else
242: // Match - Any string matches in the sql.
243: data[i][LABEL_IND] = label.append("Match: ")
244: .append(data[i][LABEL_IND]).toString();
245:
246: }
247:
248: }
249:
250: private SQLDeArgMon start() {
251: synchronized (this ) { // keep data from being clobbered while being accessed by this method
252: if (monitors == null) {
253: // note if this is called in the constructor then monitors would be created even if sql summary is disabled
254: monitors = MonitorComposite.getMonitors(data);
255: data = null;
256: }
257: }
258:
259: monitors.start();
260: return this ;
261: }
262:
263: private SQLDeArgMon stop() {
264: monitors.stop();
265: return this ;
266: }
267:
268: private SQLDeArgMon add(double time) {
269: monitors.add(time);
270: return this ;
271: }
272:
273: private synchronized int incrementCounter() {
274: return accessCounter++;
275: }
276:
277: private String getSQL() {
278: return sql;
279: }
280:
281: public String toString() {
282: return "accessCounter=" + accessCounter + ", sql=" + sql;
283: }
284: }
285:
286: public static void main(String[] args) {
287: JDBCMonProxy.SQLDeArgMon sqlMon = new JDBCMonProxy.SQLDeArgMon(
288: "Statement", "select * from table where name='steve'",
289: null);
290: System.out.println(sqlMon);
291: }
292:
293: }
|