среда, 31 декабря 2008 г.

Миграция данных между БД

Перемещение/преобразование данных между разными БД требует использование хорошо отлаженного механизма выполнения sql-запросов, которые необходимы для импорта/экспорта данных между БД. Написание такого рода движка потребует не мало времени и усилий.

Среди open-source решений на java мне известны два инструмента решающих подобную задачу - JDBCImporter и Scriptella. Первый инструмент делает экспорт данных сначала в xml, а затем производит из него импорт, т.е. имеет промежуточный xml при обработке данных. Сам JDBCImporter имеет утечки памяти при обработки данных больших объемов, да и как-то не активно развивается.

Scriptella - это инструмент для выполнения ETL (Extract-Transform-Load) процесса, который позволяет собирать данные из одного или нескольких источников данных и загружать их в другие позволяя выполнять различного рода трасформацию самих данных. При трансформации данных можно использовать скрипты прямо в sql-запросах, написанных на Velocity, JavaScript, JEXL... Сама конфигурация etl-процесса описывается в xml файле:

<!DOCTYPE etl SYSTEM "http://scriptella.javaforge.com/dtd/etl.dtd">
<etl>
<description></description>
<properties>
<include href="script.properties"/>
driver=org.jdcDriver
</properties>
<connection id="con1" driver="$driver" url="${url}" user="$user" password="12345678">
driver.property=value
</connection>
<connection id="con2" url="jdbc:hsqldb:file:db" user="sa" password=""/>
<script connection-id="out">
<include href="dbschema.sql"/>
</script>
<query connection-id="in">
SELECT * from Bug
<script connection-id="out">
INSERT INTO Bug VALUES (?ID, ?priority, ?summary, ?status);
</script>
</query>
</etl>

В описании конфигурации выделяют три основных элемента:

  • connection - представляет собой соединение с источником данных, таковым может являться как БД, XLS, CSV, XML... Можно также реализовать свой драйвер источника данных для Scriptella.

  • script - скрипт, написанный на языке источника данных к которому он относится через connection-id. Именно здесь выполняются различные скрипты на Velocity, JavaScript, JEXL и т.д.

  • query - запрос, выполняемый на источнике данных connection-id. Может содержать любое кол-во вложенных query и script элементов.

Запуск Scriptella можно произвести из командной строки, с помощью Ant-а, либо прямиком из Java.

четверг, 11 декабря 2008 г.

Event-Driven Development в Spring. Часть 2

Механизм рассылки event-ов в контексте Spring реализован одним интерфейсом org.springframework.context.event.ApplicationEventMulticaster. Именно ему и будут делегироваться все добавления, удаления listener-ов, а также публикация event-ов для них. По-умолчанию, в качестве реализации такого интерфейса будет использоваться объект класса org.springframework.context.event.SimpleApplicationEventMulticaster. В самом же контексте данный bean регистрируется под именем applicationEventMulticaster. Однако, если под таким именем bean уже существует и реализует нужный интерфейс, то будет использоваться именно он.

Необоходимость создания своего ApplicationEventMulticaster-а может возникнуть, например, чтобы рассылать event-ы по spring-контекстам целого кластера. Именно этот случай и реализован в моем примере. Сам механизм коммуникации между нодами обеспечивается фреймворком JGroups, который рассылает сообщения по сети multicast-ом через UDP-протокол. Объект com.blogspot.nkoksharov.springevents.jgroups.JGroupsMulticaster переопределяет метод void multicastEvent(ApplicationEvent event) и рассылает по сети лишь те экземпляры event-ов, классы которых являются подклассами com.blogspot.nkoksharov.springevents.jgroups.JGroupsEvent. А вот и сам код JGroupsMulticaster-а:

package com.blogspot.nkoksharov.springevents.jgroups;

import org.jgroups.*;
import org.slf4j.*;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SimpleApplicationEventMulticaster;

