среда, 19 ноября 2008 г.

Генерация javabean в runtime

Если требуется создать javabean в runtime по набору полей, то никакими стандартными средствами java эту задачу решить не удастся.

По началу я решил, что с этой задачей может справиться какая-либо из реализаций org.apache.commons.beanutils.DynaBean. Однако работать с ним можно лишь так if (object instanceof DynaBean) ... . То есть, если модуль использующий этот объект работает с ним только через reflection и ничего не знает о DynaBean, то такое решение не подходит.

В итоге, было решено использовать Javassist. Очень удобный инструмент для подобного рода действий, позволяет производить как создание так и модификацию уже загруженных JVM классов. Предоставляет два вида API, на уровне исходного кода и собственно самого байткода. Использовать конечно лучше первый, т.к. мало кого устроит работать напрямую с байткодом.

В альтернативу classloader-у используется класс javassist.ClassPool, он позволяет не только загружать, но и создавать классы javassist.CtClass. При этом класс не будет доступен приложению сразу же, а лишь после преобразования в обычный класс java.lang.Class. Сам javassist.CtClass обладает богатыми возможностями для создания, удаления методов и полей, изменения наследования или даже названия уже загруженного класса. Для доступа ко всем классам загруженным JVM следует использовать ClassPool.getDefault(), хотя можно реализовать и свой, чтобы, например, загружать классы самостоятельно.

Итак, решение с использованием Javassist будет выглядеть так:

public Class<?> createClass(String className, Map<String, Class<?>> props) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass cc = classPool.makeClass(className);

for (Entry<String, Class<?>> entry : props.entrySet()) {
String name = entry.getKey();
Class<?> type = entry.getValue();
CtClass fieldType = classPool.get(type.getName());
CtField field = new CtField(fieldType, name, cc);
cc.addField(field);

String n = camelize(name);
CtMethod setter = CtNewMethod.setter("set" + n, field);
cc.addMethod(setter);
CtMethod getter = CtNewMethod.getter("get" + n, field);
cc.addMethod(getter);
}

return cc.toClass();
}

private String camelize(String s) {
return s.substring(0, 1).toUpperCase() + s.substring(1);
}

воскресенье, 16 ноября 2008 г.

Log4jdbc логгер для sql-запросов

Log4jdbc представляет собой jdbc-драйвер, который может логировать sql-вызовы используя SLF4J. Реализует как JDBC 3 так и JDBC 4 спецификации. Для настройки к вашему jdbc-url необходимо просто добавить префикс jdbc:log4. Например:

jdbc:postgresql://localhost:5432/test, заменить на jdbc:log4jdbc:postgresql://localhost:5432/test

Затем в log4j.xml или log4j.properties настроить logger на любую из категорий:

  • jdbc.sqlonly - логирование только sql запросов,

  • jdbc.sqltiming - логирование sql-запросов и их времени выполнения,

  • jdbc.audit - логирование всех jdbc-операций за исключением ResultSet-ов,

  • jdbc.resultset - самое полное логирование, включающее в себя операции со всеми ResultSet-объектами,

  • jdbc.connection - логирование операций с jdbc-соединениями

среда, 12 ноября 2008 г.

Отключение кэширования html-страницы. Часть 2

Использование meta-тэгов, описываемых в первой части помогает нам избавиться от кэширования в браузере. Однако, помимо кэша браузера существует кэш прокси-сервера, который проводит анализ лишь HTTP-заголовков. К тому же может появится необходимость отключить кэширование не только самой html-страницы, но и ресурсов, находящихся на ней (jpg, swf ...). Данную проблему можно решить таким вот servlet-фильтром:

package ru.someone;

import java.io.IOException;
import java.util.*;
import javax.servlet.*;
import org.slf4j.*;

