На текущем проекте появилась необходимость получить список неиспользуемых классов с целью избавиться от них :)

Так как классов много и связи между ними не всегда можно отследить в IDE – сейчас редкий проект обходится без использования Spring, но мне, в очередной раз, повезло. У нас испольуется самописаное решение, которое пришлось немного доработать и научить делать dependency-injection, чтобы развязать классы между собой и начать по-тихоньку избавляться от синглтонов, которые только портят жизнь.

Для оценки покрытия кода тестами у нас используется Cobertura. Именно по этому я решил использовать именно ее для сбора статистики использования классов – мне для этого пришлось просто добавить сборку инструментированного jar-файла, который так же должен попадать в итоговую сборку.

Собственно сам сбор статистики заключается в том, чтобы инструментированные классы использовались приложением во время работы. Все в точности как и при оценке покрытия за тем лишь исключением, что мы гоняем не тесты, а реально работающее приложение на одном из тестовых стендов.

Ниже представлен пример Ant-скрипта, который инструментирует классы:

<?xml version="1.0" encoding="UTF-8"?>
<project name="UsageStatistics" basedir=".">
	<property name="src.main.java" location="${basedir}/src/main/java" />
	<property name="src.test.java" location="${basedir}/src/test/java" />

	<property name="lib" location="${basedir}/lib" />

	<property name="target" location="${basedir}/target" />	

	<property name="target.compile" location="${target}/compile" />
	<property name="target.compile.classes" location="${target.compile}/classes" />

	<property name="target.instrument" location="${target}/instrument" />
	<property name="target.instrument.classes" location="${target.instrument}/classes" />
	<property name="target.instrument.report" location="${target.instrument}/report" />

	<property name="target.test" location="${target}/test" />
	<property name="target.test.classes" location="${target.test}/classes" />
	<property name="target.test.report" location="${target.test}/report" />

	<property name="target.package" location="${target}/package" />

	<target name="compile">
		<mkdir dir="${target.compile.classes}" />
		<javac srcdir="${src.main.java}" destdir="${target.compile.classes}" source="1.5" debug="on" debuglevel="lines,source">
			<classpath>
				<fileset dir="${lib}" includes="**/*.jar" />
			</classpath>
		</javac>
	</target>

	<target name="instrument" depends="compile">
		<mkdir dir="${target.instrument.classes}" />
		<copy todir="${target.instrument.classes}">
			<fileset dir="${target.compile.classes}" includes="**/*"/>
		</copy>

		<cobertura-instrument todir="${target.instrument.classes}" datafile="${target.instrument}/cobertura.ser">
		    <fileset dir="${target.instrument.classes}" includes="**/*.class"/>
		</cobertura-instrument>

		<copy file="${target.cobertura}/cobertura.ser" tofile="${target.cobertura}/cobertura-clean.ser"/>
	</target>

	<target name="test-compile" depends="instrument">
		<mkdir dir="${target.test.classes}" />
		<javac srcdir="${src.test.java}" destdir="${target.test.classes}" source="1.5" debug="on" debuglevel="lines,source">
			<classpath>
				<fileset dir="${target.instrument.classes}" includes="**/*" />
				<fileset dir="${lib}" includes="**/*.jar" />
			</classpath>
		</javac>
	</target>

	<target name="test" description="Run tests" depends="compile-test" unless="test.skip">
		<mkdir dir="${target.test}" />
		<mkdir dir="${target.test.report}" />

		<junit printsummary="true" fork="yes" forkmode="once">
			<classpath>
				<pathelement location="${target.instrument.classes}"/>
				<pathelement location="${target.test.classes}"/>
				<fileset dir="${lib}" includes="**/*.jar" />
			</classpath>

			<sysproperty key="net.sourceforge.cobertura.datafile" file="${target.instrument}/cobertura.ser" />

			<batchtest todir="${target.test.report}">
				<fileset dir="${target.compile.test}">
					<include name="**/*Test.class"/>
				</fileset>
				<formatter type="xml"/>
			</batchtest>
		</junit>

		<cobertura-report datafile="${target.instrument}/cobertura.ser" format="html" destdir="${target.instrument.report}" srcdir="${src.main.java}" />
	</target>

	<target name="package" depends="test">
		<mkdir dir="${target.package}" />
		<jar jarfile="${target.package}/package.jar" basedir="${target.compile.classes}"/>
		<jar jarfile="${target.package}/package-instrumented.jar" basedir="${target.instrument.classes}"/>
	</target>
</project>

Очень важное замечание: во время инструментации Cobertura генерирует файл, в который записывается информация о обо всех существеющих классах , его же и нужно использовать во время сбора статистики. В противном случае Cobertura создаст новый файл, в которои будет информация только о тех классах, которые использовались приложением и пользы от него будет 0. Поэтому не забывайте копировать этот файл на сервер, где будет работать ваше приложение.

Для того, чтобы Cobertura на сервере нашла этот файл приложению нужно каким-то образом передать путь к файлу. Есть несколько способов это сделать:

- положить файл cobertura.properties в classpath приложения и в нем прописать net.sourceforge.cobertura.datafile=/путь/к/файлу

- педедать параметр JVM -Dnet.sourceforge.cobertura.datafile=/путь/к/файлу

Если ваше приложение состоит из нескольких Java-процессов, они могут безопасно использовать один и тот же файл для сбора статистики.

Ниже приведены скриншоты отчетов статистики и покрытия для сравнения. Сейчас уже разница не такая большая, так как часть пакетов я уже удалил :)

Отчет статистики использования классов
Usage Statistics

Покрытие кода тестами
Coverage Report

Сравнивая отчеты можно заметить, что package23 используется, хотя тестами не покрыт, package22 не используется, но при этом покрыт тестами, пакеты package16, package24, package25 – не используются.