统计tomcat启动次数

突然心血来潮想统计tomcat的启动次数,不要问为什么,纯粹是想玩。 最开始的设计是执行bat文件的时候,在doStart里面调用jar来记录次数,比如:
QQ图片20170727204446.png

统计方法非常简单:

public class Counter {

	public static void main(String[] args) throws IOException {
		String location = args[0];
		if (location == null) {
			location = "count.json";
		}
		File file = new File(location);
		if (!file.exists()) {
			try {
				file.createNewFile();
			} catch (Exception e) {
				System.exit(-1);
			}
		}
		try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
			try (FileLock lock = raf.getChannel().lock()) {
				Gson gson = new GsonBuilder().setPrettyPrinting().create();
				byte[] countJsonBits = new byte[(int) raf.length()];
				raf.readFully(countJsonBits);
				String countJson = new String(countJsonBits);
				Count count = gson.fromJson(countJson, Count.class);
				if (count == null) {
					// 第一次生成文件
					count = new Count();
				}
				// 获取最后一次的时间记录
				TimeCount last = count.getLast();
				SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
				long now = System.currentTimeMillis();
				String ymd = sdf.format(new Date(now));
				if (last == null) {
					// 如果沒有最後一條(第一次生成文件)
					count.add(new TimeCount(1, now, ymd));
				} else {
					// 查看当前日期是否和最后一条记录的日期相匹配
					if (sdf.format(new Date(last.getMill())).equals(ymd)) {
						last.setCount(last.getCount() + 1);
					} else {
						//如果当前時間比最後一次時間大,说明已经过了一天
						if (now > last.getMill()) {
							count.add(new TimeCount(1, now, ymd));
						} else {
							// 如果不匹配,遍历
							// 查看是否存在和当前日期匹配的timecount
							boolean exists = false;
							for (TimeCount tc : count.getCounts()) {
								if (sdf.format(new Date(tc.getMill())).equals(ymd)) {
									// 如果存在,次数+1
									tc.setCount(tc.getCount() + 1);
									exists = true;
									break;
								}
							}
							// 如果不存在,创建一个新的TimeCount
							if (!exists) {
								count.add(new TimeCount(1, now, ymd));
							}
						}
					}
				}
				// 总次数+1
				count.setTotal(count.getTotal() + 1);
				// 按照时间从大到小排序
				count.sort();
				// 写入文件
				raf.seek(0);
				raf.write(gson.toJson(count).getBytes());
			}
		}
	}

}

其中Count、TimeCount分别是总体统计和按天统计:

package me.qyh.tomcatcounter;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Count implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private int total;
	private List<TimeCount> counts = new ArrayList<>();

	public int getTotal() {
		return total;
	}

	public void setTotal(int total) {
		this.total = total;
	}

	public List<TimeCount> getCounts() {
		return counts;
	}

	public void setCounts(List<TimeCount> counts) {
		this.counts = counts;
	}

	public void sort() {
		Collections.sort(counts);
	}

	public TimeCount getLast() {
		return counts.isEmpty() ? null : counts.get(0);
	}

	public void add(TimeCount count) {
		this.counts.add(count);
	}

}
package me.qyh.tomcatcounter;

import java.io.Serializable;

public class TimeCount implements Serializable, Comparable<TimeCount> {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private int count;
	private long mill;
	private String ymd;
	public TimeCount(){
		super();
	}

	public TimeCount(int count, long mill, String ymd) {
		super();
		this.count = count;
		this.mill = mill;
		this.ymd = ymd;
	}

	public int getCount() {
		return count;
	}

	public void setCount(int count) {
		this.count = count;
	}

	public long getMill() {
		return mill;
	}

	public void setMill(long mill) {
		this.mill = mill;
	}

	public String getYmd() {
		return ymd;
	}

	public void setYmd(String ymd) {
		this.ymd = ymd;
	}

	@Override
	public int compareTo(TimeCount o) {
		return -Long.compare(mill, o.mill);
	}

}