/**
* Alternative multicaster to org.springframework.context.event.SimpleApplicationEventMulticaster
*
* @author nkoksharov
*
*/
public class JGroupsMulticaster extends SimpleApplicationEventMulticaster implements InitializingBean, DisposableBean, LocalEventMulticaster {

private final Logger logger = LoggerFactory.getLogger(getClass());
private JChannel jchannel;
private JGroupsReceiver receiver = new JGroupsReceiver();
private String configFile;
private String clusterName;

public void multicastEvent(ApplicationEvent event) {
if (event instanceof JGroupsEvent) {
try {
jchannel.send(new Message(null, null, event));
} catch (ChannelNotConnectedException e) {
logger.error("channel not connected", e);
} catch (ChannelClosedException e) {
logger.error("channel closed", e);
}
} else {
super.multicastEvent(event);
}
}

public void localMulticastEvent(JGroupsEvent event) {
super.multicastEvent(event);
}

public void afterPropertiesSet() throws Exception {
jchannel = new JChannel(configFile);
receiver.setLocalMulticater(this);
jchannel.setReceiver(receiver);
jchannel.connect(clusterName);
}

public void destroy() throws Exception {
jchannel.close();
}
public void setConfigFile(String configFile) {
this.configFile = configFile;
}
public void setClusterName(String clusterName) {
this.clusterName = clusterName;
}
}

Рассылка же абсолютно всех event-ов, в частности системных, может привести к некорректной работе всего spring-контекста.

Проект собирается с помощью Maven-а и запускается командой:

mvn compile exec:java

Каждый запуск - это новый контекст, т.е. можно запустить приложение на разных компьютерах в локальной сети и наблюдать появление сообщений в консоли о получении event-ов. Также можно запустить несколько экземпляров приложения на одном компьютере, все должно работать.

Скачать исходники
Event-Driven Development в Spring. Часть 1

понедельник, 1 декабря 2008 г.

Rhino и Janino для вычисления математических выражений

Для вычисления математических формул в run-time можно воспользоваться script-движком реализующим JSR-223. В Java 1.6 реализация этой спецификации представлена javascript-движоком Rhino. Однако применение целого script-движка для обычного вычисления формул будет довольно неоправданным решением. Мой поиск привел к Janino - это целый фреймворк для динамической компиляции и исполнения java. То есть он позволяет выполнять скрипты на java внутри самой java. Одно из его призваний это компиляция и выполнение различных математических выражений с помощью org.codehaus.janino.ExpressionEvaluator.

Я решил сравнить производительность Rhino и Janino. Для теста выбрал простую формулу: (x-10)*Math.sin(0.1)*2, вычисление которой будет осуществляться 1000000 раз.

package com.blogspot.nkoksharov;

import javax.script.*;

import org.apache.commons.lang.time.StopWatch;
import org.codehaus.janino.ExpressionEvaluator;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import org.testng.annotations.Test;