public class ResponseHeaderFilter implements Filter {

private final Logger log = LoggerFactory.getLogger(getClass());
private final Map httpParams = new HashMap();

public void init(FilterConfig filterConfig) throws ServletException {
Enumeration enums = filterConfig.getInitParameterNames();
while(enums.hasMoreElements()) {
final String name = enums.nextElement();
final String value = filterConfig.getInitParameter(name);
log.debug("HTTP-header registred - {}:{}", name, value);
httpParams.put(name, value);
}
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
for (Entry entry : httpParams.entrySet()) {
httpResponse.setHeader(entry.getKey(), entry.getValue());
}
chain.doFilter(request, response);
}

public void destroy() {
}

}

Этот фильтр выставляет HTTP-заголовки, объекту javax.servlet.HttpServletResponse, которые берет из конфигурации фильтра. Соответственно в самом web.xml нужно написать:

<filter>
<filter-name>responseHeaderFilter</filter-name>
<filter-class>ru.someone.ResponseHeaderFilter</filter-class>
<init-param>
<param-name>Cache-Control</param-name>
<param-value>no-store, max-age=0, must-revalidate</param-value>
</init-param>
<init-param>
<param-name>Pragma</param-name>
<param-value>no-cache</param-value>
</init-param>
<init-param>
<param-name>Expires</param-name>
<param-value>Fri, 01 Jan 1990 00:00:00 GMT</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>responseHeaderFilter</filter-name>
<url-pattern>*.jpg</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>responseHeaderFilter</filter-name>
<url-pattern>*.swf</url-pattern>
</filter-mapping>

воскресенье, 9 ноября 2008 г.

Slf4j на смену commons-logging

Наверное каждый из вас знаком с commons-logging. Однако пользы от него не так много. Наличие трюков с class-loader-ами, а также излишнее использование reflection-а и отсутствие возможности использования параметризированных сообщений, мягко говоря огорчают.

Довольно популярной заменой commons-logging является Slf4j (Simple Logging Framework for Java). Во-первых, в нем реализована поддержка MDC (пока что такая возможность реализована в logback и log4j). Этот механизм удобно использовать, когда у вас в контексте одного потока в лог записывается много параметра, которые действительны только для данного контекста. Например, для каждого сообщения необходимо выводить также имя пользователя, его логин и т.д. Вместо того чтобы указывать эти параметры каждый раз в сообщении. Можно положить их в MDC-контекст:

MDC.put("ipAddress", request.getRemoteAddr());
Principal user = request.getUserPrincipal();
MDC.put("user", user.getName());

И указать в ConversionPattern log4j эти параметры:

%X{ipAddress} - %X{user} %n%m

Во-вторых, есть наличие параметризованных сообщений. То есть, если раньше надо было писать так:

logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));

Если у нас будет не debug уровень, то будет тратиться время на сборку строки, которая не будет выводиться. Потому надо будет написать так:

if(logger.isDebugEnabled()) { 
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}

Используя Slf4j все это можно заменить одной строкой:

logger.debug("Value {} was inserted between {} and {}.", 
new Object[] {newVal, below, above});

Символами {} как раз и обозначаются параметры строки, которые передаются последним аргументом. При этом сборка строки не будет производиться при уровне ниже чем debug.

В-третьих, сама архитектура представляет собой два модуля, api slf4j-api.jar и реализация slf4j-XXXXX.jar. Реализация представлена 6-ью модулями:

На главной странице этого проекта перечислены проекты, которые используют данный фреймворк и их число растет.

Пример использования:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class TestClass {

private final Logger log = LoggerFactory.getLogger( TestClass.class );

...

log.debug( "..." );
}

вторник, 4 ноября 2008 г.

Кто такой javaagent?

У Java-машины есть один интересный параметр -javaagent. О нем почему-то весьма мало сказано в документации по самой JVM, есть лишь описание в javadoc java.lang.instrument. Сам параметр появился начиная с Java 1.5, и позволяет получать доступ к механизму манипулирования с байт-кодом классов (трансформация, переопределение классов).

В командной строке он выглядит так:

    -javaagent:jarpath[=options]

где jarpath - путь к jar-нику содержащему класс agent-а, и options - строка доп. параметров agent-а, передается при его вызове.