但是在eclipse里面却出现了问题。。。因为eclipse根本不是靠bat来启动tomcat的,研究了一下后发现它是通过tomcat\bin下面的bootstrap.jar(好熟悉的名字)来启动的,一个最简单也是最笨的方法就是下载了tomcat的 源码之后重新制作一个bootstrap.jar并且覆盖它。 其实制作的过程倒也很顺利(需要引入tomcat\lib下面的所有包,然后根据原来的bootstrap jar中的类找对应的源码,复制到工程中),只是一个JndiPermission文件实在是找不到。。。

打开Bootstrap类,对start()方法做如下修改:

/**
	 * Start the Catalina daemon.
	 */
	public void start() throws Exception {
		if (catalinaDaemon == null)
			init();

		Method method = catalinaDaemon.getClass().getMethod("start", (Class[]) null);
		try{
			method.invoke(catalinaDaemon, (Object[]) null);
		}finally {
			try {

				Properties pros = new Properties();
				pros.load(Bootstrap.class.getResourceAsStream("count.properties"));
				String location = pros.getProperty("location");
				File file = new File(location);

				if (!file.exists()) {
					try {
						file.createNewFile();
					} catch (Exception e) {
					}
				}
				//记录次数,见Counter类
			} catch (Throwable e) {
			}
		}

	}
    

由于我用到了gson,所以也需要把Gson的源码一并拷贝到bootstrap.jar中,此时替换原来的bootstrap.jar即可正常统计。

由于 下面的方法会block线程,所以启动tomcat后不会立即有记录,而是在停止tomcat的时候才会写入统计,另外如果通过 任务管理器 eclipse直接关闭tomcat 等方式直接关闭tomcat,那么也无法统计

method.invoke(catalinaDaemon, (Object[]) null);

为了克服上述问题,更进一步,重写tomcat的catalina,但是这样有个问题,catalina跟tomcat的版本关联非常大,但是查看源码的过程中发现,catalina是可以被继承的,因此将bootstrap的init方法修改如下:

public void init() throws Exception {

		initClassLoaders();

		Thread.currentThread().setContextClassLoader(catalinaLoader);

		SecurityClassLoad.securityClassLoad(catalinaLoader);

		// Load our startup class and call its process() method
		if (log.isDebugEnabled())
			log.debug("Loading startup class");
		
		Class<?> startupClass = catalinaLoader.loadClass("me.qyh.tomcatcounter.Catalina");
		Object startupInstance = startupClass.newInstance();

		// Set the shared extensions class loader
		if (log.isDebugEnabled())
			log.debug("Setting startup class properties");
		String methodName = "setParentClassLoader";
		Class<?> paramTypes[] = new Class[1];
		paramTypes[0] = Class.forName("java.lang.ClassLoader");
		Object paramValues[] = new Object[1];
		paramValues[0] = sharedLoader;
		Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
		method.invoke(startupInstance, paramValues);

		catalinaDaemon = startupInstance;

	}

同时将start方法还原

init方法将tomcat的catalina替换为了自己的Catalina,此时需要重写它的两个方法:

@Override
	public void start() {
		super.start();
		if (!await) {
			try {
				logCount();
			} catch (Throwable e) {
			}
		}
	}

	@Override
	public void await() {
		try {
			logCount();
		} catch (Throwable e) {
		}
		getServer().await();
	}

最后将自己的catalina打包成jar(比如counter.jar),丢到tomcat的lib下面,那么启动tomcat之后就会增加统计信息了

最最重要的一点,千万记得备份、千万不要用于生产环境!!!

其他注意事项:
bootstrap.jar是取用的tomcat8.5源码编译的,但是在tomcat7.0.67中也能跑。。。
至少需要jre7及以上
需要修改counter.jar中 me.qyh.tomcatcounter 下的count.properties来修改统计文件的存放位置,默认位置为E:/count.json

项目源码以及bootstrap.jar下载: https://pan.baidu.com/s/1jHRTyy6

tomcat覆盖.zip解压后直接丢到tomcat根目录(注意备份bootstrap.jar),count.proprties应该位于tomcat根目录,其中location指向统计文件的位置,例如c:/count, 那么最终文件位置为c:/count-(tomcat版本号).json。如果没有count.properties文件,那么会在用户目录下生成count-(tomcat版本号).properties.