public class ExpressionTest {

@Test
public void testRhino() throws ScriptException {
ScriptEngineManager scriptManager = new ScriptEngineManager();
ScriptEngine se = scriptManager.getEngineByName("JavaScript");
SimpleBindings bindings = new SimpleBindings();
bindings.put("x", 2);

StopWatch stopWatch = new StopWatch();
stopWatch.start();
for (int i = 0; i < 1000000; i++) {
Double result = (Double) se.eval("(x-10)*Math.sin(0.1)*2", bindings);
}
stopWatch.stop();
System.out.println("JDK Rhino time: " + stopWatch);
}

@Test
public void testRhinoFunc() throws ScriptException, NoSuchMethodException {
ScriptEngineManager scriptManager = new ScriptEngineManager();
ScriptEngine se = scriptManager.getEngineByName("JavaScript");

String script = "function count(x) { return (x-10)*Math.sin(0.1)*2; }";
se.eval(script);

StopWatch stopWatch = new StopWatch();
stopWatch.start();
for (int i = 0; i < 1000000; i++) {
Invocable inv = (Invocable) se;
Double result = (Double) inv.invokeFunction("count", 2);
}
stopWatch.stop();
System.out.println("JDK Rhino func time: " + stopWatch);
}

@Test
public void testNativeRhinoCompiledFunc() {
ContextFactory cf = new ContextFactory();
Context cx = cf.enterContext();
Scriptable scope = cx.initStandardObjects();
Function f = cx.compileFunction(scope, "function count(x) { return (x-10)*Math.sin(0.1)*2; }", null, 0, null);

StopWatch stopWatch = new StopWatch();
stopWatch.start();
for (int i = 0; i < 1000000; i++) {
Double value = (Double) f.call(cx, scope, scope, new Object[] {2});
}
stopWatch.stop();
System.out.println("Mozilla Rhino compiled-func time: " + stopWatch);
}

@Test
public void testJanino() throws Exception {
ExpressionEvaluator ee = new ExpressionEvaluator("(x-10)*Math.sin(0.1)*2",
Double.class, new String[] {"x"}, new Class[] {Integer.class});

StopWatch stopWatch = new StopWatch();
stopWatch.start();
for (int i = 0; i < 1000000; i++) {
Double result = (Double) ee.evaluate(new Object[] {2});
}
stopWatch.stop();
System.out.println("Janino time: " + stopWatch);
}
}

Результат вполне оправдал мои ожидания:

                                 (ч:мм:сс.мс)
JDK Rhino time: 0:01:50.061
JDK Rhino func time: 0:00:59.040
Mozilla Rhino compiled-func time: 0:00:00.344
Janino time: 0:00:00.156

В итоге получается, что скорость выполнения с Rhino использующейся в JDK заметно отстает от Janino, даже не смотря на выполнение через javascript-функцию. Однако, если вы будете использовать реализацию Rhino от Mozilla, с возможностью компиляции в bytecode, то получите лишь 2-х кратное отставание по скорости. И так первенство у Janino, который также компилирует выражения перед выполнением. При этом обращения к методу ExpressionEvaluator.evaluate будут thread-safe.

среда, 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-и для классов, экземпляры которых вы не можете заменить обычными способами, либо они содержат статические методы, заменить которые возможно только до загрузки класса.

пятница, 31 октября 2008 г.

Event-Driven Development в Spring. Часть 1

Еще в Spring 1.2 была возможность посылать различные event-ы через весь контекст приложения. Я считаю этот вид коммуникации в некоторых местах приложения единственным верным решением.

Опишу кратко как работает этот механизм. Базовым классом для event-а должен быть org.springframework.context.ApplicationEvent, для рассылки которого используется метод org.springframework.context.ApplicationContext.publishEvent(event). Ну а получать event-ы будут те bean-ы, которые реализуют интерфейс org.springframework.context.ApplicationListener с единственным методом onApplicationEvent(event).

Допустим, что у нас есть логика, после выполнения которой другим сервисам необоходимо также сделать какие-то действия. Стандартный вариант решения такой задачи предполагает использование IoC:

public class BussinesServiceImpl implements BussinesService {

@Autowired
private EmailService emailService;
@Autowired
private LoggingService logService;

public void doAction() {

...

emailService.sendEmail(email, body);
logService.recordLog(data);
}
}

Также можно использовать и аспекты. Только вот получать параметры, которые пришли не через аргументы метода, advice-ам будет сложно. Реализовать такой случай с помощью event-ов можно так:

public class BussinesServiceImpl implements BussinesService, ApplicationContextAware {

private ApplicationContext context;

public void doAction() {

...

ActionEvent event = new ActionEvent();
event.setEmail(email);
event.setEmailBody(body);
event.setData(data);
context.publishEvent(event);
}

public void setApplicationContext(ApplicationContext value) {
context = value;
}
}

Соответственно сами сервисы будут выглядеть так:

