Triangle
Сайт разработчика
RSS
  • Home Page Home
  • Gefest Web Server
  • Contact

Приклад використання Callable для повернання результата

Java 0 Comment »

Я завжди використовував Runnable, але недавно довелось перейти на Callable

Наведу простий приклад використання. Є офіційний приклад Using Callable to Return Results From Runnables

Я наводжу свій приклад і результат виконання.

Class Main – клас який запускає на виконання пул потоків а також відображає результат виконання.

Class Task – Клас який виконує завдання в пулі потоків


package callabletest;

/**
*
* @author ivi
*/

import java.util.*;
import java.util.concurrent.*;
import callabletest.Task;
public class Main {

public static void main(String[] args) throws Exception
{
ExecutorService pool = Executors.newFixedThreadPool(3);

Set<Future<Object>> set = new HashSet<Future<Object>>();

for(int i =0; i < 10; i ++)
{
Callable<Object> callable = callabletest.Task.getTask();
Future<Object> future = pool.submit(callable);
set.add(future);
}

for (Future<Object> future : set)
{
Object obj  = future.get();
Task task  =  (Task) obj;
System.out.println("Test Return Values: "+task.getCurrent());
}
pool.shutdown();

}

}

package callabletest;
/**
*
* @author ivi
*/
import java.util.concurrent.*;

public class Task implements Callable
{
private static int counter  = 0;

/**
* @return the counter
*/
public static int getCounter() {
return counter;
}
private int current  = 0;

private Task()
{
counter ++;
this.current =  counter;
}

public synchronized static Task getTask()
{
return new Task();
}

Результат

run:
Task #1
Task #2
Task #3
Task #4
Task #5
Test Return Value: 1
Task #6
Task #7
Task #8
Task #9
Test Return Value: 6
Task #10
Test Return Value: 10
Test Return Value: 5
Test Return Value: 4
Test Return Value: 3
Test Return Value: 9
Test Return Value: 8
Test Return Value: 7
Test Return Value: 2
BUILD SUCCESSFUL (total time: 4 seconds)

August 18th, 2010  
Tags: Callable, Executors, ExecutorService, Future, Java, java.util.concurrent



Настройка кластера MySQL

Databases 0 Comment »

Введение

Кластерное решение на базе MySQL является отказоустойчивым, избыточным и масштабируемым решением для баз данных, основанным на открытых исходных текстах. Использование такой схемы позволяет достигнуть надежности в 99.999 %. В этой статье мы опишем процесс установки, настройки и тестирования кластера MySQL, состоящего из трех узлов.

Схема подключения

MySQL

Аппаратное обеспечение

Мы использовали четыре сервера Sun Ultra Enterprise, но процесс установки кластера на другой UNIX- или Linux-подобной системе будет отличаться очень незначительно.

Наши четыре машины относятся к одной из трех ролей:

  1. Хранилище (mysql-ndb-1 и mysql-ndb-2)
  2. API (mysql-api-1)
  3. Сервер управления и консоль управления (mgmt)

Обратите внимание, что узлы хранилища также явзяются API нодами, но API нода не является хранилищем. Узел API – полноправный член кластера, но он не хранит никаких данных кластера и его состояние (работает/не работает) не затрагивает целостность или доступность данных. Об этой ноде можно думать как о “клиенте” кластера. Приложения, такие как Web-сервер, установлены на ноде API и общаются с процессом MySQL, запущенным локально, именно этот процесс запрашивает данные от хранилищ. На хранилищах также могут быть установлены приложения, поскольку они совмещают в себе API ноды, но для промышленного применения такое совмещение нежелательно.

Программное обеспечение

Мы используем Sun Solaris 8 и mysql-max-4.1.9.

Мы использовали прекомпилированный пакет MySQL для Sun SPARC Solaris 8, вы должны использовать программное обеспечение в зависимости от используемой вами архитектуры, но в любом случае, необходимо использовать вариант “max”.

Порядок действий

Шаг 1: После загрузки нод mysql-ndb-1 (192.168.0.33) и mysql-ndb-2 (192.168.0.34) устанавливаем и настраиваем MySQL:

    
    mysql-ndb-1# groupadd mysql
    mysql-ndb-1# useradd -g mysql mysql
    mysql-ndb-1# cd /usr/local
    mysql-ndb-1# wget http://dev.mysql.com/get/Downloads/MySQL-4.1/
    mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz/from/http://mysql.he.net/
    mysql-ndb-1# gzip -dc mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz | tar xvf -
    mysql-ndb-1# ln -s mysql-max-4.1.9-sun-solaris2.8-sparc mysql
    mysql-ndb-1# cd mysql
    mysql-ndb-1# scripts/mysql_install_db --user=mysql
    mysql-ndb-1# chown -R root  .
    mysql-ndb-1# chown -R mysql data
    mysql-ndb-1# chgrp -R mysql .
    mysql-ndb-1# cp support-files/mysql.server /etc/init.d/mysql.server
    
    mysql-ndb-2# groupadd mysql
    mysql-ndb-2# useradd -g mysql mysql
    mysql-ndb-2# cd /usr/local
    mysql-ndb-2# wget http://dev.mysql.com/get/Downloads/MySQL-4.1/
    mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz/from/http://mysql.he.net/
    mysql-ndb-2# gzip -dc mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz | tar xvf -
    mysql-ndb-2# ln -s mysql-max-4.1.9-sun-solaris2.8-sparc mysql
    mysql-ndb-2# cd mysql
    mysql-ndb-2# scripts/mysql_install_db --user=mysql
    mysql-ndb-2# chown -R root  .
    mysql-ndb-2# chown -R mysql data
    mysql-ndb-2# chgrp -R mysql .
    mysql-ndb-2# cp support-files/mysql.server /etc/init.d/mysql.server
    

Не запускайте сервис!

Шаг 2: Установим сервер и консоль управления на mgmt (192.168.0.32):

    
    
    mgmt# gzip -dc mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz | tar xvf -
    mgmt# cp mysql-max-4.1.9-sun-solaris2.8-sparc/bin/ndb_mgm /usr/bin
    mgmt# cp mysql-max-4.1.9-sun-solaris2.8-sparc/bin/ndb_mgmd /usr/bin
    mgmt# rm -r mysql-max-4.1.9-sun-solaris2.8-sparc
    mgmt# mkdir /var/lib/mysql-cluster
    mgmt# cd /var/lib/mysql-cluster
    mgmt# vi config.ini
    

Файл config.ini содержит необходимую информацию для кластера:

    
    [NDBD DEFAULT]
    NoOfReplicas=2
    [MYSQLD DEFAULT]
    [NDB_MGMD DEFAULT]
    [TCP DEFAULT]
    # Management Server
    [NDB_MGMD]
    HostName=192.168.0.32           # IP address of this server
    # Storage Nodes
    [NDBD]
    HostName=192.168.0.33           # IP address of storage-node-1
    DataDir= /var/lib/mysql-cluster
    [NDBD]
    HostName=192.168.0.34           # IP address of storage-node-2
    DataDir=/var/lib/mysql-cluster
    # Setup node IDs for mySQL API-servers (clients of the cluster)
    [MYSQLD]
    [MYSQLD]
    [MYSQLD]
    [MYSQLD]
    

Запускаем сервер управления и проверяем его работу:

    
    mgmt# ndb_mgmd
    mgmt# ps -ef | grep [n]db
    

Шаг 3: Конфигурируем MySQL на нодах mysql-ndb-1 (192.168.0.33) и mysql-ndb-2 (192.168.0.34):

    
    mysql-ndb-1# vi /etc/my.cnf
    mysql-ndb-2# vi /etc/my.cnf
    

В данном случае файл конфигурации выглядит следующим образом:

    
    [mysqld]
    ndbcluster
    ndb-connectstring='host=192.168.0.32'    # IP address of the management server
    [mysql_cluster]
    ndb-connectstring='host=192.168.0.32'    # IP address of the management server
    

Запускаем сервисы и проверяем их работу:

    
    mysql-ndb-1# mkdir /var/lib/mysql-cluster
    mysql-ndb-1# cd /var/lib/mysql-cluster
    mysql-ndb-1# /usr/local/mysql/bin/ndbd --initial
    mysql-ndb-1# /etc/init.d/mysql.server start
    mysql-ndb-1# ps -ef | grep [n]dbd
    mysql-ndb-1# ps -ef | grep [m]ysqld
    
    mysql-ndb-2# mkdir /var/lib/mysql-cluster
    mysql-ndb-2# cd /var/lib/mysql-cluster
    mysql-ndb-2# /usr/local/mysql/bin/ndbd --initial
    mysql-ndb-2# /etc/init.d/mysql.server start
    mysql-ndb-2# ps -ef | grep [n]dbd
    mysql-ndb-2# ps -ef | grep [m]ysqld
    

Если сервис не запустился, то просмотрите файл /usr/local/mysql/data/${HOSTNAME}.err и устраните проблему.

Шаг 4: Запускаем сервер и консоль управления, проверяем состояние кластера:

    
    mgmt# ndb_mgm
    -- NDB Cluster -- Management Client --
    ndb_mgm> show
    Connected to Management Server at: localhost:1186
    Cluster Configuration
    ---------------------
    [ndbd(NDB)]     2 node(s)
    id=2    @192.168.0.33  (Version: 4.1.9, starting, Nodegroup: 0, Master)
    id=3    @192.168.0.34  (Version: 4.1.9, starting, Nodegroup: 0)
    
    [ndb_mgmd(MGM)] 1 node(s)
    id=1    @192.168.0.32  (Version: 4.1.9)
    
    [mysqld(API)]   4 node(s)
    id=4 (not connected, accepting connect from any host)
    id=5 (not connected, accepting connect from any host)
    id=6 (not connected, accepting connect from any host)
    id=7 (not connected, accepting connect from any host)
    

Шаг 5: Создаем тестовую базу данных и проверяем корректность операций:

Создаем на хранилищах mysql-ndb-1 и mysql-ndb-2 тестовую базу:

    
    mysql-ndb-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 1 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> create database foo;
    Query OK, 1 row affected (0.09 sec)
    
    mysql-ndb-2# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 6 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> create database foo;
    Query OK, 1 row affected (0.13 sec)
    

Вернитесь на хранилище mysql-ndb-1, и создайте простейшую таблицу с некоторыми значениями:

    
    mysql-ndb-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 1 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Database changed
    mysql> create table test1 (i int) engine=ndbcluster;
    Query OK, 0 rows affected (0.94 sec)
    
    mysql> insert into test1 () values (1);
    Query OK, 1 row affected (0.02 sec)
    
    mysql> select * from test1;
    +------+
    | i    |
    +------+
    |    1 |
    +------+
    1 row in set (0.01 sec)
    

Перейдите на ноду mysql-ndb-2 и проверьте доступность данных:

    
    mysql-ndb-2# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 7 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test1;
    +------+
    | i    |
    +------+
    |    1 |
    +------+
    1 row in set (0.00 sec)
    

Если у вас все получилось, то это хороший признак, хотя стоит учесть то, что на самом то деле данные могут и не скопироваться. В очередной раз напомню, что хранилище (mysql-ndb-2) также является и API-нодой и этот тест просто показывает, что данные в кластере можно восстановить. Для более наглядной демонстрации мы воспользуемся следующим тестом.

Убейте процесс NDB (ndbd) на хранилище (mysql-ndb-2) для того, чтобы имитировать отказ одной из нод.

    
    mysql-ndb-2# ps -ef | grep [n]db
        root  3035  3034  0 17:28:41 ?   0:23 /usr/local/mysql/bin/ndbd --initial
        root  3034     1  0 17:28:41 ?   0:00 /usr/local/mysql/bin/ndbd --initial
    mysql-ndb-2# kill -TERM 3034 3035
    mysql-ndb-2# ps -ef | grep [n]db
    

Сервер управления должен обнаружить отказ хранилища mysql-ndb-2 (192.168.0.34), но связь с API должна быть.

    
    ndb_mgm> show
    Cluster Configuration
    ---------------------
    [ndbd(NDB)]     2 node(s)
    id=2    @192.168.0.33  (Version: 4.1.9, Nodegroup: 0)
    id=3 (not connected, accepting connect from 192.168.0.34)
    
    [ndb_mgmd(MGM)] 1 node(s)
    id=1    @192.168.0.32  (Version: 4.1.9)
    
    [mysqld(API)]   4 node(s)
    id=4    @192.168.0.33  (Version: 4.1.9)
    id=5    @192.168.0.34  (Version: 4.1.9)
    id=6 (not connected, accepting connect from any host)
    id=7 (not connected, accepting connect from any host)
    

В хранилище mysql-ndb-1 создайте еще одну таблицу:

    
    mysql-ndb-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 4 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> create table test2 (i int) engine=ndbcluster;
    Query OK, 0 rows affected (1.00 sec)
    
    mysql> insert into test2 () values (2);
    Query OK, 1 row affected (0.01 sec)
    
    mysql> select * from test2;
    +------+
    | i    |
    +------+
    |    2 |
    +------+
    1 row in set (0.01 sec)
    

Перейдем на ноду mysql-ndb-2 и выполним следущую команду:

    
    mysql-ndb-2# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 9 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test2;
    +------+
    | i    |
    +------+
    |    2 |
    +------+
    1 row in set (0.01 sec)
    

Хранилище и сервер API являются независимыми приложениями, поэтому как только сервис хранилища ndbd будет запущен, данные будут среплицированы, что и будет продемонстрировано в следующем тесте.

Сперва перезапустите хранилище mysql-ndb-2:

    
    mysql-ndb-2# /usr/local/mysql/bin/ndbd
    

Затем, останавливаем хранилище на mysql-ndb-1, используя консоль управления или команду kill:

    
    mgmt# ndb_mgm
    ndb_mgm> show
    Cluster Configuration
    ---------------------
    [ndbd(NDB)]     2 node(s)
    id=2    @192.168.0.33  (Version: 4.1.9, Nodegroup: 0, Master)
    id=3    @192.168.0.34  (Version: 4.1.9, Nodegroup: 0)
    
    [ndb_mgmd(MGM)] 1 node(s)
    id=1    @192.168.0.32  (Version: 4.1.9)
    
    [mysqld(API)]   4 node(s)
    id=4    @192.168.0.33  (Version: 4.1.9)
    id=5    @192.168.0.34  (Version: 4.1.9)
    id=6 (not connected, accepting connect from any host)
    id=7 (not connected, accepting connect from any host)
    
    ndb_mgm> 2 stop
    Node 2 has shutdown.
    

После того, как хранилище на mysql-ndb-2 было перезапущено, необходимо убедиться в репликации данных:

    
    mysql-ndb-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 5 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test2;
    +------+
    | i    |
    +------+
    |    2 |
    +------+
    1 row in set (0.01 sec)
    
    mysql-ndb-2# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 10 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test2;
    +------+
    | i    |
    +------+
    |    2 |
    +------+
    1 row in set (0.01 sec)
    

Тем самым мы убедились в репликации данных между хранилищами. Запускаем хранилище mysql-ndb-1:

    
    mysql-ndb-1# /usr/local/mysql/bin/ndbd
    

Шаг 6: Теперь мы добавим в кластер ноду API. Она является полноценным членом кластера, за исключением того, что на ней не запущен движок хранилища NDB. Данные на эту ноду не реплицируются и она выполняет только “клиентские” функции. Как правило, на такие ноды устанавливаются приложения, требующие для своей работы MySQL. Приложения обращается к серверу MySQL на localhost, а он, в свою очередь, обращается за данными к кластеру.

Сперва установим сервер MySQL для API ноды mysql-api-1 (192.168.0.35):

    
    mysql-api-1# groupadd mysql
    mysql-api-1# useradd -g mysql mysql
    mysql-api-1# cd /usr/local
    mysql-api-1# wget http://dev.mysql.com/get/Downloads/MySQL-4.1/
    mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz/from/http://mysql.he.net/
    mysql-api-1# gzip -dc mysql-max-4.1.9-sun-solaris2.8-sparc.tar.gz | tar xvf -
    mysql-api-1# ln -s mysql-max-4.1.9-sun-solaris2.8-sparc mysql
    mysql-api-1# cd mysql
    mysql-api-1# scripts/mysql_install_db --user=mysql
    mysql-api-1# chown -R root  .
    mysql-api-1# chown -R mysql data
    mysql-api-1# chgrp -R mysql .
    mysql-api-1# cp support-files/mysql.server /etc/init.d/mysql.server
    

Устанавливаем простой файл конфигурации /etc/my.cnf:

    
    [mysqld]
    ndbcluster
    ndb-connectstring='host=192.168.0.32'    # IP address of the management server
    [mysql_cluster]
    ndb-connectstring='host=192.168.0.32'    # IP address of the management server
    

Запускаем сервер MySQL:

    
    mysql-api-1# /etc/init.d/mysql.server start
    

Выполним несколько запросов к таблицам, которые мы создали ранее:

    
    mysql-api-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 1 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> create database foo;
    Query OK, 1 row affected (0.11 sec)
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test1;
    +------+
    | i    |
    +------+
    |    1 |
    +------+
    1 row in set (0.01 sec)
    
    mysql> select * from test2;
    +------+
    | i    |
    +------+
    |    2 |
    +------+
    1 row in set (0.01 sec)
    

С помощью консоли управления убедимся, что API нода теперь доступна:

    
    ndb_mgm> show
    Cluster Configuration
    ---------------------
    [ndbd(NDB)]     2 node(s)
    id=2    @192.168.0.33  (Version: 4.1.9, Nodegroup: 0)
    id=3    @192.168.0.34  (Version: 4.1.9, Nodegroup: 0, Master)
    
    [ndb_mgmd(MGM)] 1 node(s)
    id=1    @192.168.0.32  (Version: 4.1.9)
    
    [mysqld(API)]   4 node(s)
    id=4   (Version: 4.1.9)
    id=5   (Version: 4.1.9)
    id=6    @192.168.0.35  (Version: 4.1.9)
    id=7 (not connected, accepting connect from any host)
    

Теперь наша конфигурация похожа на диаграмму, представленую в верхней части статьи.

Шаг 7: Теперь мы готовы проверить отказоустойчивость кластера, обслуживая запросы с API ноды:

С помошью сервера API ноды создадим тестовую базу данных и наполним ее неким содержимым:

    
    mysql-api-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 258519 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> create table test3 (i int) engine=ndbcluster;
    Query OK, 0 rows affected (0.81 sec)
    
    mysql> quit
    Bye
    
    

Вставим случайные данные в таблицу, руками или используя этот короткий сценарий:

    
    #!/bin/sh
    for i in 1 2 3 4 5 6 7 8 9 10
    do
            random=`perl -e "print int(rand(100));"`
            echo "use foo; insert into test3 () values ($random);" | mysql -u root
    done
    

Выполняем запросы:

    
    mysql-api-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 258551 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test3;
    +------+
    | i    |
    +------+
    |   92 |
    |   20 |
    |   18 |
    |   84 |
    |   49 |
    |   22 |
    |   54 |
    |   91 |
    |   79 |
    |   52 |
    +------+
    10 rows in set (0.02 sec)
    

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

    
    ndb_mgm> show
    Cluster Configuration
    ---------------------
    [ndbd(NDB)]     2 node(s)
    id=2 (not connected, accepting connect from 192.168.0.33)
    id=3    @192.168.0.34  (Version: 4.1.9, Nodegroup: 0)
    
    [ndb_mgmd(MGM)] 1 node(s)
    id=1    @192.168.0.32  (Version: 4.1.9)
    
    [mysqld(API)]   4 node(s)
    id=4 (not connected, accepting connect from any host)
    id=5   (Version: 4.1.9)
    id=6    @192.168.0.35  (Version: 4.1.9)
    id=7 (not connected, accepting connect from any host)
    

Для API ноды данные кластера все еще доступны?

    
    mysql-api-1# mysql -u root
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 258552 to server version: 4.1.9-max
    
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
    
    mysql> use foo;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from test3;
    +------+
    | i    |
    +------+
    |   54 |
    |   91 |
    |   79 |
    |   52 |
    |   92 |
    |   20 |
    |   18 |
    |   84 |
    |   49 |
    |   22 |
    +------+
    10 rows in set (0.02 sec)
    

Подключим кабель обратно. Хранилище попытается подключиться обратно в кластер но, вероятно, будет отключен сервером управления, с появлением подобной ошибки (/var/lib/mysql-cluster/mdb_2_error.log):

    
    Date/Time: Saturday 12 February 2005 - 12:46:21
    Type of error: error
    Message: Arbitrator shutdown
    Fault ID: 2305
    Problem data: Arbitrator decided to shutdown this node
    Object of reference: QMGR (Line: 3796) 0x0000000a
    ProgramName: /usr/local/mysql/bin/ndbd
    ProcessID: 1185
    TraceFile: /var/lib/mysql-cluster/ndb_2_trace.log.3
    ***EOM***
    

Перезапускаем процесс ndb и видим, как нода присоединяется к кластеру:

    
    mysql-ndb-1# /usr/local/mysql/bin/ndbd
    
    ndb_mgm> show
    Cluster Configuration
    ---------------------
    [ndbd(NDB)]     2 node(s)
    id=2    @192.168.0.33  (Version: 4.1.9, Nodegroup: 0)
    id=3    @192.168.0.34  (Version: 4.1.9, Nodegroup: 0, Master)
    
    [ndb_mgmd(MGM)] 1 node(s)
    id=1    @192.168.0.32  (Version: 4.1.9)
    