Сам параметер может содержаться несколько раз в строке параметров JVM, позволяя загружать несколько agent-ов. Указываемый JAR должен удовлетворять спецификации JAR-файла, т.е. иметь манифест файл META-INF/MANIFEST.MF, со следующими возможными атрибутами:

  • Premain-Class (обязательный) - он содержит имя класса agent-а с premain методом.

  • Boot-Class-Path - содержит как абсолютные так и относительные пути поиска классов, для Bootstrap classloader-а. Относительный путь начинается от абсолютного пути самого JAR-файла. Пути могут указывать как на директорию, так и на jar-библиотеку. В качестве разделителя используются пробел. Эти пути будут использоваться в случае, если какие-то классы не были найдены стандартными механизмами Java.

  • Can-Redefine-Classes - указывает возможность переопределения классов, может иметь значение true, либо false (по-умолчанию).

  • Can-Retransform-Classes (только в Java 1.6) - указывает возможность ретрансформации классов, может иметь значение true, либо false (по-умолчанию).

Метод premain, может иметь одну из следующих сигнатур:

public static void premain(String agentArgs, Instrumentation inst);

public static void premain(String agentArgs); (только Java 1.6)

В agentArgs передается сама строка options, разработчик должен сам реализовать логику парсинга этой строки, если хочет передавать в ней несколько аргументов. Собственно сам интерфейс java.lang.instrument.Instrumentation и предоставляет нам доступ к механизмам операций с байт-кодом. Сам метод premain вызывается, как вы уже должно быть поняли, еще до выполнения метода main приложения.

public class InstrumentExample {

private static Instrumentation inst;

public static void premain(String options, Instrumentation inst) {
inst.addTransformer(new SomeTransformer());
this.inst = inst;
}

public static void main(String args[] ) {

...

byte[] classBytes = ...
ClassDefinition classDef = new ClassDefinition(SomeClass.class, classBytes);
inst.redefineClasses(classDef);

...

}

public static class SomeTransformer implements ClassFileTransformer {
public byte[] transform(java.lang.ClassLoader loader,
java.lang.String className,
java.lang.Class classBeingRedefined,
java.security.ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException
{
...
}
}

...
}

Чтобы использовать механизм трансфорамации классов вам нужно реализовать интерфейс java.lang.instrument.ClassFileTransformer. И зарегистрировать свою реализацию через метод Instrumentation.addTransformer. Сам трасформер будет срабатывать каждый раз при:

  • загрузке класса ClassLoader.defineClass
  • переопределении класса Instrumentation.redefineClasses
  • ретрансформации класса Instrumentation.retransformClasses.

Аргумент classfileBuffer содержит байт-код текущей версии класса, и его нельзя модифицировать, для его переопределения трансформер должен вернуть новый массив байт, либо null, если трансформация не была произведена. В случае, когда трансформеров несколько, то они будут вызваны по цепочке, при этом classfileBuffer будет результатом предыдущего трансформера. Чтобы сообщить об ошибке при трансформации необходимо выбросить java.lang.instrument.IllegalClassFormatException, при unchecked-ошибках результат будет такой, как если бы метод вернул null.

Для переопределения класса необходимо указать выше описанный параметер в манифесте и использовать класс java.lang.instrument.ClassDefinition. Только вот есть ограничения: переопределение класса не должно добавлять, удалять новые поля или методы, менять сигнатуру методов или иерархию наследования. Можно менять только тело методов, структура класса меняться не должна. Если в приложении уже используются экземпляры предыдущей версии класса, то с ними ничего не произойдет, однако при последующем создании класса, будет использована уже новая версия.

К сожалению, Java-платформа не предоставляет стандартных инструментов для модификации/генерации байт-кода. В этом случае можно воспользоваться уже готовыми фреймворками, например, Javaassist или BCEL.

Javaаgent реализован в фреймворке Spring, чтобы можно было использовать AOP для классов, вне контекста. Тестовый фреймворк Jmockit также реализует своего javaagent-а. Тем самым открывая возможность писать mock-и для классов, экземпляры которых вы не можете заменить обычными способами, либо они содержат статические методы, заменить которые возможно только до загрузки класса.