public class EmailServiceImpl implements EmailService, ApplicationListener {

...

public void onApplicationEvent(ApplicationEvent event) {
if (!(event instanceof ActionEvent)) {
return;
}

ActionEvent actionEvent = (ActionEvent) event;
sendEmail(actionEvent.getEmail(), actionEvent.getEmailBody());
}
}

public class LoggingServiceImpl implements LoggingService, ApplicationListener {

...

public void onApplicationEvent(ApplicationEvent event) {
if (!(event instanceof ActionEvent)) {
return;
}

ActionEvent actionEvent = (ActionEvent) event;
recordLog(actionEvent.getData());
}
}

Какое преимущество дает такой подход? Изоляцию сервиса, от лишних зависимостей. Такая изоляция дает возможность выносить сервисы типа BussinesService в отдельные модули, которые не потребуют в дальнейшем модификации, если понадобится добавить вызов какого-либо сервиса.

среда, 29 октября 2008 г.

Идентификация пользователя в браузере. Как узнать IP и MAC в Java-апплете.

Задача: необходимо чтобы пользователь, с одного компьютера, имел лишь одну сессию с сервером.

При этом нужно учитывать, что пользователь может использовать разные браузеры, так что зацепка за какой-нибудь параметр http-заголовока типа User-agent ничего не даст. Однако возможно получить реальный ip-адрес клиента в подсети, а не возможного прокси, который мы получаем через HttpServletRequest.getLocalAddr(). Редко сейчас у клиентов есть выделенные ip. Также, помимо ip можно получить и mac-адрес! Все что для этого нужно так это написать Java-Applet, который запустится у клиента в браузере и передаст нужную нам информацию.

Вообще возможность определение ip адреса во внутренней сети пришла в голову некому Lars Kindermann-у в 2002-ом году. Судя по фотке он проживает где-то на полярном круге .-) Его идея оказалась принципиально простой. Java-Applet создает подключение к серверу, с которого его скачали, через объект java.net.Socket. В результате мы легко можем получить сетевую информацию о клиенте.

Ну а чтобы получить mac-адрес, не придется даже создавать соединения, можно просто сделать вызов java.net.NetworkInterface.getHardwareAddress(). Можно было бы довольствоваться лишь mac адресом. Однако, возможность его получения появилась лишь в JRE 1.6.

В решении я использовал, как обычно, Spring Security 2.0.4. Идея следующая, после аутентификации пользователя мы проверяем есть ли такой же клиент, с тем же ip и mac-ом, уже в системе. Для этого необходимо, чтобы фильтр аутентификации перебросил нас на страничку с апплетом. Затем уже апплет собирает сетевую информацию о клиенте и отсылает ее на сервер простым GET-ом, в ответ на этот запрос, сервер, выполнив проверку, посылает редирект на следующую страничку, либо выдает ошибку. При получении запроса сервером, составляется композитный id клиента, в нем учавствует как mac и локальный ip клиента, пришедшие от апплета, так и ip шлюза с которым соединен сервер и за которым находится сам клиент. Я свел воедино оба решения, использовать нужно либо ip+ip шлюза, либо mac вместе их использовать уже избыточно. Если есть возможность, и в требованиях вашего приложения предусмотрена возможность установки JRE 1.6 на машину клиента, то лучше использовать mac. Способ идентификации основанный только на ip имеет недостаток. В случае если за одним шлюзом может находится несколько подсетей, а следовательно и клиентов с одинаковым адресом может быть тоже несколько.

Вот собственно и сам исходный код апплета:

import java.applet.Applet;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.util.Enumeration;