    [mysqld(API)]   4 node(s)
    id=4   (Version: 4.1.9)
    id=5   (Version: 4.1.9)
    id=6    @192.168.0.35  (Version: 4.1.9)
    id=7 (not connected, accepting connect from any host
    

Разное

  • Помните, что данные SQL, сохраненные (реплицированные) в кластере, таблицы баз данных должны быть созданы, используя engine=NDBCLUSTER (как показано в примерах выше). Возможно использовать этот механизм, чтобы определить различные типы хранения для таблиц в пределах одной базы данных, в зависимости от индивидуальной работы и требований надежности. Некритичные таблицы баз данных могут не храниться в кластере.
  • Можно сделать тип хранения NDBCLUSTER используемым по умолчанию с помощью записи в /etc/my.cnf:
      
      [mysqld]
      default-table-type=NDBCLUSTER
      
  • Иногда, после аварийного завершения узла кластера (например, аварийный отказ или отключение питания) мы видим “зависшие” подключения, и после рестарта отказавший узел неспособен присоединиться к кластеру. В этом случае, сеанс должен быть вручную удален с консоли управления, используя команду “purge stale sessions”:
      
      ndb_mgm> purge stale sessions
      Purged sessions with node id's: 3
      ndb_mgm>
      

Перевод: Михаил Сгибнев (www.dreamcatcher.ru)


August 12th, 2010  
Tags: Cluster, MySQL, NDB



Некоторые недокументированные функции Java

Java 0 Comment »

Даниил Алиевский

Изучение недокументированных функций в применении к языку Java может показаться несколько странным. Java – грамотный, современный, высоконадежный объектно-ориентированный язык программирования, поставляемый фирмой Sun совместно с обширнейшими библиотеками готовых классов. Неужели в среде Java существуют задачи, которые не решаются с помощью стандартных библиотек и для решения которых имеет смысл прибегать к недокументированным функциям? И что вообще такое «недокументированная функция» в рамках Java?

Слова «недокументированная функция» в случае Java имеют самый прямой смысл. В стандартный комплект поставки языка Java (мы рассматриваем версии 1.4 и выше – Sun Java SDK) входит, помимо документированных пакетов типа java.lang.*, java.util.*, javax.swing.* и т. д. также целый ряд недокументированных пакетов, прежде всего подпакеты sun.* и com.sun.*. Фирма Sun совершенно справедливо рекомендует не пользоваться классами из этих пакетов. Фирма Sun оставляет за собой право в любой момент поменять поведение и даже сам набор этих классов, так что программа, пользующаяся ими, рискует оказаться несовместимой с будущими версиями Java.

На самом деле в подавляющем большинстве ситуаций недокументированные функции, точнее, недокументированные классы из подпакетов sun.* и com.sun.* действительно не нужны. Эти классы в основном обеспечивают низкоуровневую реализацию универсальных классов и интерфейсов, предназначенных для использования в прикладных программах, и почти ничего не добавляют к тем возможностям, которые и так предоставляются стандартными документированными прикладными пакетами.

Из этого правила иногда встречаются исключения.

Автор этой статьи участвовал в разработке сложной системы на языке Java, содержащей более 100 тысяч строк исходного кода. При этом нам лишь несколько раз потребовались недокументированные функции Java. Именно о таких исключениях мне хотелось бы рассказать в данной статье.

Встроенный компилятор JAVA

Наверное, наиболее популярная «недокументированная функция» Java – это компиляция исходных текстов Java в .class-файлы.

Стандартный компилятор javac, входящий в комплект поставки Sun Java SDK, вполне последовательно реализован фирмой Sun на том же самом языке Java в виде сложной иерархии Java-классов. Утилита javac не более чем тривиальная программа, реализованная в машинном коде для всех операционных систем, предназначенная для запуска виртуальной машины Java и обращения к Java-классу, выполняющему собственно компиляцию.

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

На самом деле тут существуют даже два решения – классы sun.tools.javac.Main и com.sun.tools.javac.Main. Оба этих класса находятся в JAR-файле tools.jar, входящем в комплект поставки Sun Java SDK. Этот класс по умолчанию не входит в так называемый Java Runtime Environment (JRE) – набор классов и подкаталогов, который разрешается свободно распространять совместно с вашим Java-приложением для обеспечения его корректного исполнения. Тем не менее лицензионное соглашение фирмы Sun специально разрешает распространять tools.jar в дополнение к JRE совместно с вашими приложениями.

Оба класса – sun.tools.javac.Main и com.sun.tools.ja-vac.Main – хотя и не документированы, но довольно активно обсуждаются на форумах сайта http://java.sun.com.

Первый класс, sun.tools.javac.Main, имеет конструктор:

public Main(

OutputStream p0,

String p1)

В качестве параметров конструктор принимает некоторый поток p0, в который будут выдаваться все сообщения компилятора, и некоторую строку неизвестного (автору статьи) назначения; известные мне примеры использования данного класса передавали в качестве p1 строку «javac».

Собственно компиляцию выполняет метод того же класса:

public synchronized boolean compile(

String[] p0)

Этому методу нужно передать в качестве аргумента массив строк-параметров, которые обычно передаются утилите javac, например:

new String[] {“-d”,”/путь_к_подкаталогу”,”myfile.java”}

О результатах компиляции можно узнать из результата метода compile() – в случае успеха он должен вернуть true – или с помощью отдельного метода:

public int getExitStatus()

который в случае успеха должен вернуть 0.

Самая ценная особенность класса sun.tools.javac.Main – возможность указать в конструкторе выходной поток, который будет использоваться для выдачи сообщений об ошибках компилятора. Это дает возможность легко преобразовать этот поток, скажем, в переменную типа String, с тем чтобы самостоятельно ее проанализировать и показать пользователю.

Начиная с версии Sun Java SDK 1.4, объявлен устаревшим класс sun.tools.javac.Main. Попытка явно обратиться к этому классу в программе выдает предупреждение «deprecation warning», а попытка им воспользоваться для компиляции любого класса выдает аналогичное предупреждение в поток ошибок (параметр конструктора p0), если только явно не подавить эти предупреждения ключом «-nowarn» среди параметров метода compile.

Для такого предупреждения есть основания. По крайней мере один из сложных классов, разработанных автором на Java версии 1.4 и прекрасно компилирующихся обычными компиляторами, оказалось невозможным скомпилировать с помощью класса sun.tools.ja-vac.Main. Компилятор вполне явно «сошел с ума» и стал «ругаться» на законные языковые конструкции.

Второй класс, предназначенный для компиляции исходных текстов на Java – единственный «неустаревший» в версии Sun Java SDK 1.4 – это com.sun.tools.ja-vac.Main. Функционально он эквивалентен классу sun.tools.ja-vac.Main, но несколько менее «многословен» в своем наборе методов. Конструктор этого класса не имеет параметров. Методов compile здесь два, причем несинхронизированных, в отличие от sun.tools.javac.Main:

public static int compile(String[] p0)

public static int compile(String[] p0,

PrintWriter p1)

Второй из этих методов позволяет указать поток вывода, который будет использоваться для вывода всех сообщений компилятора, например:

new PrintWriter(writer=new CharArrayWriter(),true)

Как и в случае sun.tools.javac.Main, второй метод compile позволяет перенаправить поток сообщений компилятора в собственный буфер, с тем чтобы впоследствии превратить его, скажем, в строку типа String для анализа и визуализации.

Класс com.sun.tools.javac.Main в версии Sun Java SDK 1.4 работает существенно быстрее предыдущего класса sun.tools.javac.Main и «справляется» со всеми корректными исходными текстами. Похоже, именно этот класс вызывается изнутри стандартной утилиты javac.

Файлы-ссылки – *.lnk в Microsoft Windows

Второй известный автору случай использования недокументированных функций – распознавание файлов-«ярлыков» (shortcuts) Microsoft Windows, обычно имеющих расширение «.lnk».

Стандартные средства работы с файлами из пакета java.io.* вообще не слишком хорошо «справляются» с файловой системой современных версий Microsoft Windows. Так, стандартный класс java.io.File «понятия не имеет» о том, что корнем файловой иерархии Windows следует считать «Рабочий стол» («Desktop»), у которого есть такие дочерние узлы, как «Мои документы» («My documents») или «Мой компьютер» («My computer»). Стандартный java.io.File по старинке считает корнем иерархии корневой каталог любого дискового устройства.

Чтобы скомпенсировать этот недостаток, не нарушая совместимости с классом File, фирма Sun разработала новый, более современный класс javax.swing.file-chooser.FileSystemView. Он активнейшим образом используется стандартным диалогом выбора файла javax.swing.JFileChooser, что отчасти объясняет несколько странный выбор пакета для FileSystemView.

К сожалению, даже класс FileSystemView не решает всех проблем, по крайней мере, в имеющейся у меня последней версии Sun Java SDK 1.4.1. Современные версии Windows, в частности Windows XP, предлагают пользователю интерфейс, существенно опирающийся на механизм файлов-«ссылок», или «ярлыков». Такими ссылками являются специальные файлы или даже подкаталоги (обычно, но не обязательно с расширением «.lnk»). Щелчок по ним в стандартном Windows Explorer приводит к перемещению в некоторый другой каталог или открытию некоторого другого файла. Именно так в Windows XP организована работа в локальной сети – компьютеры пользователей-«соседей» представлены маленькими виртуальными подкаталогами-«ссылками» в локальной файловой системе текущего пользователя.

Класс FileSystemView не содержит никаких средств для распознавания и обработки подобных ссылок. В результате стандартный диалог выбора файла javax.swing.JFileChooser при использовании в Windows XP производит довольно жалкое впечатление – попытка перейти к компьютерам локальной сети заканчивается позорной неудачей.

В действительности фирма Sun уже реализовала механизм обработки файлов-«ссылок» Microsoft Windows. К сожалению, он пока недокументирован. Это класс sun.awt.shell.ShellFolder. Среди прочих методов, имеющих документированные эквиваленты в классе FileSystemView, класс ShellFolder содержит следующие два метода:

public abstract boolean isLink();

public abstract ShellFolder getLinkLocation()

throws FileNotFoundException

Вот как можно ими пользоваться:

public static boolean isLink(File f) {

try {

return sun.awt.shell.ShellFolder

.getShellFolder(f).isLink();

} catch (FileNotFoundException e) {

return false;

}

}

public static File getLinkLocation(File f)

throws FileNotFoundException

{

File result= sun.awt.shell.ShellFolder

.getShellFolder(f).getLinkLocation();

if (result==null ||

result.getPath().trim().length()==0)

throw new FileNotFoundException(

“Incorrect link – it is empty”);

return result;

}

Применяя эти методы, при желании можно «исправить» поведение стандартного диалога выбора файла javax.swing.JFileChooser, «научив» его правильно работать с современными локальными сетями Microsoft Windows.

Конечно, будет гораздо лучше, если фирма Sun в очередной версии Java SDK включит в FileSystemView документированные эквиваленты этих методов и исправит javax.swing.JFileChooser. Существование подобных методов в sun.awt.shell.ShellFolder позволяет на это надеяться. Пока же приходится пользоваться недокументированными методами.

Стек вызовов метода

Предположим, нужно получить «трассу стека» – узнать, какой метод исполняется в данный момент, какой метод вызвал этот метод, и т. д. Трудно представить, зачем это может понадобиться, за исключением отладки программы. Однако в реальной практике автору это однажды понадобилось – чтобы выяснить, какие загрузчики классов использовались для загрузки текущего исполняемого кода и всех классов «трассы стека», вызвавших этот код.

Даже для этой столь экзотической ситуации фирма Sun предусмотрела документированную технику. Достаточно возбудить фиктивное исключение, тут же «поймать» его и воспользоваться методом объекта-исключения getStackTrace().

Проблема может заключаться в том, что метод getStackTrace() возвращает исключительно «описательную» информацию о классах и методах «трассы стека» – попросту строковые имена классов и методов. Чтобы получить собственно классы, задействованные в данный момент в стеке (объекты типа Class), необходимо вызвать метод Class.forName или эквивалентный. Но что, если текущий загрузчик классов не в состоянии загрузить такие классы просто по имени? Что, если текущий исполняемый код загружен самым обычным традиционным загрузчиком классов, а класс, который его вызвал – это интернетовский class-файл, загруженный и исполняемый совершенно другим специальным загрузчиком? Тогда метод getStackTrace() никак не поможет «добраться» до этого класса (объекта Class) хотя бы для того, чтобы получить ссылку на загрузивший его загрузчик классов.

В подобной ситуации автору пришлось прибегнуть к недокументированному классу sun.reflect.Reflection, точнее, к его методу.

public static native Class getCallerClass(int p0)

Вот как выглядит использование этого метода:

public static Class[] getCurrentStackTraceClasses() {

List result= new ArrayList();

for (int count=1; count<10000

/* страховка на всякий случай */;

count++)

{

Class clazz= sun.reflect.Reflection

.getCallerClass(count);

if (clazz==null) break;

result.add(clazz);

}

return (Class[])result.toArray(

new Class[0]);

}

Провайдеры сервисов

Сервис-провайдеры (Service provider) – довольно распространенная и неплохо документированная техника среди стандартных библиотек Java. Тем более удивительно, что фирма Sun оставила недокументированным механизм перечисления сервис-провайдеров – класс sun.misc.Service.

Идея сервис-провайдеров вполне очевидна. Допустим, есть некоторый сервис (интерфейс или абстракный класс), предназначенный для использования прикладными программами. Есть также произвольное количество провайдеров – классов, реализующих этот интерфейс или абстрактный класс. Предполагается, что набор провайдеров не фиксирован и может меняться в зависимости от поставки программы. Не исключено, что пользователь может самостоятельно приобрести и инсталлировать в систему дополнительный набор провайдеров какого-либо стандартного сервиса.

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

Согласно документации, для поддержки сервис-провайдеров нужно положить в подкаталог META-INF/Services некоторого JAR-файла, присутствующего в путях поиска классов Java, специальный текстовый файл. Имя текстового файла должно совпадать с полным именем некоторого сервиса – интерфейса или абстрактного класса, например, «com.pupkin.vasya.My-Service». Этот файл должен содержать (в отдельных строках) список некоторых классов – провайдеров этого сервиса, присутствующих в данном JAR-файле. Тогда система Java сумеет стандартным образом получить этот список в виде набора объектов – экземпляров соответствующих провайдеров. Стандартные библиотеки фирмы Sun примерно так и поступают, когда нужно прочитать изображение или аудиозапись. Почему-то остался недокументированным лишь тот самый стандартный способ, которым извлекается список провайдеров некоторого сервиса.

Этот способ следующий:

for (Iterator iterator=

sun.misc.Service.providers(

класс_сервиса.class);

iterator.hasNext(); )

{

класс_сервиса o= (класс_сервиса)

iterator.next();

используем o – экземпляр

очередного провайдера сервиса;

}

На самом деле для использования такой техники не обязательно создавать JAR-файл – вполне достаточно разместить правильный подкаталог META-INF в одном из каталогов поиска class-файлов.

http://www.samag.ru/cgi-bin/go.pl?q=articles;n=03.2003;a=08


July 27th, 2010  
Tags: Java



Java: магия отражений

Java 0 Comment »

Часть III. Компиляция Java средствами Java

Даниил Алиевский

В предыдущих частях статьи мы познакомились с технологией отражений (Java Reflection). Это мощнейший механизм Java, позволяющий делать с .class-файлами практически все что угодно – загружать из произвольных файлов, анализировать набор членов класса, обращаться к этим членам, при необходимости обходя стандартную защиту «private»/«protected». При желании можно даже подменить стандартный механизм загрузки Java-классов и взять этот процесс под полный контроль, например, разрешить перезагружать изменившиеся версии .class-файлов без полной перезагрузки Java-машины (эта техника подробно рассматривалась в части II, см. №1(2) журнала «Системный администратор»).

В этой части статьи мы научимся компилировать Java-код в .class-файлы. Совместно с технологией отражений это позволит в процессе исполнения программы «на лету» создавать новые классы в виде исходного текста, компилировать их, загружать и использовать. Столь мощные возможности обычно присущи лишь чисто интерпретируемым, сравнительно медленным языкам типа JavaScript или Perl или Ассемблеру (точнее, машинному языку).

Всюду далее, если не оговорено обратное, мы будем подразумевать последнюю (на момент написания статьи) версию Java фирмы Sun: Sun Java SDK 1.4.

Как скомпилировать Java-файл с исходным текстом

Решение, вообще говоря, совершенно банально – вызвать стандартный компилятор javac!

На первый взгляд подобное решение кажется неизящным, сопряженным с массой проблем. Например, компилятор придется поставлять всем вашим пользователям. Другой пример – интеграция компилятора с вашей системой: сообщения компилятора должны выдаваться не на системную консоль, а «внутрь» вызывающей программы для обработки и визуализации в рамках вашего пользовательского интерфейса.

Разработчики Java позаботились о том, чтобы эти проблемы легко решались.

Прежде всего, стандартный компилятор javac входит в комплект поставки Sun Java SDK, распространяемого совершенно бесплатно. Правда, здесь есть одна тонкость.

При формировании дистрибутива Java-приложения обычно принято включать в этот дистрибутив некий фрагмент Java SDK, так называемый JRE (Java2 TM Runtime Environment) – набор файлов, достаточный для запуска Java-приложения. В комплекте Sun Java SDK этот набор оформлен в виде подкаталога jre/. По умолчанию JRE не содержит компилятора javac (и ряда других полезных утилит из Java SDK). Включать в дистрибутив полный пакет Java SDK запрещено лицензионным соглашением фирмы Sun.

Однако в том же лицензионном соглашении специально оговорено, что компилятор байт-кода javac вместе с необходимым вспомогательным JAR-файлом tools.jar можно включать в дистрибутив в дополнение к стандартному JRE, точнее, распространять совместно с Java-приложением. См. файл jre/license в комплекте поставки Sun Java SDK 1.4.1, раздел «JavaTM 2 runtime environment (j2re), standard edition, version 1.4.1_x supplemental license terms», пункт 3 и файл jre/README.txt, раздел «Redistribution of Java 2 SDK Files».

Но самое приятное заключается в том, что в действительности компилятор фирмы Sun реализован на том же языке Java – в виде класса com.sun.tools.javac.Main и пакета вспомогательных классов, размещенных в архиве tools.jar. Утилита javac является всего-навсего «оболочкой», стартующей виртуальную машину Java и запускающей указанный класс. Архив tools.jar, как и саму утилиту javac, разрешается свободно распространять (в дополнение к стандартному JRE) совместно с Java-приложением.

Это означает, что для компиляции Java-класса из Java-приложения нет необходимости обращаться к внешней утилите javac средствами операционной системы (методами Runtime.getRuntime().exec(…)). Можно напрямую воспользоваться классом com.sun.tools.javac.Main.

Использование класса com.sun.tools.javac.Main предельно просто. Вот полный интерфейс этого класса (конструктор и public-методы):

// Constructors

public Main()

// Methods

public static void main(String[] p0)

public static int compile(String[] p0)

public static int compile(String[] p0, PrintWriter p1)

Для вызова компилятора нужно обратиться к к одному из двух его static-методов compile.

В качестве аргумента p0 нужно передать массив строк-параметров, которые обычно передаются утилите javac, например:

new String[] {

“-d”,

“/путь_к_подкаталогу”,

“myfile.java”

}

В качестве аргумента p1 во второй из этих методов нужно передать поток вывода, который будет использоваться для вывода всех сообщений компилятора. Таким образом можно «перехватить» все сообщения компилятора и перенаправить их в свой собственный буфер, который впоследствии можно проанализировать или показать пользователю.

В качестве результата оба метода возвращают стандартный код возврата утилиты javac (errorlevel в терминах MS-DOS). 0 сигнализирует об успешном завершении, другие значения – о каких-либо ошибках.

Приведем пример вызова компилятора путем обращения к классу com.sun.tools.javac.Main:

String[] args= параметры утилиты javac;

CharArrayWriter writer= new CharArrayWriter();

int result= com.sun.tools.javac.Main.compile(

args,

new PrintWriter(writer,true));

if (result!=0) {

/* – произошла какая-то ошибка */

анализируем и, возможно, показываем пользователю строку сообщений компилятора writer.toString()

}

Описанное решение очень просто, удобно и эффективно. Но у него есть серьезный недостаток.

Класс com.sun.tools.javac.Main, как и все классы из пакетов com.sun.* и sun.*, является недокументированным. Приведенное выше описание использования класса опирается на здравый смысл и эксперименты, а не на официальную документацию фирмы Sun. Фирма Sun имеет полное право в очередной версии Java SDK изменить поведение или интерфейс этого класса или даже вообще исключить его.

Указанная проблема не надумана.

Действительно, все сказанное выше справедливо лишь для версии Sun Java SDK 1.4. В предыдущей версии, Sun Java SDK 1.3, тот же самый класс com.sun.tools.javac.Main имел совершенно другой интерфейс:

// Constructors

public Main()

// Methods

public static void main(String[] p0)

public int compile(String[] p0)

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

В версии Java SDK 1.3 для компиляции исходных текстов Java обычно использовался другой класс, sun.tools.javac.Main (расположенный все в том же архиве tools.jar). Этот класс также позволяет указать свой поток для сообщений компилятора. Причем этот класс полностью сохранил свой интерфейс при переходе от версии SDK 1.3 к SDK 1.4.

Класс sun.tools.javac.Main имеет нестатический синхронизованный метод compile:

public synchronized boolean compile(String[] p0)

Поток для сообщений компилятора указывается в качестве первого параметра конструктора:

public Main(OutputStream p0, String p1)

(не PrintWriter, а более «архаичный» OutputStream). Смысл второго параметра конструктора я так и не выяснил, но найденные мной в Интернете примеры использования данного класса передавали в качестве p1 строку «javac».

Код возврата утилиты javac возвращается отдельным методом:

public int getExitStatus()

Факт успешности компиляции можно также узнать по boolean-результату метода compile (false означает неудачу).

Но и этот класс, несмотря на сохранение интерфейса, в действительности изменил свое поведение при переходе от версии SDK 1.3 к SDK 1.4.

Во-первых, он был объявлен как устаревший («deprecated»). Конечно, предупреждение компилятора, появляющееся при попытке использовать данный класс («warning: sun.tools.javac.Main in sun.tools.javac has been deprecated»), можно и проигнорировать. Но фирма Sun почему-то решила добавлять аналогичное предупреждение в любое сообщение, выдаваемое самим компилятором sun.tools.javac.Main! Если попытаться использовать sun.tools.javac.Main для компиляции любого, даже совершенно корректного Java-файла, в любом случае будет выдано предупреждение «sun.tools.javac.Main has been deprecated». Чтобы избавиться от него, sun.tools.javac.Main придется использовать с ключом «-nowarn», но тогда вообще теряется возможность получать и анализировать предупреждения компилятора.

Во-вторых, в версии Sun Java SDK 1.4 компилятор sun.tools.javac.Main попросту не всегда адекватно работает. На простых тестах это трудно обнаружить. Но когда я попытался скомпилировать с помощью этого компилятора все исходные тексты большого Java-проекта, обнаружилось, что некоторые сложные, но вполне корректные классы, прекрасно компилируемые «штатными» компиляторами и классом com.sun.tools.javac.Main, не компилируются с помощью sun.tools.javac.Main. В частности, компилятор sun.tools.javac.Main «сломался» на некоторых нетривиальных случаях перегрузки методов с аргументами примитивных типов, а также при попытке объявить метод toString() у некоторого вложенного класса. Это очень похоже на внутреннюю ошибку компилятора, которую фирма Sun не сочла нужным исправлять в устаревшем наборе классов.

Есть также мелкие отличия в самом синтаксисе языка Java, понимаемом компиляторами sun.tools.javac.Main и стандартным com.sun.tools.javac.Main. (Ошибка, о которой ранее шла речь, не связана с этими мелкими отличиями – там действительно имела место явная ошибка компилятора.) Например, sun.tools.javac.Main разрешает импортировать (предложением import) конкретные классы, расположенные в корневом пакете – т.е. непосредственно в корне одного из каталогов, перечисленных в путях поиска CLASSPATH. Стандартный компилятор в современных версиях Java не допускает такого экзотического импорта – все импортируемые классы должны лежать внутри какого-либо пакета.

Также можно заметить, что в версии Sun Java SDK 1.4 компилятор sun.tools.javac.Main работает примерно вдвое медленнее, чем com.sun.tools.javac.Main.

Все сказанное означает, что использование для компиляции Java-файлов конкретных классов типа com.sun.tools.javac.Main или sun.tools.javac.Main – рискованное занятие. Соответствующий код придется заново тестировать при выпуске каждой новой версии Java SDK и, возможно, в какой-то момент его придется радикально переписывать.

В случае com.sun.tools.javac.Main лично мне риск не кажется слишком большим. Похоже, что в этом классе фирма Sun наконец «довела до ума» решения, существовавшие в предыдущих версиях Java в этом же классе и в sun.tools.javac.Main. Трудно представить, чтобы возможности класса com.sun.tools.javac.Main в какой-то версии исчезли или радикально поменялись. Скорее всего, этот класс либо сохранится, либо превратится в легальный документированный класс, например, в пакете java.*, тогда необходимые изменения будут минимальны.

Если необходимо надежное документированное решение, то на сегодня единственный доступный вариант – вызвать внешнюю утилиту javac одним из методов Runtime.getRuntime().exec(…).

Для этого, правда, эту утилиту нужно еще найти в файловой системе. Если данная утилита входит в состав дистрибутива и инсталлируется вместе с Java-приложением, то инсталлятор может сам позаботиться о том, чтобы полный путь к утилите javac передавался в Java-программу. Если же приложение дожно работать под управлением «чужого» Java SDK, инсталлируемого пользователем независимо от приложения, то можно поискать файлы:

n  bin/javac.exe (случай Microsoft Windows);

n  bin/javac (случай Unix/Linux);

n  bin/sparcv9/javac (случай Solaris SPARC)

в каталоге System.getProperty(«java.home») и содержащем его каталоге. (Чаще всего System.getProperty(«java.home») соответствует подкаталогу jre/ в главном каталоге Sun Java SDK. Соответственно, утилита javac расположена в подкаталоге bin/ содержащего его каталога.)

Для получения сообщений компилятора в данном случае можно использовать стандартную технику – чтение из потоков, возвращаемых методами getInputStream() и getErrorStream() объекта Process, полученного в результате обращения к методу Runtime.getRuntime().exec(…).

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

Process p= Runtime.getRuntime().exec(массив_аргументов);

/* первый элемент в массиве должен содержать полное имя файла утилиты javac, остальные элементы – параметры этой утилиты */

final InputStreamReader is=new InputStreamReader(p.getInputStream());

final InputStreamReader es=new InputStreamReader(p.getErrorStream());

final StringBuffer out= new StringBuffer();

final StringBuffer err= new StringBuffer();

new Thread() {

public void run() {

try {

char[] buf= new char[32768];

int len;

while ((len=is.read(buf,0,buf.length))>=0) {

out.append(buf,0,len);

}

} catch (Exception e) {

e.printStackTrace();

}

}

}.start();

new Thread() {

public void run() {

… (аналогичный цикл для es и err)

}

}.start();

int result= p.waitFor();

/* дожидаемся завершения утилиты javac и получаем ее код завершения */

if (result!=0) {

/* – произошла какая-то ошибка */

анализируем и, возможно, показываем пользователю сообщения компилятора out и err

}

Обратите внимание: чтение потоков вывода и ошибок в отдельных параллельных потоках совершенно необходимо. Если этого не сделать, то вызов метода waitFor() может привести к зависанию в случае, когда внешняя программа выводит хоть что-нибудь в свои потоки вывода и ошибок.

Как скомпилировать исходный текст Java, заданный в виде строки

Итак, мы научились вызывать компилятор Java. Таким образом можно скомпилировать любой Java-файл – достаточно следовать инструкциям по использованию компилятора javac. Но что делать, если у нас нет готового Java-файла, размещенного где-то в файловой системе? Допустим, мы располагаем просто исходным текстом Java-программы в виде строки типа String – загруженным, скажем, из базы данных, или сгенерированным автоматически. Как скомпилировать такой текст?

Очевидно, нужно создать некоторый временный файл с расширением .java, записать туда исходный текст, после чего вызвать компилятор. Эти действия далеко не так просты, как кажется на первый взгляд. Рассмотрим это подробнее.

В стандартных библиотеках Java есть средства для создания временных файлов – это 2 статических метода createTempFile класса File, создающие файл с уникальным «случайным» именем. К сожалению, воспользоваться ими в данном случае невозможно. Новый Java-файл, создаваемый для сохранения заданного исходного Java-кода, не может иметь произвольное имя – его имя обязательно должно совпадать с именем public-класса, объявленного в этом исходном коде (если, конечно, таковой имеется). Кроме того, нужен какой-то каталог, куда будут записаны .class-файлы, полученные в результате компиляции.

Все это означает, что нужна специальная функция, создающая временный подкаталог – так же, как File.createTempFile создает временный файл. Внутри этого подкаталога можно создать серию вложенных каталогов, соответствующую пакету, в котором должен располагаться компилируемый класс. Затем в самый внутренний каталог нужно записать Java-файл с исходным текстом, присвоив этому файлу имя, соответствующее имени компилируемого класса. Эту же структуру каталогов можно использовать для размещения результирующих .class-файлов, передав соответствующие инструкции компилятору javac.

Написать функцию создания временного каталога не очень сложно. Достаточно использовать в качестве образца реализацию File.createTempFile в исходном тексте класса java.io.File. Основная идея – циклически генерировать более или менее случайные имена подкаталогов внутри каталога временных файлов операционной системы, для каждого подкаталога пытаться его создать (методом File.mkdir) и выйти из цикла, как только очередная попытка будет удачной (mkdir вернет true). Цикл генерации имен и создания подкаталога нужно синхронизовать относительно какого-либо глобального объекта точно так же, как это сделано в методе File.createTempFile. Чтобы найти умолчательный каталог временных файлов операционной системы, можно обратиться к переменной среды:

System.getProperty(“java.io.tmpdir”)

В версии Sun Java SDK 1.4.1 эта переменная является документированной (в отличие от Sun Java SDK 1.3).

Возникает также проблема автоматического удаления созданного временного подкаталога при выходе из Java-программы. В случае файла эта задача решалась бы методом deleteOnExit класса File. Но непустые подкаталоги этот метод удалять не умеет. Самое простое решение – написать функцию, рекурсивно удаляющую созданный временный каталог вместе со всеми файлами и подкаталогами, и вызвать ее в потоке, зарегистрированном методом Runtime.getRuntime().addShutdownHook(…).

Следующий вопрос, требующий некоторого внимания – как правильно записать Java-файл с исходным текстом. Исходный текст Java (представленный по условию в виде строки String) может содержать произвольные символы Unicode, а компилятор javac обычно используется с ASCII-файлами.

Здесь есть два решения. Во-первых, начиная с версии Sun Java SDK 1.4, компилятор javac «понимает» дополнительный параметр «-encoding». Можно, например, сохранить файл в кодировке «UTF-8» и указать такую же кодировку в качестве параметра «-encoding». Чтобы сохранить текст в файле с заданной кодировкой, используется объект java.io.Writer, создаваемый вызовом:

OutputStreamWriter writer= new OutputStreamWriter(new FileOutputStream(file),encoding)

Во-вторых, компилятор javac – и в SDK 1.4, и в предыдущих версиях – поддерживает специальный способ кодирования Unicode-символов. А именно, цепочка символов вида \uNNNN, где N – шестнадцатеричные цифры, в любом месте Java-файла воспринимается компилятором как Unicode-символ с кодом NNNN. Можно перед записью Java-файла заменить все символы с кодами і128 такими цепочками и записать полученный «чистый» ASCII-файл.

После того как исходный текст скомпилирован, если компилятор сообщил об отсутствии ошибок (нулевой код возврата) и если параметры компилятора были заданы правильно (точнее, параметр «-d»), можно ожидать, что в структуре каталогов появятся все необходимые .class-файлы. Сколько их появится, заранее сказать невозможно (без полного синтаксического анализа исходного кода). Так, каждый анонимный класс породит отдельный .class-файл. Но, по крайней мере, должен появиться .class-файл, соответствующий главному public-классу, объявленному в исходном тексте (если, конечно, исходный текст не состоял из описаний одного или нескольких не-public-классов). В качестве последней «страховки» имеет смысл проверить существование этого .class-файла – его отсутствие говорит о неверных настройках компилятора.

Затем остается только загрузить сгенерированный .class-файл. Для этого нужен собственный загрузчик классов – наследник ClassLoader, умеющий загружать классы из нестандартного каталога (в нашем случае из временного каталога, созданного внутри каталога временных файлов операционной системы). Эта задача подробно рассматривалась во второй части статьи: «ClassLoader – скрытые возможности». Здесь мы не будем на ней останавливаться.

В завершение стоит добавить, что скомпилированные и загруженные классы имеет смысл кэшировать в оперативной памяти с помощью таблицы HashMap, ставящей каждому исходному тексту в соответствие результирующий объект Class – загруженный Java-класс. Если нужно скомпилировать исходный текст, идентичный тексту, который когда-то уже компилировался, то в большинстве случаев нет смысла компилировать его повторно – достаточно извлечь готовый класс из кэша.

Eval на Java: интерпретатор формул

Теперь мы располагаем чрезвычайно мощным инструментом. Мы умеем компилировать произвольные исходные тексты Java, заданные в виде строковой переменной, и загружать получаемые при этом классы. Обычно такую технику называют самопрограммированием. Эта возможность традиционно присутствует в медленных интерпретируемых скриптовых языках типа JavaScript или Perl, но отсутствует в высокоэффективных компилируемых языках (к которым относится и Java), исключая разве что Ассемблер. Фактически, мы снабдили язык Java самопрограммированием.

Но пока что пользоваться этой техникой не очень удобно.

В скриптовых языках типа JavaScript или Perl самопрограммирование выглядит крайне просто. Например, в JavaScript можно написать так:

var a= 23;

var b= 45;

var formula= “a+b”;

var result= eval(formula);

Оператор eval исполняет переданный ему фрагмент JavaScript-кода так, как если бы он непосредственно был вставлен в общий JavaScript-код вместо самого оператора eval.

Мы попробуем реализовать аналогичную технику в рамках Java.

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

Требуется реализовать статический метод некоторого класса-библиотеки (допустим, Evaluator), имеющий следующий интерфейс:

public Object eval(String javaExpression, Object context) throws Exception

В качестве javaExpression передается произвольное выражение Java, например то же «a+b». В качестве context передается объект, предоставляющий «пространство имен». Это значит, что в выражении javaExpression должна быть возможность без дополнительных уточнений обращаться ко всем public- и, может быть, protected-членам объекта context. Так, в случае формулы «a+b» объект context должен содержать числовые (или строчные) поля a и b. В своем результате eval возвращает объект Java, получаемый в результате интерпретации выражения javaExpression.

Приведенный выше пример на JavaScript для Java выглядел бы примерно так:

public static class Context {

public int a,b;

}

…

public void НекоторыйМетод() {

…

Context c= new Context();

c.a= 23;

c.b= 45;

String formula= “new Integer(a+b)”;

Integer result= (Integer)Evaluator.eval(formula,c);

…

}

Язык Java по обыкновению создает затруднения при попытке работать с примитивными типами на общих основаниях – их приходится заменять соответствующими классами-оболочками. Специально для упрощения работы с примитивными типами имеет смысл дополнить основной метод eval версиями evalInt, evalLong, evalFloat, evaDouble, evalBoolean, возвращающими результат соответствующего примитивного типа. Тогда последние 2 строки примера выглядели бы проще:

…

String formula= “a+b”;

int result= Evaluator.evalInt(formula,c);

…

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

public class НекоторыйКласс {

…

public int a,b;

public void НекоторыйМетод() {

…

a= 23;

b= 45;

String formula= “a+b”;

int result= Evaluator.evalInt(formula,this);

…

}

}

Это практически так же удобно, как и eval в скриптовых языках.

Как решить поставленную задачу – реализовать описанные методы eval, evalInt и прочие?

Существует достаточно элементарное частичное решение.

Потребуем, чтобы все обращения к членам объекта context в формуле javaExpression производились не напрямую, а через некоторую дополнительную переменную – ссылку на объект context. Формула в этом случае приобретает примерно такой вид: «c.a+c.b» (имя ссылки «c» могло бы быть дополнительным аргументом метода eval).

В этом варианте задачу решить легко. Конструируем «на лету» текст Java-класса:

import java.io.*;

import java.util.*;

какие-нибудь еще полезные import, которые могут пригодиться внутри формулы

public class ___ExpressionNNN {

public static int ___performEval(

имя_класса_context c)

{

int returnValue= текст_формулы;

return returnValue;

}

}

Вместо NNN подставляется некоторый уникальный индекс – свой для каждого текста формулы. (Если разные формулы будут интерпретироваться с помощью разных классов, то эти классы можно будет кэшировать и не компилировать повторно – смотри о кэшировании в конце предыдущего раздела.) Вместо «имя_класса_context» подставляется context.getClass().getName(), вместо «текст_формулы» – значение javaExpression.

Тип результата int метода ___performEval и тип переменной returnValue соответствуют варианту метода evalInt. Другие варианты – eval, evalDouble и прочие – должны использовать другой тип (соответственно Object, double, и т. д.).

Имя «c» аргумента ___performEval – это имя, под которым объект context будет доступен внутри формулы. Оно может быть дополнительным аргументом методов eval, evalInt, …

Компилируем полученный текст класса и загружаем скомпилированный класс, как описано в предыдущих разделах. Затем средствами отражений вызываем его статический метод ___performEval, передавая ему в качестве аргумента наш объект context, и возвращаем полученный результат. Задача решена.

Дополнительная переменная returnValue позволяет передавать в качестве формулы javaExpression фрагменты кода, состоящие более чем из одного оператора Java. В этом случае по умолчанию результатом вызова eval окажется результат первого оператора, но всегда можно будет в последнем операторе написать что-нибудь вроде

returnValue= результат_наших_вычислений;

и тем самым вернуть другой результат.

Приведенное решение, разумеется, неизящно. В формуле приходится ссылаться на члены класса-контекста через громоздкую запись типа «c.a». Попробуем избавиться от явной ссылки «c.». Язык Java разрешает ссылаться непосредственно, без дополнительных уточнений, на члены текущего класса, его предков, члены класса, по отношению к которому текущий является вложенным, и члены предков этого класса. Если класс, которому принадлежит метод ___performEval, унаследовать от класса context.getClass().getName() или вложить в другой класс, унаследованный от context.getClass().getName(), то к членам этого предка можно будет обращаться из нашей формулы непосредственно. Метод ___performEval, разумеется, нужно будет сделать нестатическим.

Таким способом формулу типа «a+b» скомпилировать удастся – синтаксически все будет соблюдено. Но как добиться, чтобы a и b ссылались именно на члены данного экземпляра context, переданного в качестве аргумента в метод eval (или evalInt, evalDouble, …)? Все, что можно сделать «легально» для исполнения формулы – создать новый экземпляр для нашего нового класса (или по экземпляру для серии вложенных классов) и указать именно этот новый экземпляр при вызове метода ___performEval через отражения. Виртуальная машина Java не позволит «подменить» экземпляр нового временного класса объектом context, так же context является предком нашего нового объекта, и его нельзя использовать там, где декларировано использование объекта-потомка.

Приходит в голову банальное решение – перед вызовом ___performEval скопировать все поля объекта context в заново созданный экземпляр наследника context.getClass().getName(), а в конце скопировать все поля обратно (на случай, если формула их изменяла). Все это в принципе осуществимо средствами отражений, но вряд ли такое решение можно назвать качественным. Если полей много, такое копирование может занять много времени. Кроме того, в сложных случаях такое поведение попросту может оказаться ошибочным. Представьте себе, что некоторый внешний поток постоянно наблюдает за состоянием экземпляра context, и каждое изменение его состояния, в том числе внутри формулы (в результате вызова методов context), должно быть немедленно обнаружено. Очевидно, при описанном подходе изменения вообще обнаружены не будут. Все изменения будут произведены с другим объектом – копией context, а в момент присваивания новых полей полям экземпляра context наш метод eval «не будет знать», как сообщить об этих изменениях.

Хотелось бы попытаться все-таки получить доступ непосредственно к экземпляру context.

Решение существует. Для этого нам придется «обмануть» компилятор – создать .class-файл, который не может быть создан «законным» компилятором javac.

Рассмотрим следующий код Java:

import java.io.*;

import java.util.*;

какие-нибудь еще полезные import, которые могут пригодиться внутри формулы

public class ___ExpressionNNN

extends имя_класса_context

{

public class ___Performer {

public int ___performEval() {

int returnValue= текст_формулы;

return returnValue;

}

}

}

Все, как в прошлый раз, но теперь добавился вложенный класс ___Performer. Метод ___performEval теперь нестатический и не обладает аргументом, а внешний класс ___ExpressionNNN унаследован от context.getClass().getName().

Во что компилируется такой исходный код?

В результате компиляции получается 2 .class-файла – ___ExpressionNNN.class и ___ExpressionNNN$___Per-former.class. Рассмотрим их внимательно с помощью какого-нибудь дизассемблера, например утилиты javap (с ключами -private и -c).

Класс ___ExpressionNNN здесь – «пустышка». Он не содержит ни одного члена, кроме пустого (автоматически добавленного) конструктора. Этот класс – наследник context.getClass().getName().

Класс ___Performer имеет следующий вид:

public class ___ExpressionNNN$___Performer {

// Fields

private final ___ExpressionNNN this$0;

// Constructors

public ___ExpressionNNN$___Performer(

___ExpressionNNN p0)

{

реализация: копирует p0 в this$0

}

// Methods

public int ___performEval()

{

реализация, содержащая скомпилированный текст нашей формулы: для обращений к a,b и другим членам context используется ссылка this$0

}

}

Обратите внимание на поле this$0. Это «скрытый механизм» языка Java, позволяющий добираться из вложенных нестатических классов до текущих экземпляров содержащих их внешних классов. Заметьте: хотя идентификатор this$0 является корректным с точки зрения синтаксиса Java, компилятор javac не позволит объявить поле с таким именем в обычном классе – идентификатор «зарезервирован для внутреннего использования».

Вызов ___performEval через отражения выглядит следующим образом:

n  Вначале нужно создать экземпляр класса ___ExpressionNNN (обычным вызовом Class.newInstance).

n  Затем нужно отыскать (единственный) конструктор класса ___ExpressionNNN$___Performer и вызвать его (через java.lang.reflect.Constructor), передав в качестве аргумента ссылку на созданный экземпляр ___ExpressionNNN. Будет создан экземпляр ___ExpressionNNN$___Performer.

n  Затем нужно обычным образом (через java.lang.reflect.Method) отыскать и вызвать метод ___performEval, передав методу invoke в качестве параметра ссылку на только что созданный экземпляр ___ExpressionNNN$___Performer.

Если бы на шаге 2 удалось вместо нового экземпляра ___ExpressionNNN «подсунуть» конструктору наш экземпляр context, задача была бы решена. Метод ___performEval работал бы (через ссылку this$0) с нашим экземпляром, т.е. выполнил бы нашу формулу в требуемом контексте.

Как уже говорилось выше, виртуальная машина Java не допускает передачи в качестве аргумента типа ___ExpressionNNN экземпляра его предка context.getClass().getName().

Но можно слегка изменить класс ___Performer. Действительно, если в классе ___Performer везде заменить имя класса ___ExpressionNNN на context.getClass().getName() (прежде всего тип поля this$0 и тип аргумента конструктора), то этот класс останется с точки зрения виртуальной машины вполне корректным. Класс ___ExpressionNNN не добавляет к своему предку ни одного нового члена, к которому метод ___performEval мог бы попытаться обратиться через this$0. При работе с таким скорректированным классом через отражения на шаге 2 требования к аргументу конструктора были бы слабее: можно было бы передать ссылку на экземпляр класса context.getClass().getName(), в частности на наш экземпляр context.

Такую коррекцию невозможно сделать «легальным» путем, изменяя исходный текст и вызывая компилятор javac. Стандартный компилятор при генерации вложенного класса непременно придаст ссылке this$0 точный тип того класса, в который вложен данный. Но можно скорректировать уже скомпилированный .class-файл ___ExpressionNNN$___Performer.class.

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

В начале файла идет так называемый «пул констант», в котором собраны все символьные идентификаторы (в кодировке UTF-8), в том числе имена всех упоминаемых классов. Весь .class-файл, в частности пул констант, организован в виде последовательности секций примерно такого вида:

n  код_типа_секции

n  содержимое_фиксированной_длины

или

n  код_типа_секции

n  длина_секции

n  содержимое_переменной_длины

Если в произвольной секции изменить содержимое и соответствующим образом подправить длину секции (размер файла при этом тоже поменяется), то файл сохранит свою корректность. В .class-файле не встречаются прямые смещения от начала файла или длины в байтах каких-то фрагментов файла, за исключением длин элементарных секций.

Имя класса ___ExpressionNNN может встречаться внутри файла ___ExpressionNNN$___Performer.class в виде строковых констант трех видов:

n  непосредственно строка «___ExpressionNNN» (используется не всеми компиляторами);

n  строка «L___ExpressionNNN;» – внутреннее имя типа ___ExpressionNNN: именно таким образом виртуальная машина «именует» классы «внутри себя» (для примитивных типов и массивов используются другие обозначения);

n  строка «(L___ExpressionNNN;)V» – сигнатура конструктора или любого другого метода с единственным аргументом типа ___ExpressionNNN.

Если бы наш класс ___ExpressionNNN был вложен внутрь какого-либо некорневого пакета (в наших примерах он размещается в корневом пакете), то в его полном имени нужно было бы заменить точки символами “/”.

Строковые константы, в частности перечисленные выше, представлены в пуле констант в виде следующих цепочек байтов:

n  1 байт: 1 (код строкового типа);

n  2 байта: длина length строковой константы в кодировке UTF-8 (сначала старший байт, потом младший);

n  length байтов: содержимое строковой константы в кодировке UTF-8.

Все, что нам нужно сделать – загрузить файл ___ExpressionNNN$___Performer.class в виде массива байтов, отыскать в нем все такие цепочки байтов для строк «___ExpressionNNN», «L___ExpressionNNN;», «(L___ExpressionNNN;)V» и заменить их соответствующими цепочками для строк «XXXX», «LXXXX;», (LXXXX;)V». Здесь XXXX – полное имя класса context, в котором точки (разделители имени пакета) заменены знаком «/»:

context.getClass().getName().replace(‘.’,'/’)

Полученный новый массив байтов (другой длины) нужно записать обратно в файл ___ExpressionNNN$___Performer.class.

Если имя ___ExpressionNNN не используется в Java-приложении ни для каких других целей – для максимальной уверенности можно заменить его чем-нибудь вроде ___Expression_Asj5Sjl3_NNN, – то полученный .class-файл будет вполне корректным. Остается загрузить его, создать экземпляр, передав конструктору в качестве аргумента наш объект context, и выполнить метод ___performEval(). Задача решена полностью.

Возможно, в формулах имеет смысл открыть непосредственный доступ (без уточняющего имени класса) к какому-либо набору стандартных функций или констант. Например, в математических формулах естественнее смотрелась бы запись «sin(a+PI/4)», а не «Math.sin(a+Math.PI/4)». Для этого достаточно автоматически добавить желаемый набор функций и констант во вложенный класс ___Performer.

Некоторое неудобство приведенного решения связано с тем, что класс объекта context обязан быть public, а в случае локального класса – public static. В частности, недопустимо использовать анонимные классы или локальные классы, объявленные внутри методов. В противном случае нам попросту не удастся унаследовать от него класс ___ExpressionNNN, расположенный, вообще говоря, в совершенно другом пакете (в наших примерах – в корневом пакете).

Очевидно также, что для достижения хорошей эффективности все скомпилированные формулы необходимо кэшировать, чтобы одна и та же формула не компилировалась повторно. Имеет смысл для каждой пары «формулы, экземпляр context» сохранить (в таблице HashMap) готовый объект java.lang.reflect.Method для метода ___performEval и экземпляр вложенного класса ___Performer, чтобы при повторном обращении к eval осталось просто вызвать метод invoke. Также есть смысл проверить, что одна и та же формула с одним и тем же context вызывается повторно, и в этом случае перейти на особо быструю ветку – не обращающуюся к таблице HashMap. Подобная оптимизация позволяет достичь чрезвычайно высокого быстродействия, недостижимого для интерпретируемых скриптовых языков: накладные расходы будут укладываться в доли микросекунды (на компьютерах класса Pentium-III 800).

Идея описанного выше изящного решения принадлежит моему коллеге, Алексею Вылегжанину (anv@siams.com).

Разумеется, это решение (в отличие от первого варианта, требующего использования в формулах ссылки «с.»), не является стопроцентно переносимым. Оно зависит от особенностей компилятора javac, которые в принципе могут измениться в следующей версии Java. Например, компилятору ничто не мешает вставить в класс ___Performer явную проверку, что поле this$0 принадлежит нужному типу, причем сформировать имя типа динамически путем сложения строк «___Expres» и «sionNNN». Тогда скорректированный нами класс работать не будет. Кажется маловероятным, что подобное произойдет. Но на всякий случай описанное решение желательно заново тестировать с каждой новой версией Sun Java SDK.

Технология интерпретации формул на Java может оказаться чрезвычайно полезной в сложных приложениях, гибко настраиваемых пользователем. Скажем, процедура построения графика или статистического анализа может принимать на вход не только массив чисел, но и некоторую набранную пользователем аналитическую формулу. При заполнении сложной формы с множеством параметров можно разрешить в некоторых полях указывать формулы, оперирующие другими полями формы (например, «предыдущее_поле + 1»). Можно снабдить пользователя простейшим формульным калькулятором, который всегда под рукой и обладает функциями, специфичными для данного приложения.

Единственной серьезной проблемой может оказаться безопасность. Если разрешить пользователю Java-приложения самостоятельно писать формулы, исполняемые системой, то пользователь сможет, вообще говоря, выполнить любой законный оператор языка Java – скажем, добраться до содержимого диска. Если это недопустимо, то придется разрабатывать специальный менеджер безопасности («песочницу») для исполнения формул или жестко контролировать текст формулы.

Заключение

Нашу экскурсию по миру отражений можно считать законченной. Мы рассмотрели основные задачи по управлению классами: доступ к произвольным классам и их членам, загрузку классов из произвольного источника и, наконец, компиляцию новых классов из Java-файлов либо «на лету». Эти технологии позволяют профессионально решать по-настоящему сложные задачи, а также могут быть чрезвычайно удобны и в небольших утилитах.

Я сознательно не стал приводить Java-реализацию большинства из описанных выше технологий. Кроме того что это было бы чрезвычайно громоздко, мне кажется, что подобные задачи лучше разработчику решать самостоятельно, если только готовое решение не существует в виде стандартизованных и общеизвестных библиотек фирмы Sun или другой фирмы, заслуживающей не меньшего доверия. Разумеется, я располагаю законченной библиотекой, воплощающей все приведенные идеи, в том числе динамическую загрузку классов, описанную во второй части статьи. Но я никоим образом не берусь утверждать, что моя библиотека достаточно грамотно реализована и хорошо протестирована – настолько же, насколько это можно сказать о библиотеках фирмы Sun. Если вы реализуете необходимые функции самостоятельно, то, по крайней мере, вы сможете достаточно свободно отлаживать их и совершенствовать, что было бы гораздо труднее сделать с чужим кодом.

При написании статьи были использованы официальные материалы фирмы Sun, приведенные на сайте http://java.sun.com, а также следующие книги:

n  Арнолд К., Гослинг Дж., Холмс Д. Язык программирования Java. Издательский дом «Вильямс», Москва – С.-Петербург – Киев, 2001.

n  Вебер Дж. Технология Java(TM) в подлиннике. «BHV – Санкт-Петербург», С.-Петербург, 1997.

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

http://www.samag.ru/cgi-bin/go.pl?q=articles;n=02.2003;a=15


July 27th, 2010  
Tags: Java, Java Reflection



Java: Магия отражений

Java 0 Comment »

Часть II. ClassLoader – скрытые возможности

Даниил Алиевский

Самый интригующий класс мира отражений

В этой статье речь пойдет о классе java.lang.ClassLoader. Во многих отношениях он заслуживает эпитета «самый». Это самый фундаментальный класс в механизме отражений Java и одновременно самый яркий пример этой технологии. Это, пожалуй, самый необычный модуль не только в мире Java, но и среди большинства компилируемых языков. Это самый низкоуровневый, «глубинный» механизм Java, позволяющий вмешиваться практически в ядро Java-машины, причем оставаясь в рамках программирования на Java. Наконец, это один из самых трудных в понимании и использовании прикладных классов.

Как следует из названия, класс ClassLoader обеспечивает загрузку классов Java. Точнее, обеспечивают его наследники, конкретные загрузчики классов – сам ClassLoader абстрактен. Каждый раз, когда загружается какой-либо .class-файл, например, вследствие обращения к конструктору или статическому методу соответствующего класса – на самом деле это действие выполняет один из наследников класса ClassLoader.

Существует стандартный вариант реализации ClassLoader – так называемый системный загрузчик классов. Этот загрузчик используется по умолчанию при запуске приложений Java командой:

java Имя_главного_класса

Системный загрузчик классов реализует стандартный алгоритм загрузки из каталогов и JAR-файлов, перечисленных в переменной CLASSPATH (переменной среды либо параметре «-cp» утилиты «java»), а также из JAR-файлов, содержащих стандартные системные классы вроде java.lang.String и входящих в любой комплект поставки Java.

Одна из замечательных особенностей языка Java заключается в том, что вы можете реализовать свой собственный загрузчик классов – наследник ClassLoader – и использовать его вместо системного.

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

Реализуя наследников ClassLoader, вы можете полностью контролировать процесс загрузки абсолютно всех Java-классов. Вы можете загружать их из любого источника, скажем, из собственной системы каталогов, не отраженной в CLASSPATH, из базы данных или из Internet. Вы можете предоставить загрузку стандартных библиотечных классов системному загрузчику, но при этом протоколировать факт обращения к ним. При желании вы даже можете сконструировать байт-код класса в памяти и после этого работать с ним, как с нормальным классом, загруженным из «добропорядочного» .class-файла. Среди компилируемых языков подобные возможности встречаются разве что в ассемблере.

Единственное, что вы не можете сделать – создать новый класс, не располагая его байт-кодом. Каким-либо образом: загрузив c диска, из Internet, из базы данных или создав как-то иначе, – ваш наследник ClassLoader обязан получить корректный байт-код класса (образ в памяти обычного .class-файла) в виде массива byte[]. Затем его нужно передать специальному стандартному методу ClassLoader.defineClass, который «превратит» его в готовый класс – объект типа Class.

Ниже мы подробно рассмотрим весь этот механизм и решим с его помощью практическую задачу – динамическую подгрузку изменившихся версий .class-файлов без перезапуска главной Java-программы. В процессе решения этой задачи мы увидим, как поразительным образом изменится поведение, казалось бы, четко стандартизованных конструкций языка Java. С помощью довольно простого Java-кода мы изменим сам язык Java, адаптируя его к новым возможностям, которые обеспечиваются нашим вариантом класса ClassLoader.

Как Java загружает классы

Основной способ работы с классом ClassLoader – это реализация наследников от него. Прежде чем переходить к рассмотрению этой техники, мы немного поговорим о том, как Java использует загрузчики классов.

Как уже было сказано, в системе всегда существует по крайней мере один готовый наследник ClassLoader – системный загрузчик. Его всегда можно получить с помощью вызова ClassLoader.getSystemClassLoader() – статического метода класса ClassLoader, объявленного следующим образом:

public static ClassLoader getSystemClassLoader()

Когда вы запускаете приложение Java с помощью стандартной команды:

java Имя_главного_класса

виртуальная машина Java первым делом создает системный загрузчик, загружает с его помощью .class-файла вашего главного класса и вызывает статический метод вашего класса, соответствующий объявлению:

public static void main(String[] argv)

(или же сообщает об ошибке, не обнаружив такого метода).

Java – язык с отложенной загрузкой кода. Первоначально загружается только один класс – тот, который передан в качестве параметра утилите «java»[1]. Как только код этого класса обращается к какому-то другому классу (любым способом: вызовом конструктора, обращением к статическому методу или полю), загружается другой класс. По мере выполнения кода, загружаются всё новые и новые классы. Ни один класс не загружается до тех пор, пока в нем не возникнет реальная потребность. (Такое поведение заложено в стандартный системный загрузчик.)

Главный класс приложения всегда загружается системным загрузчиком. А какие загрузчики будут использоваться для загрузки всех прочих классов?

В Java поддерживается понятие «текущего» загрузчика классов. Текущий загрузчик – это тот загрузчик классов (экземпляр некоторого наследника ClassLoader), который загрузил класс, код которого исполняется в данный момент. Каждый класс «помнит» загрузивший его загрузчик. Загрузчик, загрузивший некоторый класс, всегда можно узнать, вызвав метод getClassLoader:

public ClassLoader getClassLoader()

у объекта типа Class, соответствующего данному классу. Например, если мы находимся внутри некоторого метода класса MyClass, то вызов MyClass.class.getClassLoader() вернет ссылку на загрузчик, загрузивший этот класс, т.е. загрузивший тот самый байт-код, который выполняет вызов «MyClass.class.getClassLoader()».

Когда возникает необходимость загрузить другой класс вследствие обращения к его конструктору, статическому методу или полю, виртуальная Java-машина автоматически обращается к текущему загрузчику классов, о котором «помнит» текущий исполняемый класс. При этом другой класс также «запоминает» этот загрузчик в качестве текущего. Иначе говоря, текущий загрузчик, загрузивший данный класс, по умолчанию наследуется всеми классами, прямо или косвенно вызываемыми из данного.

Так как главный класс приложения обычно загружается системным загрузчиком, то он же используется и для загрузки всех остальных классов, необходимых приложению. В случае Java-апплета броузер загружает главный класс апплета своим собственным загрузчиком (умеющим читать классы с веб-сервера); в результате тот же самый загрузчик используется для загрузки всех вспомогательных классов апплета.

На самом деле наследование текущего загрузчика – лишь поведение по умолчанию. Загрузчик классов можно написать и так, что он не будет наследоваться для некоторых классов. В тот момент, когда к загрузчику приходит запрос «выдать класс по заданному имени», он может передать этот запрос какому-нибудь другому загрузчику. Тогда данный класс и другие классы, вызываемые из него, будут загружаться новым загрузчиком. Например, специальный загрузчик, реализуемый броузером для загрузки классов апплетов, вполне может «передать свои полномочия» системному загрузчику, когда дело касается стандартного системного класса вроде java.lang.String.

Стандартный способ загрузить некоторый класс загрузчиком, отличным от текущего – специальная версия статического метода Class.forName:

public static Class forName(String name,boolean initialize,ClassLoader loader)

В качестве name передается полное имя класса (с указанием пакета), в качестве loader – требуемый загрузчик. Не столь очевидный (и не столь важный) параметр initialize управляет инициализацией класса, т.е. установкой значений всех static-полей класса и исполнением кода в секциях:

static {

…

}

Если initialize содержит true, то инициализация происходит немедленно, в противном случае – откладывается до первого обращения к любому конструктору, статическому методу или полю этого класса.

Более простая форма метода Class.forName, о которой шла речь в первой части статьи («Основы», журнал «Системный администратор» №1, октябрь 2002г.)

public static Class forName(String className)

всегда использует текущий загрузчик классов. На самом деле, вызов

Class.forName(name)

эквивалентен вызову

Class.forName(name,true,Текущий_класс.class.getClassLoader())

где Текущий_класс – имя класса, внутри которого содержится данный вызов.

Загрузив класс, можно создать его экземпляр или вызвать статический метод средствами отражений. (Техника работы с классом через отражения была подробно описана в первой части статьи.) Дальше этот класс может обычными средствами языка Java обращаться к другим классам – для них будет вызван тот же самый загрузчик loader (либо какие-то другие загрузчики, если реализация loader в какой-то момент «решит» передать управление другому загрузчику). Простейший пример:

Class clazz= Class.forName(“Имя_класса”,true,Мой_нестандартный_загрузчик);

clazz.newInstance(); // создаем экземпляр класса

Конструктор без параметров класса Имя_класса может стартовать поток (Thread), решающий некоторую сложную задачу. Все классы, необходимые этой задаче, будут загружены указанным загрузчиком.

Обзор класса ClassLoader

Перед тем как переходить к нашей основной задаче – реализации наследников класса ClassLoader – давайте посмотрим, какие методы предоставляет этот класс. Как обычно, мы рассмотрим только наиболее важные из них. Полный список можно найти в документации фирмы Sun.

Один из методов ClassLoader мы уже рассматривали. Это статический метод, возвращающий ссылку на стандартный системный загрузчик.

public static ClassLoader getSystemClassLoader()

Среди прочих методов самый «бросающийся в глаза»:

public Class loadClass(String name)

Этот метод загружает класс с заданным именем. На самом деле его реализация сводится к вызову другого protected-метода:

protected synchronized Class loadClass(String name,boolean resolve)

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

Я не знаю, почему метод loadClass(String name) объявлен как public. Ведь уже существует стандартный способ загрузки класса по имени, с помощью произвольного загрузчика – вызов

Class.forName(“Имя_класса”,true,loader)

(Классы Class и ClassLoader расположены в общем пакете – так что метод loadClass(String name) вполне мог бы быть protected. Это не помешало бы методу Class.forName его вызвать.)

Может быть, раз уж loadClass – public-метод, то вместо Class.forName(«Имя_класса»,true,loader) можно пользоваться прямым обращением loader.loadClass(“Имя_класса”) ?

Судя по всему, следует все же всегда использовать вызов Class.forName. Хотя это совершенно неочевидно из документации. Несколько позже мы увидим, что метод Class.forName выполняет с классом некоторые дополнительные действия, в частности, кэширует его, обеспечивая, в отличие от прямого вызова loadClass, стабильную работу даже при недостаточно аккуратной реализации загрузчика loader.

Есть также группа методов, предназначенных для загрузки ресурсов:

public URL getResource(String name)

public InputStream getResourceAsStream(String name)

public final Enumeration getResources(String name)

public static URL getSystemResource(String name)

public static InputStream getSystemResourceAsStream(String name)

public static Enumeration getSystemResources(String name)

Это более полный аналог методов getResource и getResourceAsStream класса Class, рассмотренных в первой части статьи. На самом деле методы Class.getResource и Class.getResourceAsStream как раз обращаются к соответствующим методам текущего загрузчика, загрузившего данный класс. Главное отличие методов работы с ресурсами класса ClassLoader – абсолютные пути. Путь к ресурсу отсчитывается не от каталога, содержащего данный class-файл (как в случае Class.getResource и Class.getResourceAsStream), а от одного из каталогов, указанных в переменной CLASSPATH.

Обратите внимание: названия методов getSystemResource, getSystemResourceAsStream, getSystemResources вовсе не означают, что загружаются какие-то особые «системные» ресурсы. Слово «System» в этих названиях говорит о том, что для загрузки ресурсов будет в любом случае использоваться стандартный системный загрузчик.

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

Ставим задачу: перезагрузка классов «на лету»

Мы приступаем к самой интересной части – реализации своего наследника абстрактного класса ClassLoader.

Обычно в книгах по языку Java реализацию ClassLoader рассматривают на примере загрузки .class-файлов из какого-либо нестандартного источника, например, каталога, не указанного в переменной CLASSPATH. Такой пример достаточно прост, но, на мой взгляд, не очень интересен. В большинстве ситуаций поиск .class-файлов в путях, перечисленных в CLASSPATH, – вполне нормальное решение. Загрузка же из принципиально иных источников типа Internet вряд ли будет полезна вне контекста куда более сложной задачи, включающей такие вопросы, как политика безопасности или кэширование загруженных файлов на локальном диске.

Мы попробуем решить другую задачу.

Предположим, разрабатывается большая программа на Java. По каким-либо причинам эту программу нежелательно часто перезагружать: останавливать и запускать снова. Например, это может быть сложная серверная программа, каждую секунду обслуживающая многих клиентов, для которой даже сравнительно кратковременная неработоспособность является критичной. Или просто программа настолько «тяжелая», что полный ее перезапуск требует несколько минут, и часто перезапускать ее крайне неудобно.

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

Что касается действительно новых классов – тут проблем нет. Вы вольны в любой момент скомпилировать новый класс с новым уникальным именем – даже когда программа уже запущена. Если ваша программа после этого выполнит вызов Class.forName(name) с этим именем (например, в результате автоматического сканирования каталогов поиска CLASSPATH в поисках новых .class-файлов), то этот класс будет успешно подключен, и программа сможет им пользоваться.

Что касается версий .class-файлов – тут все значительно хуже. Однажды обратившись к некоторому классу, стандартный системный загрузчик запомнит его в своем внутреннем кэше и будет всегда использовать именно его. Никакие последующие перекомпиляции .class-файла и даже физическое удаление этого файла не отразятся на работе этого класса. Насколько я знаю, никакими силами, кроме полного перезапуска программы (т.е. всей виртуальной машины Java), нельзя заставить системный загрузчик «забыть» тот байт-код класса, который он однажды загрузил.

Если вы когда-нибудь разрабатывали и отлаживали апплеты, возможно вы сталкивались с неприятной особенностью броузеров: не учитывать изменения в перекомпилированном классе апплета до тех пор, пока программа-броузер не будет закрыта и запущена заново. Судя по всему, происхождение этой проблемы кроется как раз в описанном поведении системного загрузчика (вместе с нежеланием разработчиков броузера рестартовать по кнопке «Refresh»/»Reload» виртуальную Java-машину).

На самом деле у такого поведения системного загрузчика есть веские причины. Это станет вполне очевидно позже, когда мы – как вы, вероятно, уже догадались – реализуем собственный загрузчик классов, лишенный описанного «недостатка». На самом деле возможность «на лету» менять реализацию классов, не перезапуская программу, может оказаться очень полезной в описанной выше ситуации. Но при этом, как мы увидим, в разработке Java-программ появляются некоторые экзотические нюансы, которые были бы совершенно неуместны в большинстве обычных программ – поэтому разработчики Java и заблокировали подобную возможность в стандартном системном загрузчике.

Итак, наша задача – написать загрузчик классов, аналогичный стандартному системному, который, в отличие от него, умел бы «забывать» загруженные ранее версии классов и загружать .class-файлы заново.

Заодно мы решим и более простую традиционную задачу – загрузку .class-файлов из собственного списка каталогов поиска. Это нам ничего не будет стоить. Какая разница – загружать файлы непременно из каталогов CLASSPATH или из каких-либо других. Мы будем учитывать, что некоторые (или все) каталоги CLASSPATH могут попасть в наш список.

Для простоты мы не будем поддерживать в этом загрузчике работу с JAR-архивами. Все-таки JAR предназначен для упаковки достаточно стабильных версий программных модулей, которые вряд ли стоит обновлять настолько часто, что ради этого нежелательно выполнять перезапуск основной Java-программы. В частности, загрузку стандартных библиотечных классов (пакеты java.lang и подобные), которые обычно находятся в JAR-файле, мы возложим на системный загрузчик.

Назовем наш новый загрузчик DynamicClass-Overloader – «динамический перезагрузчик классов».

Первые тесты и первые успехи

Итак, мы собираемся создать наследника абстрактного класса ClassLoader, который умел бы загружать классы из некоторого заданного набора каталогов поиска так же, как это делает системный загрузчик для каталогов, перечисленных в переменной CLASSPATH. В отличие от системного загрузчика наш вариант ClassLoader должен уметь «забывать» о загруженных ранее классах.

Мы должны реализовать собственные версии следующих методов ClassLoader:

protected synchronized Class loadClass(String name,boolean resolve)

throws ClassNotFoundException

protected Class findClass(String name)

throws ClassNotFoundException

protected java.net.URL findResource(String name)

protected java.util.Enumeration findResources(String name)

throws IOException

Абстрактный класс ClassLoader в действительности предоставляет реализацию для первого метода, loadClass, основанную на двух других protected-методах – findLoadedClass и findClass. (Метод findLoadedClass объявлен как final – его переопределять не нужно.) Эта реализация проверяет, не был ли загружен класс раньше (вызовом «findLoadedClass(name)»); если нет – делает попытку загрузить класс стандартным образом, и если эта попытка терпит неудачу – обращается к методу findClass.

Для решения более традиционной задачи – обеспечения загрузки классов из нестандартного источника – такая реализация вполне подходит. В этом случае было бы достаточно реализовать метод findClass. Но мы хотим загружать классы из вполне стандартного источника (хотя и нестандартным образом): из набора каталогов, который может соответствовать стандартной переменной CLASSPATH. Значит мы не должны первым делом вызывать стандартный загрузчик, полагаясь на то, что для наших имен классов он потерпит неудачу, и loadClass обратится к нашему методу findClass. Нам нужно реализовать свою версию loadClass, действующую наоборот: вначале пытающуюся загрузить .class-файл самостоятельно и лишь в случае неудачи (скажем, для классов из пакета java.lang, которые обычно упакованы в JAR-архив) обращающуюся к системному загрузчику.

Методы findResource и findResources, подобно findClass, обеспечивают работоспособность public-методов загрузки ресурсов getResource, getResourceAsStream и getResources. Для ресурсов задачу динамической перезагрузки без рестарта программы решать не нужно – они и так всегда загружаются динамически. Поэтому, в отличие от ситуации с findClass, нам вполне достаточно переопределить методы findResource и findResources. Сделать это необходимо, так как, возможно, мы будем использовать наш загрузчик с каталогами, неизвестными системному загрузчику, т.е. отличными от каталогов CLASSPATH.

Мы не будем отвлекаться на реализацию метода findResources. Его использование представляется чересчур экзотичным. Мы реализуем только findResource – это почти ничего не стоит, и на этом методе основаны все типичные приемы работы с ресурсами (через методы Class.getResource и Class.getResourceAsStream), описанные в первой части статьи.

Как нужно реализовывать эти методы?

Согласно документации loadClass должен просто найти или загрузить указанный класс с помощью findClass, после чего, если параметр resolve содержит true, вызвать для полученного класса protected-метод resolveClass. Что этот метод в точности делает – для нас в данный момент неважно.

Метод findClass должен загрузить байт-код указанного класса (в нашем случае это просто чтение файла), после чего выполнить для полученного массива байтов специальный «магический» метод defineClass:

protected final Class defineClass(String name,byte[] b, int off, int len)

throws ClassFormatError

Это как раз то самое место, где цепочка байтов – образ .class-файла (фрагмент массива b длины len по смещению off) – «чудесным образом» превращается в готовый к использованию класс. Метод defineClass, как и следовало ожидать, реализован в native-коде. Именно он помещает байт-код класса в недра виртуальной машины, где он приобретает вид, пригодный для непосредственного исполнения на конкретной аппаратной платформе, в частности, компилируется в машинный код процессора для более быстрого исполнения (так называемая технология Just-In-Time, сокращенно JIT-компиляция).

Наконец, метод findResource должен просто найти файл, соответствующий данному ресурсу – по тем же правилам, по которым отыскивается файл класса в методе findClass – и вернуть ссылку на него в виде URL.

Системный загрузчик классов не просто загружает файлы классов с диска, но еще и запоминает их во внутреннем кэше – так что последующие обращения к loadClass для того же имени класса просто выдают готовый объект Class из кэша. Кэширование, вообще говоря, представляется разумной идеей: зачем каждый раз заново обращаться к диску? Мы будем хранить кэш в нестатическом private-поле типа java.util.HashMap нашего класса DynamicClassOverloader. Таким образом, каждый новый экземпляр нашего загрузчика будет создаваться с новым кэшем, и для «забывания» загруженных ранее классов будет достаточно просто создать новый экземпляр загрузчика.

Итак, реализация: версия первая.

import java.io.*;

public class DynamicClassOverloader extends ClassLoader {

private java.util.Map classesHash= new java.util.HashMap();

public final String[] classPath;

public DynamicClassOverloader(String[] classPath) {

// Набор путей поиска – аналог переменной CLASSPATH

this.classPath= classPath;

}

protected synchronized Class loadClass(String name,boolean resolve)

throws ClassNotFoundException

{

Class result= findClass(name);

if (resolve) resolveClass(result);

return result;

}

protected Class findClass(String name)

throws ClassNotFoundException

{

Class result= (Class)classesHash.get(name);

if (result!=null) {

/*

System.out.println(“% Class “+name+” found in cache”);

/*

return result;

}

File f= findFile(name.replace(‘.’,'/’),”.class”);

// Класс mypackage.MyClass следует искать файле mypackage/MyClass.class

/*

System.out.println(“% Class “+name+(f==null?”":” found in “+f));

/*

if (f==null) {

return findSystemClass(name);

// Обращаемся к системному загрузчику в случае неудачи. findSystemClass – это метод абстрактного класса

// ClassLoader с объявлением protected final Class findSystemClass(String name) (т.е. предназначенный

// для использования в наследниках и не подлежащий переопределению). Он выполняет поиск и загрузку класса

// по алгоритму системного загрузчика. Без вызова findSystemClass(name) нам пришлось бы самостоятельно

// позаботиться о загрузке всех стандартных библиотечных классов типа java.lang.String, что потребовало бы

// реализовать работу с JAR-архивами (стандартные библиотеки почти всегда упакованы в JAR)

}

try {

byte[] classBytes= loadFileAsBytes(f);

result= defineClass(name,classBytes,0,classBytes.length);

} catch (IOException e) {

throw new ClassNotFoundException(“Cannot load class “+name+”: “+e);

} catch (ClassFormatError e) {

throw new ClassNotFoundException(“Format of class file incorrect for class “+name+”: “+e);

}

classesHash.put(name,result);

return result;

}

protected java.net.URL findResource(String name) {

File f= findFile(name,”");

if (f==null) return null;

try {

return f.toURL();

} catch(java.net.MalformedURLException e) {

return null;

}

}

private File findFile(String name, String extension) {

// Поиск файла с именем name и, возможно, расширением extension в каталогах поиска, заданных параметром

// конструктора classPath. Имена подкаталогов в name разделяются символом ‘/’ – даже если в операционной

// системе принят другой разделитель для подкаталогов. (Именно в таком виде получает свой параметр метод

// findResource.)

for (int k=0; k<classPath.length; k++) {

File f= new File((new File(classPath[k])).getPath()+File.separatorChar+name.replace(‘/’,File.separatorChar)+extension);

if (f.exists()) return f;

}

return null;

}

public static byte[] loadFileAsBytes(File file)

throws IOException

{

byte[] result= new byte[(int)file.length()];

FileInputStream f= new FileInputStream(file);

try {

f.read(result,0,result.length);

} finally {

try {

f.close();

} catch (Exception e) {

// Игнорируем исключения, возникшие при вызове close. Они крайне маловероятны и не очень

// важны – файл уже прочитан. Но если они все же возникнут, то они не должны замаскировать

// действительно важные ошибки, возникшие при вызове read.

};

}

return result;

}

}

Проверяем. Пишем тестовый класс TestModule.java, который собираемся загружать нашим загрузчиком:

public class TestModule {

public String toString() {

return “TestModule, version 1!”;

}

}

Пишем тест Test.java, который будет загружать этот класс:

import java.io.*;

public class Test {

public static void main(String[] argv) throws Exception {

for (;;) {

ClassLoader loader= new DynamicClassOverloader(new String[] {“.”});

// текущий каталог “.” будет единственным каталогом поиска

Class clazz= Class.forName(“TestModule”,true,loader);

Object object= clazz.newInstance();

System.out.println(object);

new BufferedReader(new InputStreamReader(System.in)).readLine();

}

}

}

Кладем все эти файлы в один каталог, компилируем и запускаем Test:

java Test

В каждой итерации бесконечного цикла создается экземпляр нашего загрузчика loader, с его помощью загружается класс TestModule, создается его экземпляр и распечатывается, при этом, как обычно, неявно используется метод toString(), реализованный в TestModule. Затем ожидается нажатие на клавиатуре клавиши ENTER (либо Ctrl-C для выхода из программы).

Пока наш тест ждет нажатия ENTER, перейдем в другое окно (ОС Windows) или консоль (ОС Unix), чуть-чуть исправим класс TestModule: изменим результат toString() на «TestModule, version 2!» и перекомпилируем его. После чего вернемся в наш тест и нажмем ENTER.

Ура! В следующей итерации цикла мы видим результат работы свежей версии класса TestModule.class – будет напечатана новая строка «TestModule, version 2!».

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

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

Первые проблемы

Наш тест работает, но возникает законный вопрос – ну и что? Да, мы можем в определенный момент запустить некий класс, заданный своим именем, после чего он будет выполнять какие-то действия и, в конце концов, вернет строку «object.toString()». Но это в общем-то ничем принципиально не отличается от запуска новой java-программы стандартным вызовом

java Имя_класса

Вспомним постановку задачи. У нас есть большая, очень большая Java-программа, полный перезапуск которой занимает длительное время и крайне нежелателен. Мы хотим иметь возможность в какой-то момент быстро перезагрузить некоторые ФРАГМЕНТЫ программы, чтобы все классы, относящиеся к этим фрагментам, после этого момента заново считывались с диска. Вероятнее всего, эти классы должны активно взаимодействовать друг с другом и со стационарной, неперезагружаемой частью программы. Например, они могут реализовывать различные интерфейсы, которыми пользуется стационарная часть программы, их экземпляры могут сохраняться в различных переменных в стационарной части и т. д.

В терминах нашего загрузчика классов это означает, что мы должны уметь взаимодействовать с классами, загруженными вызовом

Class.forName(“Имя_класса”,true,loader)

Мы должны работать с их экземплярами, вызывать методы, обращаться к полям, перехватывать исключения, причем по возможности стандартными способами языка Java. Аналогично классы, загруженные разными экземплярами загрузчика, например, отвечающие за слабо связанные фрагменты большой программы, которые нужно перезагружать в разные моменты – должны уметь взаимодействовать друг с другом.

Казалось бы, с этим нет никаких проблем. В тесте мы получили вызовом newInstance() переменную типа Object. Но если мы знаем, что ее тип – TestModule, мы можем спокойно привести ее к этому типу и работать дальше обычным образом:

…

Class clazz= Class.forName(“TestModule”,true,loader);

TestModule testModule= (TestModule)clazz.newInstance();

работаем с полями testModule, вызываем методы и т.д.

Если бы здесь был обычный вызов Class.forName(«TestModule»), все было бы нормально. Это был бы простейший вариант классической схемы построения расширяемых систем. В качестве аргумента forName мог бы выступать любой наследник TestModule (или класс, реализующий интерфейс TestModule), реализация которого неизвестна и не обязательно доступна в момент компиляции системы. Об этом способе работы с отражениями рассказывалось в первой части статьи.

Но с нашим необычным загрузчиком классов нас ждет неприятная неожиданность. При попытке приведения типа будет возбуждено исключение ClassCastException – ошибка приведения типа!

Новые свойства языка – «динамические» и «истинно-статические» классы

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

На самом деле мы только что столкнулись с проявлением достаточно общей проблемы. Чтобы понять ее природу, нужно заново внимательно рассмотреть понятие класса в языке Java.

В мире объектно-ориентированного программирования, в частности в Java, мы привыкли к тому, что существует два уровня иерархии сущностей – класс и экземпляр. Класс с заданным именем в системе всегда один – он однозначно идентифицируется своим полным именем. Экземпляров же у класса может быть много. Так, в Java поля с квалификатором static принадлежат целому классу, каждое такое поле существует в системе в единственном экземпляре. В отличие от этого для обычного (нестатического) поля отдельная его версия присутствует в каждом экземпляре класса.

Создав наш загрузчик DynamicClassOverloader, всегда загружающий свежие версии class-файлов, мы принципиально изменили ситуацию. Теперь есть три уровня иерархии: сам класс, версия класса – тот байт-код, который был загружен конкретной версией DynamicClassOverloader (возможно, меняющийся в процессе работы программы) и экземпляры конкретной версии класса.

На самом деле виртуальная машина Java «считает» классом как раз то, что для нас является версией. Хотя это и неочевидно из документации, но класс в Java однозначно идентифицируется не только полным именем, но еще и экземпляром загрузчика, загрузившего этот класс. Каждый экземпляр загрузчика классов порождает собственное пространство имен, внутри которого классы действительно однозначно идентифицируются полными именами, но классы в разных пространствах имен, загруженные разными загрузчиками, вполне могут иметь идентичные имена.

На самом деле, с точки зрения Java, класс TestModule, возникающий в результате прямого вызова «TestModule testModule= …» в тексте файла Test.java, и класс TestModule, полученный вызовом:

Class.forName(“TestModule”,true,loader)

– это два совершенно разных класса. Первый был загружен системным загрузчиком (вместе с самым главным классом Test), а второй – одним из экземпляров нашего загрузчика DynamicClassOverloader. По классическим законам объектно-ориентированного программирования приведение типов между ними невозможно.

Более того, каждая итерация нашего цикла, оказывается, порождала новую версию класса TestModule, не связанную с предыдущими. В этом можно убедиться следующим образом. Модифицируем класс TestModule:

public class TestModule {

private static int counter= 0;

public String toString() {

return “TestModule, version 1! “+(counter++);

}

}

В нормальных условиях каждое новое обращение к методу toString() такого класса привело бы к получению строки с новым, увеличенным на 1 значением счетчика counter. Если бы в тесте Test.java был обычный вызов Class.forName(«TestModule»), мы бы это и увидели: на каждой итерации цикла распечатывались бы разные значения счетчика. А с нашим загрузчиком мы все время будем видеть нулевое значение. Каждое пересоздание экземпляра загрузчика DynamicClassOverloader приводит к появлению нового пространства имен, в котором инициализируется совершенно новая версия класса TestModule, ничего не знающая о предыдущих версиях и о содержащихся в них статических переменных.

Фактически, мы придали языку Java новое свойство – «динамичность» классов. Теперь, обращаясь прямо или косвенно к любому классу, придется думать о том, какая именно версия этого класса будет использована и можно ли ее использовать совместно с тем классом, который к ней обращается.

Свойство, что и говорить, крайне неудобное. Как же теперь работать с «динамическими» классами? Ведь все версии классов, которыми пользуется наш первый класс, загруженный DynamicClassOverloader, и которые тем самым тоже загружены нашим загрузчиком, – все эти версии неизвестны в пространстве имен стационарной части – в нашем случае в главном классе Test.

Решение этой проблемы – заблокировать свойство «динамичности» для некоторых классов, т.е. потребовать, чтобы такие классы наш загрузчик загружал стандартным способом – через вызов «findSystemClass(name)». Назовем такие классы «истинно-статическими» – «true-static». Такие «true-static»-классы можно будет свободно совместно использовать в стационарной части программы и всех версиях «динамических» классов. Для «true-static»-классов всегда будет существовать только одна версия, как и предполагается в обычном языке Java, и не будет проблем несоответствия типа. Можно, например, сделать «true-static» все ключевые интерфейсы, основные типы данных, которые должны получать и возвращать «динамические» классы, базовые типы исключений, подлежащие перехвату и единообразной обработке, и т. п.

В сущности, уже в реализованной нами версии загрузчика существовали «true-static»-классы – это библиотечные классы из пакетов типа java.lang, которые мы не пытались грузить самостоятельно. Скажем, такими были стандартные типы Object и String. Именно поэтому в первоначальном варианте теста мы смогли получить от созданного экземпляра динамического класса TestModule строку String – результат метода toString().

Можно придумать много соглашений, по которым загрузчик должен опознавать «true-static»-классы. Например, можно проверить существование некоторого ключевого static-поля или проверить, не реализован ли в классе некоторый специальный пустой интерфейс[2]. Мы ограничимся наиболее простым (хотя и не всегда удобным) вариантом: будем проверять, не содержит ли имя класса name цепочки символов «truestatic» без учета регистра символов.

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

File f= findFile(name.replace(‘.’,'/’),”.class”);

дополнительную проверку имени name. Вот начало исходного текста нового метода findClass:

protected Class findClass(String name)

throws ClassNotFoundException

{

Class result= (Class)classesHash.get(name);

if (result!=null) {

/*

System.out.println(“% Class “+name+” found in cache”);

/*

return result;

}

if (name.toLowerCase().indexOf(“truestatic”)!=-1)

return findSystemClass(name);

File f= findFile(name.replace(‘.’,'/’),”.class”);

…

Попробуем этим воспользоваться. Создаем «true-static»-класс TrueStaticModule.java:

public class TrueStaticModule {

protected static int counter= 0;

public int getCounter() {

return counter;

}

}

В нем есть public-метод getCounter(), которым мы собираемся пользоваться в стационарной части программы.

Наследуем от него «динамический» класс DynamicModule.java:

public class DynamicModule extends TrueStaticModule {

public String toString() {

return “DynamicModule, version 1! “+(counter++);

}

}

Наконец, переписываем тест Test.java – «стационарную часть» программы:

import java.io.*;

public class Test {

public static void main(String[] argv) throws Exception {

for (;;) {

ClassLoader loader= new DynamicClassOverloader(new String[] {“.”});

// текущий каталог “.” будет единственным каталогом поиска

Class clazz= Class.forName(“DynamicModule”,true,loader);

TrueStaticModule trueStaticModule=(TrueStaticModule) clazz.newInstance();

System.out.println(trueStaticModule.getCounter());

System.out.println(trueStaticModule);

new BufferedReader(new InputStreamReader (System.in)).readLine();

}

}

}

Компилируем все эти файлы и запускаем:

java Test

Все работает нормально. Как и раньше мы можем прямо в процессе работы теста модифицировать и перекомпилировать класс DynamicModule, и это изменение будет учтено. Но для «общения» со стационарной частью программы и для хранения счетчика обращений к методу toString() теперь используется «true-static»-класс TrueStaticModule. Поэтому мы не получаем исключения ClassCastException, а счетчик counter корректно увеличивается на протяжении всей работы теста.

Поставленную задачу можно считать в принципе решенной.

Чтобы использование нашего загрузчика стало действительно удобным, стоило бы еще реализовать специальный сервисный «true-static»-класс с методом forName, аналогичным стандартному forName. Только в отличие от стандартного, наш forName обращался бы к нашему загрузчику, экземпляр которого, внутреннее private-поле, создавался бы при первом обращении к forName. Параметры конструктора для нашего загрузчика можно было бы настраивать с помощью специальных полей сервисного класса. Кроме того, в нашем сервисном классе был бы специальный метод invalidate, обнуляющий private-поле с нашим загрузчиком и вынуждающий метод forName при следующем вызове заново создать загрузчик. Метод invalidate можно было бы вызывать в Java-программе всякий раз, когда требуется перезагрузить с диска новые версии всех «динамических» классов. Написание подобного сервисного класса – достаточно понятная задача, и мы не будем на ней останавливаться.

Правила работы с «динамическими» классами

При практическом использовании описанного выше загрузчика классов программирование в языке Java заметно усложняется. Нужно помнить о возможной «динамичности» классов: каждый экземпляр загрузчика порождает отдельную версию каждого такого класса в собственном пространстве имен. Нужно заботиться о том, чтобы некоторые классы были «true-static». Все это требует четкого понимания описанных выше механизмов и достаточной аккуратности.

Я сформулирую ниже несколько правил, которыми следует руководствоваться при программировании в таком, изменившем свое поведение, языке Java.

Будем называть динамической частью Java-программы ту систему «динамических» классов, которая загружается некоторым экземпляром нашего загрузчика DynamicClassOverloader, и стационарной частью – основную систему классов, которая загружает динамическую часть, используя экземпляры DynamicClassOverloader. В программе может быть и несколько динамических частей, никак не связанных друг с другом, одновременно загруженных несколькими экземплярами DynamicClass-Overloader. В стационарную часть входят, в частности, все «true-static»-классы.

ПРАВИЛО A. Стационарную часть программы – в частности, все «true-static»-классы – следует разрабатывать таким образом, чтобы никак не использовать информацию о структуре «динамических» классов: имена классов, имена их членов, типы параметров у методов. Иными словами, нельзя прямо ссылаться на конкретные имена «динамических» классов и обращаться к ним средствами языка Java. Единственным способом взаимодействия стационарной части программы с «динамическими» классами должна быть их загрузка вызовом

Class.forName(“TestModule”,true,loader)

и использование системы «true-static»-классов (или интерфейсов), известных и стационарной, и динамической частям. Например, можно обращаться (через приведение типа) к «true-static»-интерфейсам, которые реализуют «динамические» классы, получать результаты методов этих интерфейсов в виде «true-static»-классов, перехватывать «true-static»-исключения и т. д.

Сформулированное правило вполне логично и «выдержано в духе» объектно-ориентированного программирования. «Динамические» классы для того и сделаны «динамическими», чтобы их можно было разрабатывать и компилировать уже после того, как стационарная часть программы скомпилирована и запущена. Поэтому стационарная часть и не должна ничего «знать» об этих классах кроме того, что они, возможно, реализуют какие-то заранее известные «true-static»-интерфейсы или, скажем, как-то работают со static-полями заранее известных «true-static»-классов.

В первом нашем тесте, вызвавшем ошибку приведения типа, мы нарушили это правило. При приведении типа мы непосредственно сослались из стационарной части программы на имя «динамического» класса TestModule. В правильном решении, которое мы привели позже, мы преобразовывали полученный объект неизвестного нам «динамического» типа к типу «true-static»-класса TrueStaticModule – предка «динамического» класса.

Заметим, сформулированное правило не является категорическим. Его вполне можно нарушать, т.е. прямо ссылаться из стационарной части на «динамические» классы. Просто следует помнить, что классы, которые будут при этом задействованы, и классы с теми же именами, к которым будет обращаться динамическая часть программы, – это совершенно разные классы, лежащие в разных пространствах имен.

Например, и стационарная, и динамическая части программы могут активно пользоваться некоторыми сервисными библиотеками, и эти библиотеки нет никакой необходимости делать «true-static». Они вполне могут быть «динамическими». Если мы изменим реализацию некоторого метода в таком классе-библиотеке и перекомпилируем этот класс, то стационарная часть этого «не заметит» и будет продолжать работать со своей старой версией библиотеки, а динамическая часть, после очередного пересоздания экземпляра загрузчика, воспользуется новой версией.

Из общего правила A можно выделить несколько более простых частных правил.

ПРАВИЛО A1. Не следует ссылаться из стационарной части программы (в частности, «true-static»-класса) на какие-либо поля, конструкторы или методы «динамического» класса. Точнее, следует иметь в виду, что такая ссылка означает обращение к версии «динамического» класса, загруженной системным загрузчиком по умолчанию, а никак не к версии, загруженной нашим загрузчиком DynamicClassOverloader.

ПРАВИЛО A2. Все аргументы и результат любого метода в «true-static»-классе, используемого для связи между стационарной и динамической частями программы, например, метода «true-static»-интерфейса, который реализуют конкретные «динамические» классы, – должны иметь либо примитивный тип, либо тип «true-static»-класс. То же самое относится к public-полям, используемым с аналогичными целями. (К «true-static»-классам мы относим также все стандартные системные классы, вроде java.lang.String или java.io.File, которые наш загрузчик не пытается грузить самостоятельно.)

ПРАВИЛО A3. Если «динамические» классы возбуждают какие-либо исключения, которые, возможно, потребуется перехватить оператором

catch (Тип_исключения e)

в стационарной части программы, то перехватываемый класс Тип_исключения должен быть «true-static».

Следующее общее правило:

ПРАВИЛО B. Нужно учитывать, что каждый экземпляр загрузчика порождает независимое пространство имен. Любая ссылка на «динамический» класс: на его конструкторы, методы, статические или обычные поля действует только в пределах текущего пространства имен и не относится к версиям того же класса, загруженным другими экземплярами загрузчика.

Вот частные следствия из этого правила:

ПРАВИЛО B1. Не следует думать, что статические поля «динамического» класса существуют в системе в единственном экземпляре. В каждом пространстве имен существует отдельная версия класса со своим набором статических полей.

Например, есть такая практика управления поведением Java-класса. Объявляется статическое public-поле, скажем,

public static boolean debugMode= false;

влияющее на работу некоторых методов класса. Обычно используется значение этого поля по умолчанию. Но при желании главный класс Java-приложения на этапе общей инициализации системы и загрузки конфигурационных файлов может записать в это поле другое значение.

С «динамическими» классами такой прием «не проходит». Главный класс в стационарной части может повлиять на значение static-поля только для одной версии «динамического» класса, загруженной системным загрузчиком. Все последующие версии, загруженные экземплярами DynamicClassOverloader, получат умолчательное, заново инициализированное значение этого static-поля.

Если «динамический» класс действительно нуждается в наборе истинно глобальных полей, разделяемых всеми своими версиями, то самое естественное решение – определить внутри этого класса локальный «true-static»-класс. Например:

public class Мой_динамический_класс {

public static class TrueStaticSection {

public static boolean debugMode= false;

другие глобальные переменные

}

…

}

ПРАВИЛО B2. Если «динамический» класс нуждается в доступе к некоторым полям, методам, конструкторам или локальным классам некоторого «true-static»-класса, то все эти члены «true-static»-класса обязаны быть public, даже если «динамический» и «true-static»-класс лежат в одном пакете или внутри одного java-файла. Исключение: если «динамический» класс наследует «true-static», то он, как обычно, имеет доступ к protected-членам «true-static»-класса.

Например, если в «динамический» класс вложен локальный «true-static»-класс, то «динамический» класс, вопреки обычной практике, не может пользоваться private-полями или полями с дружественным доступом вложенного класса.

Дело в том, что с точки зрения виртуальной машины Java-классы, загруженные разными загрузчиками и поэтому лежащие в разных пространствах имен, всегда имеют друг с другом столь же слабую связь, как и классы, лежащие в разных пакетах. «Дружественный» доступ или доступ к private-членам вложенного класса между разными пространствами имен «не работает».

Будьте внимательны: подобная ошибка (разумеется) не отслеживается компилятором Java и обнаруживается в виде системного исключения уже при выполнении программы.

Есть еще одно специфическое правило, не вытекающее из описанных выше общих принципов.

ПРАВИЛО C. Если некоторые методы класса являются native – эти методы реализованы в отдельном машинно-зависимом модуле, загружаемом методом System.loadLibrary в секции статической инициализации класса, то такой класс обязан быть «true-static».

Это – внутреннее свойство современной версии Java-машины, по крайней мере, для платформы Windows. Java-машина не допускает повторной загрузки внешнего модуля вызовом loadLibrary с тем же самым именем – такая попытка вызывает исключение. Для «динамических» классов инициализация происходит многократно в каждой версии класса. Если в «динамическом» классе попытаться в соответствии с документацией обратиться к System.loadLibrary в секции статической инициализации:

static {

System.loadLibrary(“Имя_внешнего_модуля”);

}

то уже вторая загрузка такого класса нашим загрузчиком возбудит внутреннее исключение с сообщением: «данный внешний модуль уже загружен другим загрузчиком классов».

Не обесценивают ли описанные сложности нового удобства – возможности «на лету» перекомпилировать и перезагружать классы? Я считаю, что нет – при условии грамотного проектирования системы в целом. На самом деле, описанные выше правила касаются только разработки сравнительно небольшого блока: стандартизованного интерфейса между стационарной и динамической частью. При разработке остальных блоков стационарной части, не работающих с «динамическими» классами, все описанные нюансы несущественны: стационарная часть загружается системным загрузчиком по обычным правилам. При разработке динамических частей программы, расширяющих ее функциональность, возможно, сторонними разработчиками, все эти правила, за исключением очень простого правила C, обычно также можно спокойно игнорировать. Все правила, за исключением C, относятся к доступу к «динамическим» классам из стационарной части и к работе с различными пространствами имен, но никак не затрагивают разработку системы «динамических» классов в пределах одного пространства имен. Поэтому все новые классы, разрабатываемые для расширения динамической части программы при уже выработанном протоколе общения со стационарной частью, почти всегда можно объявлять «динамическими» и спокойно разрабатывать по обычным правилам языка Java.

Странности кэширования: различие loadClass и forName

Здесь мне хотелось бы ненадолго вернуться назад к нашей реализации DynamicClassOverloader. В приведенной выше реализации есть две закомментированные строки с вызовом System.out.println, позволяющим увидеть момент загрузки класса и извлечение его из кэша.

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

Class result= (Class)classesHash.get(name);

if (result!=null) {

…

не срабатывает никогда!

Я пробовал повторно обращаться к загруженным классам всеми возможными способами: прямым обращением к классу, через Class.forName с различными наборами параметров, путем наследования, перехвата исключений и т. д. Во всех случаях, кроме прямого обращения к методу нашего загрузчика loadClass, до исполнения наших методов loadClass и findClass дело просто не доходило. Очевидно, Java-машина реализует дополнительное кэширование загруженных классов. Пока мы явно не подаем указание использовать другой экземпляр загрузчика с помощью соответствующего аргумента метода Class.forName. Все классы, уже загруженные один раз нашим загрузчиком, автоматически извлекаются из какого-то внутреннего кэша. Фактически наше кэширование, реализованное в DynamicClassOverloader, оказалось ненужным – классы и так прекрасно кэшируются.

Я не знаю, насколько можно полагаться на эту особенность Java-машины. Документация к классу ClassLoader рекомендует использовать вызов protected-метода findLoadedClass для выяснения, был ли уже загружен данный класс. Но в моих тестах мне не удавалось этим воспользоваться – этот метод всегда возвращал null.

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

Class.forName(“Имя_класса”,true,loader)

альтернативным вариантом:

loader.loadClass(“Имя_класса”)

Здесь метод loadClass вызывается напрямую – и здесь уже никто кроме нас не позаботится о кэшировании однажды загруженных классов. (В отличие от этого вызов Class.forName всегда обращается к внутреннему кэшу, и, если для данного загрузчика loader класс name уже однажды загружался, он будет найден и возвращен в результате forName без обращения к loadClass и findClass.)

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

loader.loadClass(“Имя_класса”)

loader.loadClass(“Имя_класса”)

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

Class.forName(“Имя_класса”,true,loader)

такую проблему пронаблюдать не удастся, даже если мы не будем реализовывать кэш.

Описанное поведение, как мне кажется, является веским аргументом, чтобы по возможности никогда, за исключением тестов, не использовать прямой вызов

loader.loadClass(“Имя_класса”)

отдавая предпочтение вызову

Class.forName(“Имя_класса”,true,loader)

или

Class.forName(“Имя_класса”,false,loader)

У внимательного читателя при изучении нашего загрузчика DynamicClassOverloader мог возникнуть вопрос. Если наша цель – научить загрузчик «забывать» старые версии классов, почему мы попросту не реализовали в классе DynamicClassOverloader метод invalidate:

public void invalidate() {

classesHash.clear();

}

очищающий наш кэш classesHash? Почему вместо этого мы пошли по более сложному пути: созданию новых экземпляров загрузчика?

Теперь мы можем ответить на этот вопрос. При использовании вызова

Class.forName(“Имя_класса”,true,loader)

метод invalidate, очищающий classesHash, просто не дал бы никакого эффекта. Виртуальная машина все равно извлекала бы класс из внутреннего кэша, пока мы не сменили бы экземпляр loader используемого загрузчика. А при использовании прямого вызова

loader.loadClass(“Имя_класса”)

метод invalidate, вместо ожидаемого «забывания» старых классов, привел бы к низкоуровневому исключению LinkageError.

Применения DynamicClassOverloader

Приведем несколько примеров ситуаций, где можно применить созданный нами загрузчик DynamicClassOverloader помимо решения поставленной нами главной задачи – загрузки измененных версий классов «на лету» без остановки основной программы.

Прежде всего напрашивается простейшее применение: профайлинг загрузки классов. С помощью параметра «-verbose:class» стандартной утилиты «java» можно проследить, какие классы загружаются в процессе работы. Но собственный загрузчик может сделать намного больше. Очень легко дополнить приведенный выше исходный код DynamicClassOverloader сбором любой статистики о загружаемых файлах: сколько классов различных пакетов загружается, сколько времени расходуется на загрузку классов, какие классы загружаются обычно в первую очередь и т. д.

DynamicClassOverloader можно использовать для загрузки классов из нестандартного каталога, не указанного в переменной CLASSPATH. Это требуется не так уж часто, но в некоторых ситуациях без этого сложно обойтись. Например, предположим, что вы пишете систему обработки документов, к которым могут прилагаться специальные Java-классы: как апплеты в веб-страницах или скрипты в документах Microsoft Word. Пользователь может работать с любым числом таких документов, расположенных где угодно в пределах своей файловой системы. Очевидно, для загрузки и исполнения таких классов, сопутствующих документу, стандартный набор путей из CLASSPATH окажется недостаточным.

Наконец, отдельные пространства имен, порождаемые экземплярами нашего загрузчика, из неудобного недостатка могут превратиться в ключевое достоинство. Обычно разработчики Java-классов избегают конфликтов имен своих классов благодаря стандартной системе именования пакетов, когда в имя пакета включается серия вложенных доменов Internet, соответствующих уникальному веб-сайту разработчика. Но если Java применяется в упрощенном виде – скажем, для реализации небольших скриптов конечными пользователями продукта – такая схема именования может оказаться чересчур обременительной. (Те же апплеты редко размещают в пакете. Часто пакет вообще не указывается, т.е. используется корневой package.) Наш загрузчик позволяет надежно изолировать друг от друга подобные простые классы, не нуждающиеся во взаимодействии друг с другом и разработанные, возможно, разными разработчиками. Если два класса загружены разными экземплярами DynamicClassOverloader, они могут спокойно иметь идентичное имя. Они «живут» в разных пространствах имен и не могут ничего «знать» друг о друге.

Заключение

Реализовав собственный загрузчик классов, мы действительно всерьез познакомились с миром отражений Java и много узнали о «внутренней кухне» виртуальной Java-машины. Конечно, это далеко не все.

Я не пытался написать исчерпывающее руководство. Я постарался предложить краткую экскурсию, позволяющую познакомиться с основными понятиями, технологиями и проблемами мира отражений Java и, я надеюсь, позволяющую почувствовать изящество и мощь этого чудесного языка. С помощью всего лишь около сотни строк исходного текста Java мы создали инструмент, позволивший существенно «подправить» поведение Java-машины и изменивший свойства самого языка Java.

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


[1] Это утверждение не совсем точное. На самом деле, вначале загружается целый ряд системных классов, в частности, системный загрузчик и все используемые ими классы. Процесс загрузки классов легко проследить, запустив утилиту «java» с ключом «-verbose:class».

[2] Что касается проверки интерфейса – это традиционное решение, но в данной ситуации реализовать его труднее чем обычно. Не так просто, построив класс – объект Class – вызовом

result= defineClass(name,classBytes,0,classBytes.length)

выяснить, не является ли он наследником данного интерфейса, скажем, TrueStatic. Простая проверка вроде

TrueStatic.class.isAssignableFrom(result)

не сработает – ведь интерфейс TrueStatic и класс result, скорее всего, загружены разными загрузчиками и лежат в разных пространствах имен. Одно из допустимых решений – получить список интерфейсов, реализуемых result, вызовом «result.getInterfaces()» и затем проверить их имена.

http://www.samag.ru/cgi-bin/go.pl?q=articles;n=01.2003;a=15


July 27th, 2010  
Tags: Java, Java Reflection, java.lang.ClassLoader



Java: Mагия отражений

Java 0 Comment »

Часть I. Основы

Даниил Алиевский

Один из самых удивительных и ярких механизмов языка Java – технология «отражения» (Java Reflection). К сожалению, в популярных учебниках нелегко найти подробную информацию об этой интереснейшей области. А тем более – о подводных камнях и неожиданных возможностях, возникающих при программировании с использованием отражений. Между тем, именно отражения позволяют Java с неподражаемым изяществом справляться с задачами, традиционно непростыми в других языках – такими, как создание оболочки для компиляции Java-проектов (наподобие Borland JavaBuilder или NetBeans), визуальное проектирование графических компонентов (JavaBeans), сериализация объектов и распределенные вычисления (RMI), и многие другие.

Мне бы хотелось предложить вам небольшую экскурсию по миру отражений Java. По ходу дела я буду показывать, как использовать мощь этой технологии для изящного решения различных практических задач.

Мы начнем с простых вещей – простых для Java, но не столь тривиальных в других языках. Пример такой «простой вещи» – динамическое расширение вашей программы новыми классами от сторонних разработчиков, не требущее переписывания и даже перекомпиляции вашей системы. По мере знакомства с отражениями, задачи будут усложняться. В качестве завершающего примера, полностью использующего возможности отражений, я расскажу, как реализовать высокоэффективный интерпертатор выражения (формулы), написанного на языке Java – эквивалент оператора eval() в скриптовых языках типа Perl или JavaScript.

Все написанное ниже относится к последней (на момент написания статьи) версии Java: Sun Java SDK 1.4.

Где искать мир отражений?

Отражения в Java – это два класса Class и ClassLoader, расположенных в пакете java.lang, и специальный пакет java.lang.reflect, содержащий (в версии Java SDK 1.4) 12 вспомогательных классов: Array, Member, Constructor, Field, Method, Modifier, InvocationHandler, Proxy, ReflectAccess, ReflectPermission, InvocationTargetException, UndeclaredThrowableExceptioCfn.

Проще всего осваивать технику отражений, начиная с класса Class. Более сложные вещи потребуют применения классов из пакета java.lang.reflect, прежде всего классов, описывающих: Constructor, Field и Method. «Высший пилотаж» работы с отражениями – это, пожалуй, создание грамотных наследников ClassLoader, позволяющих реализовать собственную систему загрузки классов.

В этой статье я покажу, как грамотно использовать эти классы и их методы.

В любом случае, статья не заменяет справочную документацию фирмы Sun. Самую полную и свежую фирменную документацию можно найти в Интернете по адресу: http://java.sun.com.

Класс по имени Class

В мире отражений все начинается с класса «java.lang.Class». Для каждого Java-класса или интерфейса, присутствующего в системе, существует экземпляр Class, содержащий подробную низкоуровневую информацию об этом классе. Например, с помощью этого экземпляра можно узнать списки членов класса – полей, методов, конструкторов, а также обратиться к этим членам. Кроме того, класс Class содержит ряд статических методов, обеспечивающих взаимодействие с внутренними механизмами Java, отвечающими за загрузку и управление классами.

Как получить экземпляр Class, соответствующий данному классу (или интерфейсу) – например, классу java.io.File? Для этого есть два основных способа.

A. Просто добавляем к имени класса суффикс «.class», например:

Class clazz= byte.class

(«clazz» – сознательно искаженное от «class»: компилятор не позволяет использовать в качестве идентификатора зарезервированное слово «class».)

B. Если мы располагаем экземпляром некоторого класса, может быть даже неизвестного в данной точке программы, можно вызвать метод getClass(), присутствующий в каждом Java-объекте (унаследованный от класса Object):

void myFunction(Object o) {

Class clazz= o.getClass();

что-то делаем с обьектом clazz;

}

…

java.io.File f= new java.io.File(“/tmp/1.txt”);

myFunction(f);

…

Здесь есть любопытный нюанс. Вообще-то, примитивные типы Java – boolean, char, byte, short, int, long, float, double – обычно не считаются полноценными классами. Они не унаследованы от Object, для них не работает наследование и т.д. Тем не менее, с ними тоже ассоциированы экземпляры Class, которые можно получить способом A, например:

Class clazz= java.io.File.class;

Существует даже специальный объект void.class – он используется в довольно экзотических ситуациях при вызове методов через отражения. Экземпляры типа Class есть также у любого массива, например:

Class clazz= byte[].class

или

byte[] v= new byte[34]; Class clazz= v.getClass();

Что же можно сделать, располагая переменной типа Class для некоторого класса?

Прежде всего, можно получить полное имя класса (скажем, для отладочной печати) методом getName(). Например: String.class.getName() возвращает «java.lang.String».

Интересно посмотреть на имена классов для примитивных типов и для массивов:

float.class.getName() возвращает «float»

float[].class.getName() возвращает «[F»

float[][].class.getName() возвращает «[[F»

String[].class.getName() возвращает [Ljava.lang.String;»

Полностью алгоритм построения такого имени описан в документации на Java.

Можно проверить, не является ли класс интерфейсом, массивом или примитивным типом: методы isInterface(), isArray() и isPrimitive(). Если класс – массив, можно получить тип его элементов (т.е. соответствующий объект Class): метод getComponentType(). Для не-массивов этот метод вернет null.

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

public static int sizeOfArray(Object v) {

if (v==null) return 0;

Class componentClass= v.getClass().getComponentType();

if (componentClass==null)

throw new IllegalArgumentException("Not an array");

int s= componentClass==byte.class? 2:

componentClass==short.class? 4:

componentClass==char.class? 4:

componentClass==int.class? 4:

componentClass==float.class? 8:

componentClass==long.class? 8:

componentClass==double.class? 16:

-1;

// не обрабатываем тип boolean: для него размер элемента не специфицирован документацией по языку Java

if (s==-1)

throw new IllegalArgumentException("Unknown component type");

return java.lang.reflect.Array.getLength(v)*s;

// метод getLength() класса java.lang.reflect.Array получает на входе массив произвольного типа и возвращает его длину

}

java.lang.reflect.Array – «странный» способ работать с массивами

Прежде чем переходить к более мощным (и ценным) возможностям класса Class, давайте обратим внимание на java.lang.reflect.Array. Этот очень простой класс из пакета java.lang.reflect представляет собой библиотеку статических методов, позволяющих выполнять все основные примитивные действия с массивом неизвестного (на этапе компиляции) типа, представленного общим типом данных Object.

Пример мы видели в приведенной выше функции sizeOfArray() – метод getLength(). Этот метод объявлен в исходном коде фирмы Sun следующим образом:

public static native int getLength(Object array)

throws IllegalArgumentException;

Любой массив в Java является объектом, т.е. наследником Object, поэтому его можно передать в getLength() в качестве параметра. На самом деле, только массивы и можно передавать в этот метод – как и в большинство других методов класса Array. Для объектов других типов методы класса Array возбуждают исключение IllegalArgumentException.

Большинство остальных методов класса Array предназначены для чтения и записи элементов в массив:

public static Object get(Object array, int index)

возвращает элемент номер index; если массив состоит из элементов примитивного типа (byte, char и т.п.), возвращается объект-оболочка (Byte, Char, ...);

public static boolean getBoolean(Object array, int index),

public static char getChar(Object array, int index)

и аналогичные методы для типов byte, short, int, long, float, double

специальные версии get на случай, когда массив состоит из элементов соответствующего примитивного типа;

public static void set(Object array, int index, Object value),

public static void setBoolean(Object array, int index, boolean z),

public static void setChar(Object array, int index, char c)

и т.д.

обратные функции для записи элементов в массив.

Наконец, можно создать массив c помощью одного из двух методов:

public static Object newInstance(Class componentType, int length),

public static Object newInstance(Class componentType, int[] dimensions)

Первый метод удобен для одномерных массивов, второй – позволяет создавать сразу многомерные массивы. В качестве componentType передается класс элементов – например, один из классов примитивных типов boolean.class, char.class и т.д.

Все подробности – в документации фирмы Sun.

На первый взгляд у неискушенного Java-программиста все это вызывает некоторое недоумение. Зачем столь замысловатым способом манипулировать с массивом, когда можно просто воспользоваться встроенными языковыми средствами? Зачем писать:

v.getByte(n)

для массива v типа byte[], когда можно просто написать v[n]?

Ответ – класс Array позволяет писать универсальные функции, принимающие на вход массив произвольного, неизвестного заранее типа. Пример такой функции мы уже видели – sizeOfArray() из предыдущего пункта. Если бы не класс Array, нам бы либо пришлось написать 7 версий этой функции для каждого варианта массивов примитивных типов, либо заменить вызов java.lang.reflect.Array.getLength(v) чем-то вроде:

v instanceof byte[]? ((byte[])v).length:

v instanceof short[]? ((short[])v).length:

v instanceof char[]? ((char[])v).length:

v instanceof int[]? ((int[])v).length:

v instanceof long[]? ((long[])v).length:

v instanceof float[]? ((float[])v).length:

v instanceof double[]? ((double[])v).length: -1

Приведем более практичный пример использования класса Array. Достаточно часто возникает задача преобразовать произвольный объект в текстовую строку – например, в целях отладки. Предназначенный для этого метод toString() обычно выдает достаточно внятную информацию о содержимом объекта. Но, к сожалению, для массивов этот метод ведет себя весьма тупо – просто возвращает внутренний адрес массива.

Я написал простую функцию toS(Object v, String separator), преобразующую произвольный массив в строку. Все элементы массива преобразуются (стандартным образом) в строки и конкатенируются через разделитель separator, заданный в качестве параметра функции. Вот текст этой функции:

public static String toS(Object v, String separator) {

if (v==null) return “”;

if (v.getClass().isArray()) {

int len;

if ((len=java.lang.reflect.Array.getLength(v))==0) return “”;

StringBuffer result= new StringBuffer();

for (int k=0; k<len; k++) {

if (k>0) result.append(separator);

result.append(String.valueOf(

java.lang.reflect.Array.get(v,k)));

}

return result.toString();

}

return String.valueOf(v);

}

Если аргумент v не является массивом, действие toS не отличается от стандартного метода v.toString(). Если v==null, возвращается пустая строка (обычно это удобнее стандартной реакции – возврата строки «null»).

Без класса Array пришлось бы написать 9 вариантов такой функции – для 8 примитивных типов и для массива объектов произвольного типа Object[].

Здесь нужно сделать одно важное замечание. Хотя класс Array действительно позволяет существенно экономить текст программы и не писать разные варианты метода для массивов разных типов, следует иметь в виду – получаемый код сравнительно неэффективен. Скажем, цикл суммирования всех элементов числового массива через вызов java.lang.reflect.Array.getDouble() будет работать на порядок дольше банального:

double s= 0.0;

for (int k=0; k<v.length; k++) s+= v[k];

В случае функции toS() разница была бы непринципиальной, так как преобразование числа в строку – сравнительно медленная операция.

Class.getResourceAsStream() – ресурсы

Один из самых очевидных примеров использования класса Class – загрузка ресурсов.

Ресурс в Java – это файл с данными, прилагаемый к вашей программе и обычно размещаемый «рядом» с class-файлами. В графических приложениях и апплетах это чаще всего изображения (jpg- или gif-файлы), но с таким же успехом это может быть и файл специального формата – например, содержащий справочные таблицы или какие-либо ваши объекты.

Наиболее общий способ прочитать файл-ресурс произвольного формата, расположенный «рядом» с некоторым классом – обратиться к методу getResourceAsStream() объекта Class, соответствующего данному классу. В качестве параметра этому методу нужно передать путь к файлу ресурса относительно каталога, в котором размещен ваш класс. В качестве результата getResourceAsStream() вернет объект InputStream, с помощью которого можно прочитать файл ресурса стандартными средствами ввода/вывода Java.

Для наиболее популярных типов ресурсов, таких как изображения, обычно существуют более удобные способы прочитать ресурс – например, метод getImage() класса java.applet.Applet. Но для текстовых файлов и файлов нестандартного формата getResourceAsStream(), как правило, – самое разумное решение.

Вот пример законченного класса, использующего эту технику:

import java.io.*;

public class MyClassWithResource {

public static final String myTextResourceName= “mydata.txt”;

public static final String myTextResource;

static {

String s= “”;

try {

InputStream stream= MyClassWithResource.class.getResourceAsStream(myTextResourceName);

if (stream==null)

throw new FileNotFoundException(myTextResourceName+” not found”);

StringBuffer sb= new StringBuffer(stream.available());

InputStreamReader reader= new InputStreamReader(stream);

char[] buf= new char[32768];

int len;

while ((len=reader.read(buf,0,buf.length))>=0) {

sb.append(buf,0,len);

}

s= sb.toString();

} catch (IOException e) {

e.printStackTrace();

}

myTextResource= s;

}

public static void main(String[] args) {

System.out.println(“Loaded resource:”);

System.out.println(myTextResource);

}

}

В этом примере файл «mydata.txt» должен быть расположен в том же каталоге, что и class-файл «MyClassWithResource.class».

Файл ресурса необязательно размещать в том же каталоге, что и class-файл. Если он расположен в одном из подкаталогов этого каталога, в качестве имени ресурса нужно передать относительный путь, разделяя имена подкаталогов символом «/» (как это принято в Internet и Unix). Можно также указать в качестве имени ресурса «абсолютный» путь, начинающийся с символа «/». Тогда Java будет искать ресурс во всех каталогах, перечисленных в путях поиска классов CLASSPATH – т.е. по тем же правилам, по которым отыскиваются class-файлы программы.

Может возникнуть вопрос – зачем нужен специальный метод класса Class, когда можно прочитать файл ресурса обычными средствами файлового ввода/вывода?

Основная причина – использование метода getResourceAsStream() является гораздо более общим решением, работающим в большем числе ситуаций.

Например, по традиции, законченные наборы классов – Java-приложения или библиотеки – принято упаковывать в архивы JAR и устанавливать на компьютер именно в таком виде. Классы Java прекрасно загружаются непосредственно из архива JAR, без предварительной распаковки. То же самое относится и к ресурсам, загружаемым методом getResourceAsStream() – или более специальными методами типа java.applet.Applet.getImage(). В то же время, обычные средства файлового ввода/вывода для чтения ресурса из JAR уже непригодны – нужно использовать специальные классы для анализа и чтения JAR-файлов.

Аналогичная ситуация – апплеты, когда файлы ресурсов и class-файлы находятся на сервере. Метод getResourceAsStream() в этом случае обеспечит чтение ресурса с сервера через Internet.

Тем не менее, в некоторых случаях для чтения ресурса все-таки может быть целесообразным использование традиционных средств файлового ввода/вывода – конечно, если вы не используете JAR и пишете сервлет или приложение, а не апплет. Например, для достижения максимальной производительности может понадобиться отобразить ресурс в память средствами отображения файлов из пакета java.nio, появившегося в Java начиная с версии SDK 1.4. Или, может быть, вы захотите просканировать каталог с ресурсами и загрузить все файлы с определенным расширением – для сканирования каталога в классе Class нет соответствующих средств.

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

Ниже приведен текст функции, возвращающей полный путь к class-файлу.

public static java.io.File getClassFile(Class clazz) {

// The file will exist only if it is usual class-file,

// not a part of JAR or Web resource

String s= clazz.getName();

s= s.substring(s.lastIndexOf(“.”)+1);

s= clazz.getResource(s+”.class”).getFile();

try {

s= java.net.URLDecoder.decode(s,”UTF-8″);

} catch(java.io.UnsupportedEncodingException e) {

}

return new java.io.File(s);

}

Прежде всего мы получаем имя класса без имени пакета, добавляем к нему расширение «.class» и передаем методу getResource() объекта Class. Этот метод возвращает экземпляр класса java.net.URL, представляющий файл в виде универсального пути к ресурсу (URL, Universal Resource Locator). В нашем случае (когда классы расположены в обычных файлах в локальной файловой системе) URL будет выглядеть примерно так:

file://путь_к_дисковому_каталогу/имя_класса.class

Метод getFile() объекта URL «отрежет» префикс «file://», оставив все остальное без изменений.

А вот дальше начинаются сложности. Оставшийся путь к файлу записан в стандарте URL, который может отличаться от формата имен файлов, принятого в текущей файловой системе.

Первое отличие: в URL подкаталоги всегда разделяются прямым слэшем /, в то время как в операционных системах, отличных от Unix, могут использоваться другие символы (например, обратный слэш \ в случае Windows). Это различие несущественно для Java – классы пакета java.io прекрасно будут работать и с именем файла, записанным через прямые слэши.

Но есть и второе отличие. Если путь к файлу содержит пробелы, русские буквы или другие символы, недопустимые в стандарте URL, то они будут закодированы комбинациями вида %XX, где XX – ASCII-код символа. Такое имя файла непригодно для обработки средствами ввода/вывода Java. Для восстановления «нормального» имени файла нужно вызвать метод java.net.URLDecoder.decode(), обязательно указав при этом кодировку символов (encoding).

Опытным путем я установил, что для кодирования не-латинских букв в имени файла Java использует кодировку UTF-8 – по крайней мере, на Windows-платформе. К сожалению, я не нашел в документации прямых указаний, что это всегда будет так на всех платформах. Поэтому я бы порекомендовал, по возможности, все же размещать свои классы в каталогах, путь к которым состоит только из латинских символов – тогда приведенная выше функция будет достаточно надежна.

Class.forName() и Class.newInstance() – динамическая загрузка классов

Мы подошли к рассмотрению по-настоящему важных и интересных технологий мира отражений. Речь пойдет о фундаметальном и чрезвычайно мощном механизме Java: динамической загрузке произвольного класса по заданному имени. Эта возможность встроена непосредственно в Java и реализуется классом Class. В других языках типа С++ или Delphi аналогичных целей можно достигнуть, используя специальные средства конкретной операционной системы (типа загрузки dll в Windows функцией loadLibrary()), но в Java это сделано по-настоящему удобно.

Итак, вашему вниманию предлагается статический метод Class.forName(). Вот его формальное объявление:

public static Class forName(String className)

throws ClassNotFoundException

Метод отыскивает в системе (среди путей поиска классов CLASSPATH) класс с заданным именем className и возвращает соответствующий экземпляр класса Class. Имя className должно быть полным, т.е. включать имя пакета. Например:

Class clazz= Class.forName(«java.lang.String»);

Если такой класс отсутствует, возбуждается исключение ClassNotFoundException.

После получения переменной типа Class, следующее наиболее типичное действие – создание экземпляра только что загруженного класса. Для этого служит метод Class.newInstance(). Его объявление:

public Object newInstance()

throws InstantiationException, IllegalAccessException

В нашем примере вызов newInstance() мог бы выглядеть так:

Object object= clazz.newInstance();

Чтобы этот метод мог выполниться, у загруженного класса должен существовать пустой конструктор (без аргументов), либо – что по существу то же самое – не должно быть описано вообще никаких конструкторов. В противном случае будет возбуждено исключение InstantiationException.

Конечно, этого еще мало, чтобы работать с полученным объектом. Если вы не знаете, что умеет делать класс – какие у него есть методы, что они ожидают получить на входе и для чего они предназначены – вы не сможете извлечь из него ничего полезного. Для формального определения, что «умеет» класс, в Java существует стандартный механизм – интерфейсы. Остается просто применить этот механизм.

Например, предположим, ваша программа должна в некоторые моменты выполнять перевод с одного языка на другой. Формально это можно описать интерфейсом:

public interface LanguageTranslator {

public String translate(String source,String sourceLanguage,String targetLanguage);

// переводит текст source с языка sourceLanguage на язык targetLanguage

}

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

String source= “текст, требующий перевода”;

String sourceLanguage= “Russian”;

String targetLanguage= “English”;

Class clazz= Class.forName(“полное_имя_класса_переводчика”);

Object object= clazz.newInstance();

if (!(object instanceof LanguageTranslator)) {

throw new Exception(“…”);

// сообщаем об ошибке: указанный класс не реализует требуемый интерфейс, т.е. не является переводчиком

}

String result= ((LanguageTranslator)object).translate(source,sourceLanguage,targetLanguage);

Описанная техника может оказаться очень полезной практически в любой достаточно большой и серьезной системе, рассчитанной на разработку многими участниками. Многие компоненты таких систем являются достаточно изолированными, и их набор может быть совершенно неизвестен на этапе компиляции основной программы – известны лишь интерфейсы, который они обязуются реализовывать. Например, так обычно строятся системы plugin’ов – модулей, добавляемых к уже работающей системе. В подобных случаях механизм отражений – методы Class.forName() и Class.newInstance() – становится единственным грамотным решением.

Constructor, Field, Method – работа с классами через отражения

На самом деле технология отражений позволяет сделать гораздо больше, чем просто загрузить по имени некоторый (заранее неизвестный) класс и создать его экземпляр. Можно получить полный список всех конструкторов, полей и методов класса и обратиться к любому из них, передав (в случае конструктора или метода) список всех параметров. Для этого служат следующие методы класса Class:

public Constructor[] getConstructors(),

public Field[]       getFields(),

public Method[]      getMethods(),

public Constructor[] getDeclaredConstructors(),

public Field[]       getDeclaredFields(),

public Method[]      getDeclaredMethods()

Перечисленные методы возвращают массивы объектов типа Constructor, Field и Method. Эти классы содержатся в пакете java.lang.reflect и обеспечивают исчерпывающий доступ к полям, конструкторам и методам.

Сразу бросается в глаза наличие двух версий методов: getXXX и getDeclaredXXX (где XXX – «Constructors», «Fields» или «Methods»). Поначалу это может даже несколько сбить с толку – какой версией следует пользоваться?

getDeclaredXXX возвращает список членов класса (конструкторов, полей или методов), объявленных при описании класса, но не унаследованных от классов-предков. При этом в список включаются все члены, независимо от их уровня защиты – т.е. public, protected, private и дружественные члены.

getXXX возвращает полный список членов, объявленных в самом классе либо унаследованных от одного из предков. Но в этот список уже попадают только public-члены.

При желании, конечно, можно получить и максимально полную информацию – список всех членов, объявленных в самом классе либо в любом из его предков. Для этого достаточно организовать цикл по цепочке классов-предков, пользуясь специальным методом Class.getSuperclass().

Кроме получения полного списка, можно также отыскать в классе конкретный член. Для этого служат методы:

public Constructor   getConstructor(Class[] parameterTypes),

public Field       getField(String name),

public Method   getMethod(String name, Class[] parameterTypes),

public Constructor getDeclaredConstructor(Class[] parameterTypes),

public Field       getDeclaredField(String name),

public Method     getDeclaredMethod(String name, Class[] parameterTypes)

(Собственно, чаще используются как раз эти методы, а не описанные выше методы получения списков.)

Аргумент name в этих методах должен содержать имя требуемого члена. Аргумент parameterTypes связан с возможностью Java перегружать конструкторы и методы – определять несколько конструкторов, либо несколько методов с одинаковым именем, отличающихся только типами параметров. (В случае конструкторов это единственный способ создать много конструкторов класса.) В качестве parameterTypes нужно отдать массив объектов типа Class, соответствующих типам всех параметров конструктора или метода.

Разница между вариантами getXXX и getDeclaredXXX здесь та же самая, что и в случае методов получения списков.

Здесь я бы порекомендовал написать небольшой тест, который распечатает списки всех конструкторов, полей и методов (объявленных и унаследованных) какого-нибудь класса. Для преобразования объектов Constructor, Field, Method в строки можно использовать стандартный метод toString(). Попробуйте распечатать такие списки для классов без конструкторов, с пустым конструктором, с конструктором, обладающим параметрами, с private- и public-полями. Попробуйте унаследовать класс и переопределить в нем (под тем же именем) public-, protected- или private-поле. Такой текст хорошо помогает понять, как в Java устроены классы, конструкторы и наследование.

Как реально работать с классами Constructor, Field, Method?

Прежде всего, заметим, что все они реализуют общий интерфейс Member, позволяющий:

n  получить символьное имя члена (конструктора, поля или метода) с помощью метода:

public String getName();

n  получить обратную ссылку на класс, в котором объявлен данный член (т.е. тот класс, который возвращает этот член в соответствующем массиве getDeclaredXXX()), с помощью метода:

public Class getDeclaringClass();

n  получить набор битовых флагов – модификаторов данного члена с помощью метода:

public int getModifiers();

Модификаторы – это битовые флаги, описывающие, обладает ли данный член следующими свойствами: public, private, protected, static, final, synchronized, volatile, transient, native, abstract или strictfp. Полный список модификаторов и средства работы с ними можно найти в классe java.lang.reflect.Modifier – см. документацию фирмы Sun.

Кстати заметим, что некоторые модификаторы (например, public) определены также у классов; их можно получить методом Class.getModifiers(). Для классов определен еще один модификатор – java.lang.reflect.Modifier.INTERFACE, означающий, что объект Class ассоциирован не с классом, а с интерфейсом.

Главное назначение классов Constructor, Field, Method – конечно, получить доступ к соответствующим членам, т.е. в случае конструкторов – создать экземпляр класса, в случае полей – прочитать или изменить поле, в случае методов – вызвать метод.

Это делается достаточно просто. Чтобы не дублировать документацию по Java, я просто приведу пример готового тестового класса, где использованы все основные приемы доступа к членам:

import java.lang.reflect.*;

public class TestClass {

public int a;

public TestClass(int a)  {this.a= a;}

public void b()          {a= 1;}

public void b(int p1)    {a= p1;}

public String toString() {return a+”";}

public static void main(String[] args) throws Exception {

Class clazz= TestClass.class;

Constructor c= clazz.getConstructor(new Class[] {int.class});

Object o= c.newInstance(new Object[] {new Integer(23)});

Field f= clazz.getField(“a”);

System.out.println(f.getInt(o));

f.setInt(o,24);

System.out.println(o);

Method m= clazz.getMethod(“b”,new Class[] {});

m.invoke(o,new Object[] {});

System.out.println(o);

m= clazz.getMethod(“b”,new Class[] {int.class});

m.invoke(o,new Object[] {new Integer(2)});

System.out.println(o);

}

}

Создание экземпляра объекта с помощью конструктора похоже на вызов метода Class.newInstance(). Но в данном случае можно легко создать экземпляр класса, не обладающий конструктором без параметров – нужно только знать список типов параметров требуемого конструктора.

Для передачи параметров в конструкторы и методы используется массив объектов (типа Object[]). Если нужно передать параметр примитивного типа, он «заворачивается» в соответствующий класс-оболочку – в случае int это объект Integer. Такое «заворачивание» – традиционная практика в мире отражений.

Для доступа к полю в классе Field реализованы общие методы get() и set(), рассчитанные на произвольный объект, и частные версии getBoolean(), getInt(), …, setBoolean(), setInt(), …, рассчитанные на примитивный тип поля. Первым параметром у этих методов всегда передается экземпляр объекта, к полю которого нужно получить доступ.

Для вызова метода служит метод invoke() класса Method. Ему передается экземпляр объекта, метод которого следует вызвать, и список параметров в виде массива Object[].

Если метод класса возвращает результат (а не описан как void, как в данном примере), этот результат будет возвращен в качестве результата invoke – в виде общего типа Object. (Примитивный тип в этом случае, как обычно, «заворачивается» в класс-оболочку.)

Располагая объектом Constructor или Method, можно узнать список типов всех параметров в виде массива Class[]: для этого служат методы Constructor.getParameterTypes() и Method.getParameterTypes(). В случае метода можно также узнать тип результата: Method.getReturnType(). Кстати, это та самая экзотическая ситуация, когда находит применение исключительно редкий объект void.class (я упоминал об этой объекте в разделе 2): void.class будет возвращен getReturnType(), если метод объявлен как void.

С помощью классов Field и Method можно также получить доступ к статическим полям и методам класса, даже не создавая экземпляра объекта. При этом в качестве первого параметра методов getXXX(), setXXX() или invoke() допускается передать null. Можно, скажем, легко написать Java-код, аналогичный по действию стандартной утилите «java» – а именно, запускающий статическую функцию «public static void main(String[] args)» у произвольного класса. Техника интерфейсов, описанная в предыдущем разделе, не позволила бы это сделать.

Обход защиты Java

В очень многих учебниках по Java подчеркивается, что модификаторы protected и private вместе с «дружественным» уровнем доступа (отсутствие модификаторов) позволяют обеспечить «100-процентную защиту» ваших полей и методов от использования посторонними классами. Например, вы никогда не сможете добраться до поля

private char value[];

объявленного в исходном коде класса String и представляющего реальное содержимое строки.

На самом деле все это не так. Смотрите, как можно добраться до этого самого поля и «нелегально» изменить строку – вопреки известному утверждению, что тип String является абсолютно неизменяемым:

import java.lang.reflect.*;

public class HackString {

public static void main(String[] args) throws Exception {

String s= “Hello!”;

System.out.println(s);

Field f= s.getClass().getDeclaredField(“value”);

// Именно getDeclaredField, а не getField: последний метод просто не нашел бы скрытого поля

f.setAccessible(true);

char[] value= (char[])f.get(s);

value[5]= ‘?’;

System.out.println(s);

}

}

Здесь «магический» метод – setAccessible(). Этот метод (и симметричный getAccessible()) имеется у всех классов Constructor, Field, Method и предназначен специально для того, чтобы отключить стандартную проверку модификаторов, осуществляемую Java-машиной.

(Естественно, все это сработает только при условии, что в вашей версии Sun Java SDK реализация класса String точно так же основана на private-поле «char value[]». Так как это поле скрытое, фирма Sun вправе в любой момент переименовать его или вообще заменить чем-нибудь другим.) Спрашивается – зачем же это сделано? И разве это не является брешью в системе безопасности?

Что до второго вопроса – разумеется, метод setAccessible() контролируется менеджером безопасности Java (так же как, например, работа с файлами), и никакая мало-мальски защищенная Java-система не позволит злоупотребить подобной возможностью.

А чтобы понять, зачем это нужно, взгляните на любую среду разработки Java-проектов – скажем, NetBeans или JavaBuilder. Традиционная возможность подобных сред – показать все поля и методы некоторого класса, например, визуальной компоненты – в том числе и скрытые, а в некоторых случаях – дать возможность отредактировать значения полей. Язык Java уникален в том отношении, что подобные действия можно легко выполнить совершенно законными средствами самого языка, не прибегая, скажем, к анализу исходного текста программы.

На самом деле неограниченный доступ ко всем конструкторам, полям и методам даже гораздо более ценен. Этот механизм мира отражений дает возможность удобно и естественно реализовать чрезвычайно мощные технологии, нереализуемые (или крайне сложно реализуемые) другими способами.

Приведем два примера. Во-первых, в Java поддерживается механизм сериализации. Достаточно реализовать в вашем классе пустой интерфейс-индикатор java.io.Serializable, и появляется возможность полностью (т.е. со всеми полями и вложенными объектами) записать этот объект в поток java.io.ObjectOutputStream и впоследствии прочитать из потока java.io.ObjectInputStream.

Те, кто изучал механизм сериализации Java, согласятся – во многом этот механизм напоминает черную магию. Каким-то образом классы java.io.ObjectInputStream и java.io.ObjectOutputStream, без всяких дополнительных подсказок со стороны разработчика класса, «догадываются», как записать или прочитать все поля объекта, включая private-поля. Более того, для «подсказки», когда она все же требуется, используются private-методы writeObject() и readObject() – классы java.io.ObjectOutputStream и java.io.ObjectInputStream каким-то образом обнаруживают и вызывают эти методы.

Конечно же, в действительности эта «черная магия» – не что иное, как магия отражений. Описанные выше технологии, в том числе метод setAccessible(), в принципе,  позволяют даже разработать свою собственную схему сериализации, отличную отстандартной, предлагаемой фирмой Sun.

Второй пример – технология RMI. Это чрезвычайно мощный механизм, позволяющий распределять вычисления по сети – так, чтобы с точки зрения кода Java вызов метода выглядел, как обычно, а на самом деле этот метод отрабатывался на другом компьютере. Здесь тоже не обойтись без технологий отражения. Только они позволяют динамически превратить объект (со всеми своими полями) в поток данных, передать его по сети, реконструировать объект на другом компьютере и вызвать нужный метод – и все это скрыто от пользователя класса.

Конечно, все это не повод, чтобы использовать отражения не по назначению для «взлома» защиты Java. Отражения следует использовать только тогда, когда без этого нельзя обойтись – иначе вы потеряете все преимущества, которые дает идеология объектно-ориентированного программирования.

Заключение

Я постарался описать самые, на мой взгляд, важные и интересные аспекты технологии отражений. Статья – не справочник и не учебник. Многие вещи «остались за бортом». Не все методы классов были описаны; некоторые специфичные классы, такие как java.lang.reflect.Proxy, я вообще не рассматривал. Чтобы получить полную и точную информацию, всегда лучше обращаться к первоисточнику – документации фирмы Sun. Кроме сайта http://java.sun.com, документацию почти всегда можно найти в комплекте поставки Java или извлечь из комментариев к исходным текстам фирмы Sun.

Я также хотел бы порекомендовать книгу: «Язык Программирования Java», К.Арнолд, Дж.Гослинг, Д.Холмс, Издательский дом «Вильямс», Москва – С.-Петербург – Киев, 2001. Это наиболее грамотная из попадавшихся мне книг на русском языке. Она написана сотрудниками фирмы Sun, участвовавшими в разработке технологии Java – т.е. в некотором роде является первоисточником.

http://www.samag.ru/cgi-bin/go.pl?q=articles;n=10.2002;a=10


July 27th, 2010  
Tags: Java, Java Reflection



Создание современных web-приложений при помощи Google Web Toolkit

Java 0 Comment »

Материал предоставлен редакцией журнала Системный администратор.
Опубликовано в журнале "Системный администратор" N 1 2010 

   Любое интернет-приложение эпохи WEB2 - это прежде всего "богатый" и
   функциональный пользовательский интерфейс, выполненный с использованием
   технологии Ajax, а следовательно, JavaScript

   Его создание - довольно непростая, требующая скрупулезности задача, на
   решение которой тратишь немало времени - на отладку, обеспечение
   кроссбраузерной совместимости, борьбу с капризами JavaScript и прочие
   вещи, мало имеющие общего с разработкой и проектированием реализации
   бизнес-логики. К счастью, уже создано немало инструментов, облегчающих
   эту задачу. Фрэймворк Google Web Toolkit пусть и не единственная
   предназначенная для этого среда, но она уже успела себя зарекомендовать
   полнофункциональными, работоспособными RIA-приложениями.

Что это? Зачем?

   На первый вопрос ответить легко - это фрэймворк-среда, набор средств и
   API-интерфейсов для разработки веб-приложений. Её отличительная
   особенность в том, что вся разработка и, что существенно, отладка
   ведётся на Java с помощью привычной IDE (сейчас существуют плагины для
   Eclipce и NetBeans) с последующей компиляцией готового приложения в
   HTML/JavaScript. Собственно, реальность использования сред разработки
   частично является ответом на второй вопрос. Создавать сложное
   веб-приложение на строго типизированном ООП-языке, с возможностью
   нормального проектирования в любимой IDE, "человеческой" отладкой,
   Unit тестированием ещё недавно казалось недостижимой мечтой.

   Google Web Toolkit во многом является воплощением этой мечты в жизнь.
   Процесс отладки приложения здесь гораздо более лёгок и эффективен, так
   как наиболее распространённые ошибки в JavaScript теперь всплывают во
   время компиляции, а не выполнения, а такие ошибки, как несоответствие
   типов или отсутствие необходимых методов, выявляются ещё на стадии
   написания кода. Подсказки и автодополнение - нормальный функционал IDE,
   хоть и не жизненно важный, но довольно существенно повышающий
   производительность, а полноценный рефакторинг (который также теперь
   доступен) в современных условиях просто необходим.

   Отдельно следует упомянуть про ООП-разработку. Реализация этой
   концепции в JavaScript вызывает много справедливых нареканий и является
   причиной неоправданной сложности разработки. В среде GWT вы
   программируете на Java, и никаких проблем такого рода не возникает.

   Кроме того, при написании веб-приложений теперь можно использовать
   такие привычные утилиты, как Jprofiler или Junit.

Как это работает?

   Код пишется и отлаживается на Java с использованием типов данных из
   пакетов java.lang и java.util, а также с новыми классами,
   предоставляемыми GWT. Поддерживаются все внутренние типы Java (в том
   числе и Object). Поддерживается работа с исключениями, в том числе и
   определяемыми пользователем.

   После отладки приложения при помощи GWT-компилятора создаётся
   приложение, использующее традиционные веб-технологии -
   HTML/JavaScript/XML/JSON, которое для GWT-приложения является аналогом
   бинарного представления в Java. Но GWT это не совсем Java!

   Прежде всего не поддерживается Reflection и динамическая загрузка
   классов (что естественно, этот механизм просто невозможно перенести в
   JavaScript). Не поддерживается сериализация. Не поддерживается
   модификатор Strict Floating Point.(strictfp), предписывающий
   "строгую" арифметику для чисел с плавающей точкой. Нет финализации
   объекта перед сборкой мусора.

   GWT-приложение как любое веб-приложение состоит из клиентской
   (обрабатываемой в браузере) и серверной частей. Весь код, отвечающий за
   клиентский функционал, в процессе компиляции переводится в
   html/javascript и, следовательно, имеет ряд естественных ограничений.

   Серверная часть приложения может вызываться клиентским кодом
   посредством асинхронных RPC-запросов. При этом серверный код
   выполняется отдельно от клиентского и не имеет каких-либо ограничений,
   накладываемых компиляцией в JavaScript. Более того, нет ограничений в
   выборе технологий реализации серверной части программы. Наряду с Java
   это может быть PHP, Perl, Python и т.д.

   Механизм отложенного связывания (deffered binding), выполняемого при
   генерации JavaScript, обеспечивает решение проблем с кроссбраузерной
   совместимостью и локализацией приложения. GWT компилирует различные
   версии приложения под каждый браузер и локализацию. Во время запуска
   клиентской части такого приложения в браузере определяется нужная
   версия, она и поставляется пользователю.

   Библиотеки GWT лицензированы под Apache License 2.0, что даёт полную
   свободу использования среды как в открытых, так и в пропроетарных
   приложениях.

Как установить?

   На компьютере должна быть установлена Java SDK версии 1.5 или выше. Ещё
   одно требование - наличие Apache Ant, java-утилиты для автоматизации
   процесса сборки. Если её нет, просто скачайте из сайта проекта
   (http://ant.apache.org) и распакуйте в любое удобное место, не забыв
   прописать переменную окружения ANT_HOME и путь к Ant/bin в переменной
   PATH. (Впрочем, при использовании Windows можно прибегнуть к сценарию
   установки в составе дистрибутива.)

   Сначала скачиваем дистрибутив с сайта code.google.com
   (http://code.google.com/intl/ru/webtoolkit/download.html) и
   распаковываем его в выбранную директорию. Собственно, на этом процесс
   установки закончен. Осталось только прописать путь к этой папке в
   системной переменной PATH.

   Для проверки работоспособности GWK перейдём в папку /samples,
   расположенную в корневой папке установленного фрэймворка. Это примеры
   простейших приложений использования среды. Заходим (в консоли) в
   /samples/Hello и выполняем команду:

           ant hosted


   Если всё правильно установлено, результат должен быть похож на рис. 1.
   Этой командой мы запускаем приложение в так называемом размещённом
   (hosted) режиме. В нём оно выполняется на виртуальной машине Java
   (JVM). Этот режим предназначен для отладки и разработки.

   Мы видим два окна встроенного браузера, в первом из них запущено само
   приложение, во втором отображается отладочная информация, в том числе
   данные Http-запросов.

   Теперь откомпилируем наше приложение в HTML/Java Script. Для этого
   вызовем команду:

           ant build


   запускающую GWT-компилятор. Результат его работы можно увидеть в папке
   samples/Hello/war, раскрыв в браузере файл MailBoxes.html. Как видим
   (см. рис. 2), всё прошло успешно. Правда, не впечатляет.

   Чтобы увидеть настоящее GWT-приложение, заглянем в папку samples/Mail,
   где хранятся демонстрационные примеры приложений, проведем аналогичные
   действия. В результате получим уже нечто вполне приемлемое (см. рис. 3).

Первое приложение

   Теперь попробуем создать собственное простенькое GWT-приложение. Для
   этого воспользуемся утилитой webApp Creator, входящей в комплект GWT:

        webAppCreator  -out MailBoxes com.samag.MailBoxes


   MailBoxes - это название нашего приложения (да-да, интерфейс к
   почтовому серверу). После выполнения команды будет создано несколько
   папок и файлов в папке MailBoxes/, которые составят скелет приложения и
   обеспечат базовый "Hello GWT" функционал:

   Created directory MailBoxes/src
   Created directory MailBoxes/war
   Created directory MailBoxes/war/WEB-INF
   Created directory MailBoxes/war/WEB-INF/lib
   Created directory MailBoxes/src/com/samag
   Created directory MailBoxes/src/com/samag/client
   Created directory MailBoxes/src/com/samag/server
   Created file MailBoxes/src/com/samag/MailBoxes.gwt.xml
   Created file MailBoxes/war/MailBoxes.html
   Created file MailBoxes/war/MailBoxes.css
   Created file MailBoxes/war/WEB-INF/web.xml
   Created file MailBoxes/src/com/samag/client/MailBoxes.java
   Created file MailBoxes/src/com/samag/client/GreetingService.java
   Created file MailBoxes/src/com/samag/client/GreetingServiceAsync.java
   Created file MailBoxes/src/com/samag/server/GreetingServiceImpl.java
   Created file MailBoxes/build.xml
   Created file MailBoxes/README.txt
   Created file MailBoxes/.project
   Created file MailBoxes/.classpath
   Created file MailBoxes/MailBoxes.launch
   Created file MailBoxes/war/WEB-INF/lib/gwt-servlet.jar

   Скрипт webAppCreator создал несколько файлов в каталоге MyApplication/,
   в том числе базовую функциональность "Hello, world" в классе. В
   корневой папке приложения появился сценарий его сборки, для Ant -
   build.xml, следовательно, наше приложение уже можно компилировать и
   запускать. Воспользовавшись вышеописанными командами ant hosted и ant
   build, мы получим простейшее рабочее AJAX (а как же!) приложение (см.
   рис. 4). Как видите, оно состоит из поля ввода, куда следует поместить
   имя, и кнопки "Отправить", после нажатия на которую появляется ответ
   сервера с приветствием, учитывающим ваши данные.

   Давайте теперь посмотрим, что у него внутри.

   GWT-приложения оформлены в виде модулей, их структура подчиняется
   правилам, сходным с организацией пакетов в Java-приложении. Она
   определяется в xml-файле (в нашем случае MessageBox.gwt.xml).

   Как уже говорилось, GWT-приложение состоит из двух основных частей -
   клиентской и серверной. Клиентский код, как нетрудно догадаться,
   содержится в MailBoxes/src/com/samag/client/. как и
   MailBoxes/src/com/samag/server/, он расположен на одном уровне с
   конфигурационным файлом.

   В папке MailBoxes/war/ расположены все веб-ресурсы приложения - HTML,
   CSS, JavaScript файлы. Там же расположена директория WEB-INF, в которой
   содержатся метаданные и GWT JavaScript-библиотека среды исполнения (GWT
   run-time JavaScript library).

   Если мы заглянем в исходный код файла
   MailBoxes/src/com/samag/client/MailBoxes.java (основного кода нашего
   приложения), то обнаружим обычный Java-код. Вот кнопка Send:

           //Создание кнопки
           final Button sendButton = new Button("Send");
           // Привязка её к таблице стилей
           sendButton.addStyleName("sendButton");
           // Добавление в панель приложения


   RootPanel.get("sendButtonContainer").add(sendButton);
   Я не собираюсь пересказывать документацию [2], но базовые понятия
   изложить придется. Основной класс приложения (в нашем случае MailBoxes)
   реализует интерфейс EntryPoint. Его метод onModuleLoad() вызывается в
   тот момент, когда веб-страница с встроенным GW- модулем отображается в
   браузере клиента. Все элементы визуального интерфейса (кнопки, поля
   ввода, чекбоксы и т.д.) унаследованы от суперкласса Widget. Все эти
   виджеты компонуются в рамках объекта класса Panel, который, разумеется,
   сам является виджетом (его аналог в .Swing - Layout).

   Компоновка в пространстве веб-страницы происходит в элементы контейнера
   HTML (в данном случае sendButton Container- это id HTML-элемента <td>),
   в который будет помещена кнопка. Далее добавляется обработчик:

           class MyHandler implements ClickHandler, KeyUpHandler {
             public void onClick(ClickEvent event) {
               sendNameToServer();
                }
             ...........................................
             }


   И связывается с кнопкой:

           MyHandler handler = new MyHandler();
           sendButton.addClickHandler(handler);


   Мне кажется, что всё ясно. Про серверную часть пока речь не ведём, а
   что касается компиляции в JavaScript/HTML, то сгенерированные
   JavaScript объекты загружаются в контейнеры, обозначенные в шаблоне
   HTML-страницы (MailBoxes/war/MailBoxes.html). Поскольку наше приложение
   сложностью не отличается, то и шаблон довольно прост:

           <table align="center">
                 <tr>
                   <td colspan="2" style="font-weight:bold;">Please enter your  name:</td>
                 </tr>
                 <tr>
                       <td id="nameFieldContainer"></td>
                   <td id="sendButtonContainer"></td>
                 </tr>
            </table>


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

           <table align="center">
                 <tr>
                   <td colspan="3" style="font-weight:bold;">Please enter your  name:</td>
                 </tr>
                 <tr>
                   <td id="nameFieldContainer"></td>
                   <td id="sendButtonContainer"></td>
                   <td id="resContainer"></td>
                     </tr>
          </table>


   Теперь в MailBoxes/src/com/samag/client/MailBoxes.java добавляем новый
   виджет (в терминах данной среды кнопка и прочие функциональные элементы
   графического интерфейса - это именно виджеты (widgets). После кода
   создания кнопки Send добавляем строчку:

           final Button sendButton = new Button("Send");
           final Button resButton = new Button("Reset");


   Добавим этот виджет на главную панель приложения:

           RootPanel.get("sendButtonContainer").add(sendButton);
           RootPanel.get("resContainer").add(resButton);


   Теперь следует связать кнопку с внешним событием (в данном случае с
   кликом по кнопке). Для этого в GWT существуют различные интерфейсы.

   GWT предоставляет несколько интерфейсов Listener для улавливания
   событий (щелчков мыши, нажатия клавиатуры, изменения содержаний поля
   ввода и прочие, знакомые по Javascript). Один из них - ClickHandler -
   отвечает за обработку клика мыши. Напишем его реализацию:

           resButton.addClickHandler(new ClickHandler() {
               public void onClick(ClickEvent event) {
               nameField.setText("");
               sendButton.setFocus(true);
               }
           });


   Готово. Компилируем приложение и наслаждаемся новым функционалом.

   Всё это хорошо, но преимущества нового способа разработки совершенно не
   очевидны. Не для того мы совершали столько телодвижений, чтобы учить
   ещё один диалект Java и отлавливать сообщения об ошибках в консоли.
   Сделаем следующий шаг - подключим к разработке современную IDE.

Работа в IDE Eclipce

   Сначала скачаваем и установливаем плагин для IDE Eclipse
   (http://code.google.com/intl/ru/eclipse). К слову сказать, если GWT
   ещё не установлены, это будет сделано на данном этапе автоматически (а
   заодно и SDK App Engine - средство разработки для платформы Google App
   Engine).

   Инсталляция плагина несколько отличается для разных версий IDE. Для
   Eclipse 3.5 эта процедура выглядит следующим образом:

   Заходим в Help -> Install New Software. В появившимся окне в поле Work
   with вводим url: http://dl.google.com/eclipse/plugin/3.5, нажимаем
   Add. В появившейся форме вводим название для источника обновлений
   (например, Google Updute) и после подтверждения выбираем необходимые
   компоненты (см. рис. 5). Поскольку GWT SDK мы уже установили, его можно
   пропустить. Далее остаётся только пару раз нажать Next и принять
   лицензионное соглашение.

   Теперь импортируем наше приложение в IDE. Для этого нажимаем File ->
   Import, в появившемся окне мастера выбираем General -> Existsing
   Project into Workspace. В следующем окне в поле Select root directory
   указываем путь до корня нашего приложения и жмём Finish. Приложение
   должно появиться в левом окне Eclipse (см. рис. 6).

   Для включения поддержки GWT щёлкаем на проекте правой кнопкой мыши. И в
   контекстном меню выбираем Google -> Web Toolkit Setting. В появившемся
   окошке помечаем чекбокс Use Google Web Toolkit. Если мы ставили GWT SDK
   не вместе с плагином, то, нажав на ссылку Configure SDK, указываем
   расположение фрэймворка.

Разработка приложения

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

   Список ящиков - это, очевидно, таблица. Чтобы выбрать подходящий объект
   из арсенала GWT, отправляемся в галерею виджетов
   (http://code.google.com/intl/ru/webtoolkit/doc/1.6/DevGuide.html),
   представленную в документации фрэймворка, и выбираем наиболее
   подходящий объект для нашей задачи. В данном случае это будет
   Grid-реализация абстрактного класса HTMLTable.

   Включаем его в наше приложение:

           final Grid mailGrid = new Grid(1,5);
           mailGrid.setTitle("mailboxes");


   При этом вначале надо импортировать класс из соответствующего
   GWT-пакета:

           import com.google.gwt.user.client.ui.Grid;


   В дальнейшем добавления почти любых новых элементов влечет за собой
   импорт необходимых пакетов, я не буду на этом останавливаться, так как
   их название подскажет IDE. Если лёгкий путь не для вас и разработка
   происходит в vi/notepad, можно воспользоваться описанием применяемых
   нами компонентов в документации.

   Заполняем первый ряд таблицы (заголовки столбцов):

           mailGrid.setText(0, 0, "ID");
           mailGrid.setText(0, 1, "Name");
           mailGrid.setText(0, 2, "Email");
           mailGrid.setText(0, 3, "Activity");


   Далее заполним таблицу значениями.

            this.getUsers(mailGrid);


   По-настоящему такие сведения должны предоставляться по запросу к
   внешнему источнику (к базе данных или, например, к LDAP), но мы
   договорились, что сейчас не обращаем внимания на серверную часть кода.
   Поэтому метод getUsers пока будет своеобразной заглушкой, впрочем, не
   совсем бестолковой:

           private void getUsers(Grid grid) {
           int rows;
           int newRow;
           String rid;
               rows=grid.getRowCount();
               newRow=grid.insertRow(rows);
               grid.setText(newRow, 0, "1");
               grid.setText(newRow, 1, "Ivfnov");
               grid.setText(newRow,2, "ivanov@gwt.ru");
               grid.setText(newRow, 3, "On");

               rows=grid.getRowCount();
               newRow=grid.insertRow(rows);
               grid.setText(newRow, 0, "2");
               grid.setText(newRow, 1, "Sidorov");
               grid.setText(newRow, 2, "sidorov@gwt.ru");
               grid.setText(newRow, 3, "On");


   Затем создадим в HTML-шаблоне (MailBox.html) необходимый контейнер:

           <div id="mailTable" align="center"></div>


   Подключаем нашу таблицу к основной панели приложения:

            RootPanel.get("mailTable").add(mailGrid);


   После компиляции мы можем видеть ужасно выглядящую и почти бесполезную
   таблицу почтовых ящиков (см. рис. 7).

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

           mailGrid.setStyleName("emailTables");


   И прописать его с таким названием в таблице стилей
   (MailBox/war/MailBox.ccs):

           emailTables{
               font-size: 120%;
               line-height: 1em;
               background: url(images/hborder.gif) repeat-x;
           }


   Любуемся результатом (см. рис. 8) и приступаем к созданию функционала.

   Сначала обеспечим возможность удаления записи. Создадим виджет кнопки
   для удаления как самостоятельный класс, производный от класса Button
   (который в свою очередь произведён от Widget):

           package com.samag.client;
           import com.google.gwt.event.dom.client.ClickEvent;
           import com.google.gwt.event.dom.client.ClickHandler;
           import com.google.gwt.user.client.Window;
           import com.google.gwt.user.client.ui.Button;
           import com.google.gwt.user.client.ui.Grid;
           public class delButton extends Button {
               public delButton(final Grid grid,final int row) {
               super("Delete", new ClickHandler() {
                      public void onClick(ClickEvent event) {
                            Window.alert("Удаляем ряд"+row);
                            grid.removeRow(row);
                             }
                       });
               }
            }


   Я думаю, что по аналогии с кнопкой Reset тут всё понятно. Новые только
   обращение к объекту Window, аналогу объекта window веб-страницы, и
   метод removeRow, который "подсказал" Eclipse. Для того чтобы вставить
   эту кнопку в список акаунтов, немного изменяем заполнение таблицы:

           newRow=grid.insertRow(rows);
           grid.setText(newRow, 0, "1");
           grid.setText(newRow, 1, "Иванов");
           grid.setText(newRow,2, "ivanov@gwt.ru");
           grid.setText(newRow, 3, "On");
           grid.setWidget(newRow, 4, new delButton(grid,1));


   На следующем этапе сделаем возможным активацию/блокирование акаунтов.
   Для этого создадим ещё один виджет - кнопку-переключатель, найдя
   предварительно подходящий класс (ToggleButton) в галерее виджетов:

           package com.samag.client;
           import com.google.gwt.user.client.ui.Grid;
           import com.google.gwt.user.client.ui.ToggleButton;
           import com.google.gwt.user.client.Window;
           public class ActiveButton extends ToggleButton {
               public ActiveButton(final Grid grid, int turn) {
               super("On",    "Off");
               if(turn!=1){
               this.setDown(true);
                }
               public void onClick(ClickEvent event) {
                      Window.alert("Меняем активность");
                       }
            }


   Вносим изменение в заполнение таблицы:

           newRow=grid.insertRow(rows);
           grid.setText(newRow, 0, "1");
           grid.setText(newRow, 1, "Ivanov");
           grid.setText(newRow,2, "ivanov@gwt.ru");
           grid.setWidget(newRow, 3, new ActiveButton(grid,1));
           grid.setWidget(newRow, 4, new delButton(grid,1));


   Осталось создать возможность заносить в таблицу новые акаунты. Для
   этого добавим форму в последнем ряду таблицы. Сначала создадим
   необходимые виджеты (которые после компиляции станут полями
   html-формы):

           final TextBox Name = new TextBox();
           final TextBox Email = new TextBox();
           final CheckBox Activity = new CheckBox();
           Activity.setValue(true);


   Создаём новый ряд таблицы и заполняем его виджетами:

           int rows=mailGrid.getRowCount();
           int newRow=mailGrid.insertRow(rows);
           mailGrid.setWidget(newRow, 1, Name);
           mailGrid.setWidget(newRow, 2, Email);
           mailGrid.setWidget(newRow, 3, Activity);
           mailGrid.setWidget(newRow, 4, new Button("Add", new ClickHandler() {
               public void onClick(ClickEvent event) {
                   this.addRow(mailGrid,Name.getValue(),Email.getValue(),Activity.getValue());
           }


   В последнею ячейку мы помещаем новую кнопку, на этот раз не создавая
   нового класса, а просто привязывавя к объекту класса Button новый
   хендлер. Метод addRow, конечно, нуждается в реализации:

           private void addRow(Grid mailGrid,String uName, String uEmail, Boolean uActivity) {
               if(uName.isEmpty() && uEmail.isEmpty()){
                   Window.alert("Не все параметры заполнены!");
                   return;
               }
               int rows=mailGrid.getRowCount();
               String rid=String.valueOf(Integer.parseInt(mailGrid.getText(rows-2, 0))+1);
               int newRow=mailGrid.insertRow(rows-1);
               mailGrid.setText(newRow, 0, rid);
               mailGrid.setText(newRow, 1, uName);
               mailGrid.setText(newRow, 2, uEmail);
               int turn = 0;
                 if(uActivity.booleanValue()){
                   turn = 1;
                   }
                  ActiveButton uActive = new ActiveButton(mailGrid,turn);
                  mailGrid.setWidget(newRow, 3, uActive);
                  mailGrid.setWidget(newRow, 4, new delButton(mailGrid,newRow));
           }


   Компилируем и проверяем результат (см. рис. 9). Обратите внимание, как
   мы совершенно безболезненно перешли с Windows/Chrome на Linux/Firefox.

   Для первого знакомства с технологией GWT пока достаточно, но за рамками
   осталось, как всегда, самое интересное - взаимодействие с сервером,
   передача данных, удалённый вызов процедур, механизм DWR (Direct Web
   Remoting) и многое другое. Эти вопросы мы рассмотрим во второй части
   статьи.

   1. Домашняя страница проекта -
   http://code.google.com/intl/ru/webtoolkit

   2. Документация по GWT -
   http://code.google.com/intl/ru/webtoolkit/doc/1.6/DevGuide.html
   http://code.google.com/intl/ru/webtoolkit/doc/latest/DevGuide.html

   3. Доклад "Архитектура Google Web Toolkit: полезные советы по
   написанию приложений на GWT" на конференции Google Developer Day 2009,
   Москва - http://www.youtube.com/watch?v=sjyy9WgaObc&feature=channelа

Статью можно обсудить на Форуме журнала "Системный администратор"  http://www.samag.ru/forum/

July 27th, 2010  
Tags: google, GUI, Java, JavaScript, web



Компания Dell прекратила online-продажу компьютеров с предустановленным Ubuntu Linux

News 0 Comment »

Спустя менее недели после переворота в рекламной политике, связанной с продвижением решений на базе Ubuntu Linux, компания Dell прекратила продажу через сайт всех моделей ПК и ноутбуков, поставляемых с предустановленным Linux. В настоящий момент, из устройств с предустановленным Linux, на сайте остается упоминание только ноутбука Dell Latitude 2100, относящегося к сегменту моделей бизнес-уровня, в то время как ранее на сайте был представлен достаточно широкий спектр поставляемых с Ubuntu устройств, включая модели из серии Inspiron, Vostro, Inspiron Mini и Studio XPS.

По утверждению представителей Dell, компания продолжит продажу систем с Ubuntu, но отныне купить модель с предустановленным Linux можно будет, только заказав ее по телефону (на сайте информация о возможности поставки Ubuntu пока отсутствует). В настоящий момент неясно будет ли возобновлена возможность заказа в online – представители Dell утверждают, что решение не окончательное и может быть пересмотрено в будущем. Утверждается, что данная акция связана с желанием упростить процесс приобретения техники в online-режиме, что привело к существенному сокращению представленных для продажи опций, из которых оставлены только самые востребованные варианты, под которые не подпадают модели с Ubuntu, которые, по мнению Dell, интересны лишь энтузиастам и продвинутым пользователям.

Напомню, что месяц назад на сайте Dell появилась рекламная страница, подчеркивающая преимущества Ubuntu над Windows, которая вскоре была отредактирована, а потом заменена на противоположный вариант, в котором утверждалось, что модели с Ubuntu могут быть интересны только программистам, заинтересованным в разработке открытых программ, а на большинстве страниц сайта Dell была добавлена вставка, рекомендующая использовать Windows 7.

http://www.opennet.ru/opennews/art.shtml?num=27415


July 26th, 2010  
Tags: dell, Linux



Resizable components in Java Swing

Java 0 Comment »

In this part of the Java Swing tutorial, we will create a resizable component.

Resizable component

Resizable components are most often used when creating charts, diagrams and similar. The most common resizable component is a chart in a spreadsheet application. For example, when we create a chart in a OpenOffice application. The chart can be moved over the grid widget of the application and resized.

In order to create a component that can be freely dragged over a panel, we need a panel with absolute positioning enabled. We must not use a layout manager. In our example, we will create a component (a JPanel) that we can freely move over a parent window and resize.

In order to distinguish which component has a focus, we draw 8 small rectangles on the border of our resizable component. This way we know, that the component has focus. The rectangles serve as a dragging points, where we can draw the component and start resizing. I have learnt to use resizable components from this blog.

package resizablecomponent;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JFrame;
import javax.swing.JPanel;

/* ResizableComponent.java */

public class ResizableComponent extends JFrame {

  private JPanel panel = new JPanel(null);
  private Resizable resizer;

  public ResizableComponent() {

      add(panel);

      JPanel area = new JPanel();
      area.setBackground(Color.white);
      resizer = new Resizable(area);
      resizer.setBounds(50, 50, 200, 150);
      panel.add(resizer);

      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      setSize(new Dimension(350, 300));
      setTitle("Resizable Component");
      setLocationRelativeTo(null);

      addMouseListener(new MouseAdapter() {
        public void mousePressed(MouseEvent me) {

          requestFocus();
          resizer.repaint();
        }
      });
  }

  public static void main(String[] args) {
      ResizableComponent rc = new ResizableComponent();
      rc.setVisible(true);
  }
}

The ResizableComponent sets up the panel and the component.

private JPanel panel = new JPanel(null);

We have already mentioned, that we cannot use any layout manager. We must use absolute positioning for resizable component. By providing null to the constructor, we create a panel with absolute positioning.

 addMouseListener(new MouseAdapter() {
   public void mousePressed(MouseEvent me) {

     requestFocus();
     resizer.repaint();
   }
 });

If we press on the parent panel, e.g outside the resizable component, we grab focus and repaint the component. The rectangles over the border will disappear.

package resizablecomponent;

import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;

import javax.swing.SwingConstants;
import javax.swing.border.Border;

// ResizableBorder.java 

public class ResizableBorder implements Border {
  private int dist = 8;

  int locations[] =
  {
    SwingConstants.NORTH, SwingConstants.SOUTH, SwingConstants.WEST,
    SwingConstants.EAST, SwingConstants.NORTH_WEST,
    SwingConstants.NORTH_EAST, SwingConstants.SOUTH_WEST,
    SwingConstants.SOUTH_EAST
  };

  int cursors[] =
  {
    Cursor.N_RESIZE_CURSOR, Cursor.S_RESIZE_CURSOR, Cursor.W_RESIZE_CURSOR,
    Cursor.E_RESIZE_CURSOR, Cursor.NW_RESIZE_CURSOR, Cursor.NE_RESIZE_CURSOR,
    Cursor.SW_RESIZE_CURSOR, Cursor.SE_RESIZE_CURSOR
  };

  public ResizableBorder(int dist) {
    this.dist = dist;
  }

  public Insets getBorderInsets(Component component) {
      return new Insets(dist, dist, dist, dist);
  }

  public boolean isBorderOpaque() {
      return false;
  }

  public void paintBorder(Component component, Graphics g, int x, int y,
                          int w, int h) {
      g.setColor(Color.black);
      g.drawRect(x + dist / 2, y + dist / 2, w - dist, h - dist);

      if (component.hasFocus()) {

        for (int i = 0; i < locations.length; i++) {
          Rectangle rect = getRectangle(x, y, w, h, locations[i]);
          g.setColor(Color.WHITE);
          g.fillRect(rect.x, rect.y, rect.width - 1, rect.height - 1);
          g.setColor(Color.BLACK);
          g.drawRect(rect.x, rect.y, rect.width - 1, rect.height - 1);
        }
      }
  }

  private Rectangle getRectangle(int x, int y, int w, int h, int location) {
      switch (location) {
      case SwingConstants.NORTH:
          return new Rectangle(x + w / 2 - dist / 2, y, dist, dist);
      case SwingConstants.SOUTH:
          return new Rectangle(x + w / 2 - dist / 2, y + h - dist, dist,
                               dist);
      case SwingConstants.WEST:
          return new Rectangle(x, y + h / 2 - dist / 2, dist, dist);
      case SwingConstants.EAST:
          return new Rectangle(x + w - dist, y + h / 2 - dist / 2, dist,
                               dist);
      case SwingConstants.NORTH_WEST:
          return new Rectangle(x, y, dist, dist);
      case SwingConstants.NORTH_EAST:
          return new Rectangle(x + w - dist, y, dist, dist);
      case SwingConstants.SOUTH_WEST:
          return new Rectangle(x, y + h - dist, dist, dist);
      case SwingConstants.SOUTH_EAST:
          return new Rectangle(x + w - dist, y + h - dist, dist, dist);
      }
      return null;
  }

  public int getCursor(MouseEvent me) {
      Component c = me.getComponent();
      int w = c.getWidth();
      int h = c.getHeight();

      for (int i = 0; i < locations.length; i++) {
          Rectangle rect = getRectangle(0, 0, w, h, locations[i]);
          if (rect.contains(me.getPoint()))
              return cursors[i];
      }

      return Cursor.MOVE_CURSOR;
  }
}

The ResizableBorder is responsible for drawing the border of the component and determining the type of the cursor to use.

  int locations[] =
  {
    SwingConstants.NORTH, SwingConstants.SOUTH, SwingConstants.WEST,
    SwingConstants.EAST, SwingConstants.NORTH_WEST,
    SwingConstants.NORTH_EAST, SwingConstants.SOUTH_WEST,
    SwingConstants.SOUTH_EAST
  };

These are locations, where we will draw rectangles. These locations are grabbing points, where we can grab the component and resize it.

  g.setColor(Color.black);
  g.drawRect(x + dist / 2, y + dist / 2, w - dist, h - dist);

In the paintBorder() method, we draw the border of the resizable component. The upper code draws the outer border of the component.

  if (component.hasFocus()) {

    for (int i = 0; i < locations.length; i++) {
      Rectangle rect = getRectangle(x, y, w, h, locations[i]);
      g.setColor(Color.WHITE);
      g.fillRect(rect.x, rect.y, rect.width - 1, rect.height - 1);
      g.setColor(Color.BLACK);
      g.drawRect(rect.x, rect.y, rect.width - 1, rect.height - 1);
    }
  }

The eight rectangles are drawn only in case that the resizable component has currently focus.

Finally, the getRectangle() method gets the coordinates of the rectangles and the getCursor() methods gets the cursor type for the grab point in question.

package resizablecomponent;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;

import javax.swing.JComponent;
import javax.swing.event.MouseInputAdapter;
import javax.swing.event.MouseInputListener;

// Resizable.java 

public class Resizable extends JComponent {

  public Resizable(Component comp) {
    this(comp, new ResizableBorder(8));
  }

  public Resizable(Component comp, ResizableBorder border) {
    setLayout(new BorderLayout());
    add(comp);
    addMouseListener(resizeListener);
    addMouseMotionListener(resizeListener);
    setBorder(border);
  }

  private void resize() {
      if (getParent() != null) {
        ((JComponent)getParent()).revalidate();
      }
  }

  MouseInputListener resizeListener = new MouseInputAdapter() {
    public void mouseMoved(MouseEvent me) {
      if (hasFocus()) {
          ResizableBorder border = (ResizableBorder)getBorder();
          setCursor(Cursor.getPredefinedCursor(border.getCursor(me)));
      }
    }

    public void mouseExited(MouseEvent mouseEvent) {
       setCursor(Cursor.getDefaultCursor());
    }

    private int cursor;
    private Point startPos = null;

    public void mousePressed(MouseEvent me) {
      ResizableBorder border = (ResizableBorder)getBorder();
      cursor = border.getCursor(me);
      startPos = me.getPoint();
      requestFocus();
      repaint();
    }

    public void mouseDragged(MouseEvent me) {

      if (startPos != null) {

        int x = getX();
        int y = getY();
        int w = getWidth();
        int h = getHeight();

        int dx = me.getX() - startPos.x;
        int dy = me.getY() - startPos.y;

        switch (cursor) {
          case Cursor.N_RESIZE_CURSOR:
            if (!(h - dy < 50)) {
              setBounds(x, y + dy, w, h - dy);
              resize();
            }
            break;

          case Cursor.S_RESIZE_CURSOR:
            if (!(h + dy < 50)) {
              setBounds(x, y, w, h + dy);
              startPos = me.getPoint();
              resize();
            }
            break;

          case Cursor.W_RESIZE_CURSOR:
            if (!(w - dx < 50)) {
              setBounds(x + dx, y, w - dx, h);
              resize();
            }
            break;

          case Cursor.E_RESIZE_CURSOR:
            if (!(w + dx < 50)) {
              setBounds(x, y, w + dx, h);
              startPos = me.getPoint();
              resize();
            }
            break;

          case Cursor.NW_RESIZE_CURSOR:
            if (!(w - dx < 50) && !(h - dy < 50)) {
              setBounds(x + dx, y + dy, w - dx, h - dy);
              resize();
            }
            break;

          case Cursor.NE_RESIZE_CURSOR:
            if (!(w + dx < 50) && !(h - dy < 50)) {
              setBounds(x, y + dy, w + dx, h - dy);
              startPos = new Point(me.getX(), startPos.y);
              resize();
            }
            break;

          case Cursor.SW_RESIZE_CURSOR:
            if (!(w - dx < 50) && !(h + dy < 50)) {
              setBounds(x + dx, y, w - dx, h + dy);
              startPos = new Point(startPos.x, me.getY());
              resize();
            }
            break;

          case Cursor.SE_RESIZE_CURSOR:
            if (!(w + dx < 50) && !(h + dy < 50)) {
              setBounds(x, y, w + dx, h + dy);
              startPos = me.getPoint();
              resize();
            }
          break;

          case Cursor.MOVE_CURSOR:
            Rectangle bounds = getBounds();
            bounds.translate(dx, dy);
            setBounds(bounds);
            resize();
          }

          setCursor(Cursor.getPredefinedCursor(cursor));
        }
     }

   public void mouseReleased(MouseEvent mouseEvent) {
     startPos = null;
    }
  };
}

The Resizable class represents the component, that is being resized and moved on the window.

  private void resize() {
      if (getParent() != null) {
        ((JComponent)getParent()).revalidate();
      }
  }

The resize() method is called, after we have resized the component. The revalidate() method will cause the component to be redrawn.

 MouseInputListener resizeListener = new MouseInputAdapter() {
   public void mouseMoved(MouseEvent me) {
     if (hasFocus()) {
         ResizableBorder border = (ResizableBorder)getBorder();
         setCursor(Cursor.getPredefinedCursor(border.getCursor(me)));
     }
 }

We change the cursor type, when we hover the cursor over the grip points. The cursor type changes only if the component has focus.

 public void mousePressed(MouseEvent me) {
   ResizableBorder border = (ResizableBorder)getBorder();
     cursor = border.getCursor(me);
     startPos = me.getPoint();
     requestFocus();
     repaint();
 }

If we click on the resizable component, we change the cursor, get the starting point of dragging, give focus to the component and redraw it.

 int x = getX();
 int y = getY();
 int w = getWidth();
 int h = getHeight();

 int dx = me.getX() - startPos.x;
 int dy = me.getY() - startPos.y;

In the mouseDragged() method, we determine the x, y coordinates of the cursor, width and height of the component. We calculate the distances, that we make during the mouse drag event.

 case Cursor.N_RESIZE_CURSOR:
   if (!(h - dy < 50)) {
    setBounds(x, y + dy, w, h - dy);
    resize();
   }
   break;

For all resizing we ensure, that the component is not smaller than 50 px. Otherwise, we could make it so small, that we would eventually hide the component. The setBounds() method relocates and resizes the component.
Resizable component

Figure: Resizable component
URL: http://zetcode.com/tutorials/javaswingtutorial/resizablecomponent/
http://zetcode.com/tutorials/javaswingtutorial/resizablecomponent/

July 12th, 2010  
Tags: Java, JComponent, JPanel, Resizable component, Swing



Ученые обнаружили жизнь на Сатурне

News 0 Comment »

Ученые обнаружили жизнь на Сатурне

Баку, 5 июня, SalamNews, А.Гасанов. Признаки существования жизни обнаружили ученые США на спутнике планеты Сатурн Титане.
НАСА пришло к выводу о наличии примитивных видов биологической жизни на основе анализа данных, полученных с американского спутника «Кассини». Согласно им, эти «жизненные формы дышат атмосферой этой крупнейшей луны Сатурна и потребляют находящиеся на поверхности Титана химические соединения, получая тем самым необходимую энергию», сообщают турецкие СМИ.
«Мы считаем, что находящийся в атмосфере Титана водород используется биологическими формами аналогично тому, как на Земле живые организмы дышат кислородом», — отметил один из исследователей Крис Маккей. Он не исключил, что речь идет о совершенно новой форме биологической жизни, полностью отличной от земной.


June 6th, 2010  
Tags: НАСА, Сатурн, Титан



Previous Entries
  • Categories

    • Development
      • C++
      • Databases
      • Java
      • PHP
    • GZM Web
    • Help
      • Web Server
    • iPhone
    • Linux
    • Netowrking
      • DNS
    • News
    • Security
    • Uncategorized
  • Downloads

    • Gefest Web Server
  • Archives

    • August 2010
    • July 2010
    • June 2010
    • May 2010
    • April 2010
    • March 2010
    • February 2010
  • Recent Posts

    • Приклад використання Callable для повернання результата
    • Настройка кластера MySQL
    • Некоторые недокументированные функции Java
    • Java: магия отражений
    • Java: Магия отражений
    • Java: Mагия отражений
    • Создание современных web-приложений при помощи Google Web Toolkit
    • Компания Dell прекратила online-продажу компьютеров с предустановленным Ubuntu Linux
    • Resizable components in Java Swing
    • Ученые обнаружили жизнь на Сатурне

Support This Project




Copyright © GZMweb.com All rights reserved. Privacy policy | Terms of use
XHTML CSS Log in