public class UserApplet extends Applet {

/**
* Запуск апплета
*/
public void start() {
String remoteHost = getDocumentBase().getHost();
int remotePort = getDocumentBase().getPort();
String ip = getLocalIp(remoteHost, remotePort);
String mac = getLocalMac();
sendInfo(ip, mac);
}

/**
* Получение локального mac
*/
private String getLocalMac() {
try {
Enumeration nis = NetworkInterface.getNetworkInterfaces();
while(nis.hasMoreElements()) {
String mac = macToString((NetworkInterface) nis.nextElement());
if (mac != null) {
return mac;
}
}
} catch (SocketException e) {
showStatus("");
}
return null;
}

private static String macToString(NetworkInterface ni)
throws SocketException {
byte mac[] = ni.getHardwareAddress();
if(mac != null) {
StringBuffer macAddress = new StringBuffer();
String sep = "";
for (int i = 0; i < mac.length; i++) {
int b = Math.abs(mac[i]);
String hexByte = Integer.toHexString(b);
macAddress.append(sep).append(hexByte);
sep = ":";
}
return macAddress.toString();
}
return null;
}

/**
* Получение локального ip
*/
private String getLocalIp(String remoteHost, int remotePort) {
Socket socket = null;
try {
socket = new Socket(remoteHost, remotePort);
String localIp = socket.getLocalAddress().getHostAddress();
return localIp;
} catch (Exception e) {
showStatus("");
} finally {
if (socket != null) {
socket.close();
}
}
return "unknown";
}

/**
* Отправляем информацию об ip и mac
*/
private void sendInfo(String ip, String mac) {
try {
URL url = new URL(getDocumentBase(), "/userInfo?ip=" + ip + "&mac=" + mac);
getAppletContext().showDocument(url, "_self");
} catch (Exception e) {
showStatus("");
}
}
};

Апплет загружает ответом нашего Spring Security фильтра ru.someone.UserInfoFilter содержимым:

<applet code="UserApplet.class" codebase="/" codetype="application/java" width="0" height="0"></applet>

После чего этот же фильтр делает необходимую проверку и посылает ответ. При логауте из системы, данные о клиенте удаляются вместе с его сессией. Чтобы это действительно было так, в web.xml нужно прописать listener: org.springframework.security.ui. session.HttpSessionEventPublisher. Который будет посылает эвенты об удалении сессии по всему Spring-контексту, а наш ru.someone.UserInfoFilter будет их получать.

Исходники можно взять здесь

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

Развивать вместе

Как вы уже должно быть заметили, рядом с элементом прокрутки окна появился рыжая кнопка "Оставьте свой отзыв". Я разместил ее с одной целью - чтобы вы писали о том, чего не хватает в моем блоге, свои идеи и предложения.

Миграция данных с LiquiBase

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

Однако, я был убежден в том, что такого рода фреймворк должен быть уже кем-то написан. Им оказался LiquiBase написанный на той же Java и выпущенный под лицензией LGPL. Вот основные фичи этого проекта:

  • Не зависит от реализации БД, поддерживает: MySQL, PostgreSQL, Oracle, MS SQL, DB2, Derby, HSQL
  • Возможность проведения миграций от нескольких пользователей одновременно
  • Поддержка rollback-а миграций
  • Описание миграций в XML файле, с помощью внутренних команд.
  • Интеграция c Maven, Ant, Spring, Hibernate, Grails, возможность запуска из коммандой строки.

Формат документа, в котором описываются сами миграции выглядит примерно следующим образом:

<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog/1.8"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog/1.8
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-1.8.xsd">

<changeSet id="1" author="bob">
<comment>Create table<comment>
<createTable tableName="department">
<column name="id" type="int">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(50)">
<constraints nullable="false"/>
</column>
<column name="active" type="boolean" defaultValue="1"/>
</createTable>
</changeSet>

<changeSet id="2" author="sam">
<comment>Change name</comment>
<sql>
UPDATE TABLE SET name = 'someone' WHERE name = 'foo';
</sql>
<rollback>
<sql>
UPDATE TABLE SET name = 'foo' WHERE name = 'someone';
</sql>
</rollback>
</changeSet>
</databaseChangeLog>
Также хорошую статью на эту тему можно прочитать здесь.



Использование liquibase с hibernate и maven. Часть 1

понедельник, 27 октября 2008 г.

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

Старая проблема - как отключить кэширование страницы в браузере, но лишний раз напомнить о ней не помешает. Итак, в html странице, нужно использовать следующие meta-тэги:

<html>
<head>
<meta http-equiv="Cache-Control" content="no-cache, no-store, max-age=0, must-revalidate"/>
<meta http-equiv="Pragma" content="no-cache"/>
<meta http-equiv="Expires" content="Fri, 01 Jan 1990 00:00:00 GMT"/>
</head>
<body>
...
</body>
<html>

Значение параметера http-equiv должно быть названием параметра используемого непосредственно в http-заголовке. В content указываем само значение параметра. Однако это вовсе не означает, что вы увидите их в самом http-заголовке, хотя есть серваки, которые автоматически встраивают такого рода meta-тэги в http-заголовок.

Кратко об используемых параметрах:

  • Cache-Control - параметер управления кешированием страниц. Введен в HTTP 1.1.
  • Pragma - отключение кэширования страницы. Имеет единственное значение no-cache. Введен в HTTP 1.0.
  • Expires - устанавливает дату и время, после которого документ считается устаревшим. Дата должна указываться в следующем формате (на английском языке): День недели (сокр.) число (2 цифры) Месяц (сокр.) год часы:минуты:секунды GMT. Введен в HTTP 0.9

Т.к. не известно заранее какая версия HTTP-протокола будет использоваться на стороне клиента, то лучше использовать все три параметра.

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

ConceptDraw Office - хорошая замена MS Visio и MS Project

Хочу представить вашему вниманию такой замечательный пакет, как ConceptDraw Office. Используя этот пакет, можно управлять всеми аспектами проекта - от мозгового штурма до создания команды, планирования, распределения ресурсов и анализа результатов. В состав пакета входят продукты ConceptDraw MINDMAP, ConceptDraw PROJECT и ConceptDraw PRO - в таком единстве, набор обеспечивает все возможности для управлениями рабочими проектами: планирование и составление графика, организация процесса работы, процесс отслеживания и управления проектом.



Интерфейс выполнен в стиле MacOS - элементы на диаграммах с закругленными краями, гибкие связи и т.д. Соответственно и сам пакет существует под две платформы MacOS и Windows.

Я посмотрел лишь ConceptDraw PRO и ConceptDraw PROJECT. ConceptDraw PRO - полноценная замена Microsoft Visio, имеет богатый набор графических компонентов, с первого взгляда впечатлила фича авто-лайаута по краям соседних элементов. ConceptDraw PROJECT - отличная замена Microsoft Project, с возможность экспорта и импорта из него. ConceptDraw PROJECT более корректно работает с единицей измерения длительности задач - час. К примеру, не пишет для группы задач выраженных в часах, длительность в 1,78 дней. А воозможность экспорта набора задач в ConceptDraw MINDMAP оставила неизгладимое впечатление.

Вообщем, рекомендую к использованию!

пятница, 24 октября 2008 г.

Logout в Spring Security используя BlazeDS

В продолжение темы программной аутентификации, решил описать программный способ logout-а в BlazeDS. Я знаю, что кому-то это может очень пригодиться .-) Необоходимо сделать logout на уровне бизнес-логики, которая о javax.servlet.http.HttpSession ничего не знает и знать не может. Покопавшись в исходниках flex.messaging.FlexSession
и его подклассе flex.messaging.HttpFlexSession я нашел замечательный метод invalidate. Опять же, код в три строчки:

public void logout() {
HttpFlexSession flexSession = (HttpFlexSession) FlexContext.getFlexSession();
flexSession.invalidate(false);
SecurityContextHolder.clearContext();
}

Программная аутентификация в Spring Security 2.0

Порой возникает необходимость произвести аутентификацию в приложении программно, а не через POST-запрос. В Spring Security это можно сделать с помощью, буквально, трех строк кода:

@Autowired
private AuthenticationManager _authenticationManager;

private void authenticate(HttpServletRequest request, String nick, String password) {
Authentication auth = new UsernamePasswordAuthenticationToken(nick, password);
WebAuthenticationDetails details = new WebAuthenticationDetails(request);
auth.setDetails(details);
Authentication fullauth = _authenticationManager.authenticate(auth);
SecurityContextHolder.getContext().setAuthentication(fullauth);
}

среда, 22 октября 2008 г.

Проблема логина через https в Spring Security 2.0

Довольно тривиальная задача, как мне казалось, сделать логин на https-соединении с дальнейшим редиректом на страницу доступную по обычному http-каналу. Для реализации такого решения использовался Spring Security 2.0.4. Однако, все оказалось не так просто.
Для начала приведу свою настройку security-контекста:

<sec:http auto-config="true">
<sec:intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" requires-channel="https"/>
<sec:intercept-url pattern="/index.jsp" access="ROLE_PLAYER" requires-channel="http"/>
<sec:form-login login-page="/login.jsp" default-target-url="/index.jsp" />
</sec:http>

Конфигурация правильная, только вот редирект не работает. Проблема в том, что после успешной аутентификации, меня перебрасывает обратно на login.jsp.

Воспользовашись HttpWatch-ем, я выяснил, что проблема в том, что при переходе, допустим, с линка https://localhost:8443/login.jsp на линк http://localhost:8080/index.jsp, jsessionid передавалась в куке через http-заголовок, а не через урл, как полагается в этом случае. Поэтому Spring Security не видел ранее созданной сесси и создавал свою. Кстати, по умолчанию, после успешной аутентификации он создает новую сессию, дабы предотвратить атаку Session Fixation.

Я завел на это багу http://jira.springframework.org/browse/SEC-1019. Не знаю, когда она будет закрыта, думаю, что не скоро. Поэтому могу предложить свое, временное, решение этой проблемы.
В конфигурации security-контекста пишем:

<sec:http entry-point-ref="authEntryPoint">
<sec:anonymous/>
<sec:port-mappings>
<sec:port-mapping http="8080" https="8443"/>
</sec:port-mappings>
<sec:intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" requires-channel="https"/>
<sec:intercept-url pattern="/index.jsp" access="ROLE_PLAYER" requires-channel="http"/>
</sec:http>

<bean id="authEntryPoint" class="org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl" value="/login.jsp"/>
</bean>

<bean id="authFilter" class="org.springframework.security.ui.webapp.AuthenticationProcessingFilter">
<sec:custom-filter position="AUTHENTICATION_PROCESSING_FILTER" />
<property name="filterProcessesUrl" value="/j_spring_security_check"/>
<property name="defaultTargetUrl" value="/"/>
<property name="authenticationManager" ref="authenticationManager"/>
<property name="targetUrlResolver">
<bean class="ru.someone.security.ExTargetUrlResolverImpl">
<property name="url" value="/index.jsp"/>
</bean>
</property>
</bean>

<sec:authentication-manager alias="authenticationManager"/>

Столько xml-я пришлось написать лишь для того, чтобы использовать свою реализацию org.springframework.security.ui.TargetUrlResolver, по-другому никак. Вот собственно и сама реализация этого интерфейса:

package ru.someone.security;

import javax.servlet.http.HttpServletRequest;
import org.springframework.security.Authentication;
import org.springframework.security.ui.TargetUrlResolverImpl;
import org.springframework.security.ui.savedrequest.SavedRequest;

public class ExTargetUrlResolverImpl extends TargetUrlResolverImpl {

private String url;

@Override
public String determineTargetUrl(SavedRequest savedRequest, HttpServletRequest request,
Authentication auth) {
String sessionId = request.getSession().getId();
return url + ";jsessionid=" + sessionId;
}

public void setUrl(String url) {
this.url = url;
}
}

Вводная часть...

Друзья! И так, свершилось - я завел себе блог! Очень этому рад и надеюсь, что он будет постоянно пополнятся моими идеями .-)