Статьи

Как лучше написать POJO Services

В Java вы можете легко реализовать некоторую бизнес-логику в классах Plain Old Java Object (POJO) и запускать их на необычном сервере или в среде без особых хлопот. Существует множество серверов / платформ, таких как JBossAS, Spring или Camel и т. Д., Которые позволят вам развернуть POJO даже без жесткого программирования их API. Очевидно, что вы получите расширенные возможности, если захотите соединиться с их спецификациями API, но даже если вы это сделаете, вы можете свести их к минимуму, заключив в оболочку свой собственный POJO и их API. Написав и спроектировав собственное приложение как можно более простым POJO, вы получите самые гибкие способы выбора платформы или сервера для развертывания и запуска вашего приложения. Одним из эффективных способов написания вашей бизнес-логики в этих средах является использование компонента Service . В этой статье я поделюсь несколькими вещами, которые я узнал при написании Сервисов .

Что такое сервис?

Слово « Сервис» используется сегодня слишком широко, и оно может означать многое для разных людей. Когда я говорю « Сервис» , мое определение — это программный компонент, который имеет минимальный жизненный цикл, такой как init , start , stop и destroy . Вам может не понадобиться все эти этапы жизненных циклов в каждой написанной вами услуге, но вы можете просто игнорировать те, которые не применяются. При написании больших приложений, предназначенных для длительной работы, таких как серверные компоненты, крайне важно определить эти жизненные циклы и убедиться, что они выполняются в правильном порядке!

Я проведу вас через демонстрационный проект Java, который я подготовил. Это очень просто и должно работать автономно. Единственная зависимость, которую он имеет, — это регистратор SLF4J . Если вы не знаете, как использовать регистратор, просто замените его на System.out.println . Однако я настоятельно рекомендую вам научиться эффективно использовать регистратор во время разработки приложений. Также, если вы хотите попробовать демо, связанные с Spring , то вам, очевидно, понадобятся и их банки.

Написание базового сервиса POJO

Вы можете быстро определить контракт Сервиса с жизненными циклами, как показано ниже в интерфейсе.

01
02
03
04
05
06
07
08
09
10
package servicedemo;
 
public interface Service {
    void init();
    void start();
    void stop();
    void destroy();
    boolean isInited();
    boolean isStarted();
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package servicedemo;
 
import java.util.concurrent.atomic.*;
import org.slf4j.*;
public abstract class AbstractService implements Service {
    protected Logger logger = LoggerFactory.getLogger(getClass());
    protected AtomicBoolean started = new AtomicBoolean(false);
    protected AtomicBoolean inited = new AtomicBoolean(false);
 
    public void init() {
        if (!inited.get()) {
            initService();
            inited.set(true);
            logger.debug('{} initialized.', this);
        }
    }
 
    public void start() {
        // Init service if it has not done so.
        if (!inited.get()) {
            init();
        }
        // Start service now.
        if (!started.get()) {
            startService();
            started.set(true);
            logger.debug('{} started.', this);
        }
    }
 
    public void stop() {
        if (started.get()) {
            stopService();
            started.set(false);
            logger.debug('{} stopped.', this);
        }
    }
 
    public void destroy() {
        // Stop service if it is still running.
        if (started.get()) {
            stop();
        }
        // Destroy service now.
        if (inited.get()) {
            destroyService();
            inited.set(false);
            logger.debug('{} destroyed.', this);
        }
    }
 
    public boolean isStarted() {
        return started.get();
    }
 
    public boolean isInited() {
        return inited.get();
    }
 
    @Override
    public String toString() {
            return getClass().getSimpleName() + '[id=' + System.identityHashCode(this) + ']';
    }
 
    protected void initService() {
    }
 
    protected void startService() {
    }
 
    protected void stopService() {
    }
 
    protected void destroyService() {
    }
}

Этот абстрактный класс обеспечивает основные потребности большинства служб. Он имеет регистратор и состояния, чтобы отслеживать жизненные циклы. Затем он делегирует новые наборы методов жизненного цикла, чтобы подкласс мог выбрать переопределение. Обратите внимание, что метод start() проверяет автоматический вызов init() если это еще не сделано. То же самое делается в методе destroy() метода stop() . Это важно, если мы используем его в контейнере, который имеет только два этапа вызова жизненных циклов. В этом случае мы можем просто вызвать start() и destroy() чтобы соответствовать жизненным циклам нашего сервиса.

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
package servicedemo;
 
public class HelloService extends AbstractService {
    public void initService() {
        logger.info(this + ' inited.');
    }
    public void startService() {
        logger.info(this + ' started.');
    }
    public void stopService() {
        logger.info(this + ' stopped.');
    }
    public void destroyService() {
        logger.info(this + ' destroyed.');
    }
}


Управление несколькими сервисами POJO с контейнером

Теперь у нас есть определение базового определения сервиса , ваша команда разработчиков может начать писать код бизнес-логики! Вскоре у вас будет библиотека ваших собственных сервисов для повторного использования. Чтобы иметь возможность эффективно группировать и контролировать эти услуги, мы также хотим предоставить контейнер для управления ими. Идея состоит в том, что мы обычно хотим контролировать и управлять несколькими сервисами с контейнером в виде группы на более высоком уровне. Вот простая реализация для начала:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package servicedemo;
 
import java.util.*;
public class ServiceContainer extends AbstractService {
    private List<Service> services = new ArrayList<Service>();
 
    public void setServices(List<Service> services) {
        this.services = services;
    }
    public void addService(Service service) {
        this.services.add(service);
    }
 
    public void initService() {
        logger.debug('Initializing ' + this + ' with ' + services.size() + ' services.');
        for (Service service : services) {
            logger.debug('Initializing ' + service);
            service.init();
        }
        logger.info(this + ' inited.');
    }
    public void startService() {
            logger.debug('Starting ' + this + ' with ' + services.size() + ' services.');
            for (Service service : services) {
                logger.debug('Starting ' + service);
                service.start();
            }
            logger.info(this + ' started.');
    }
    public void stopService() {
            int size = services.size();
            logger.debug('Stopping ' + this + ' with ' + size + ' services in reverse order.');
            for (int i = size - 1; i >= 0; i--) {
                Service service = services.get(i);
                logger.debug('Stopping ' + service);
                service.stop();
            }
            logger.info(this + ' stopped.');
    }
    public void destroyService() {
            int size = services.size();
            logger.debug('Destroying ' + this + ' with ' + size + ' services in reverse order.');
            for (int i = size - 1; i >= 0; i--) {
                Service service = services.get(i);
                logger.debug('Destroying ' + service);
                service.destroy();
            }
            logger.info(this + ' destroyed.');
    }
}

Из приведенного выше кода вы заметите несколько важных вещей:

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

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

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

Запуск сервисов POJO Запуск сервисов с помощью простой программы запуска.

В простейшей форме мы можем запускать наши сервисы POJO самостоятельно, без каких-либо причудливых серверов или платформ. Java-программы начинают свою жизнь со статического метода main , поэтому мы, безусловно, можем вызвать там init и start наших сервисов. Но нам также необходимо обратиться к жизненным циклам stop и destroy когда пользователь выключает программу (обычно, нажимая CTRL+C ). Для этого в Java есть java.lang.Runtime#addShutdownHook() . Вы можете создать простой автономный сервер для загрузки службы, например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package servicedemo;
 
import org.slf4j.*;
public class ServiceRunner {
    private static Logger logger = LoggerFactory.getLogger(ServiceRunner.class);
 
    public static void main(String[] args) {
        ServiceRunner main = new ServiceRunner();
        main.run(args);
    }
 
    public void run(String[] args) {
        if (args.length < 1)
            throw new RuntimeException('Missing service class name as argument.');
 
        String serviceClassName = args[0];
        try {
            logger.debug('Creating ' + serviceClassName);
            Class<?> serviceClass = Class.forName(serviceClassName);
            if (!Service.class.isAssignableFrom(serviceClass)) {
                throw new RuntimeException('Service class ' + serviceClassName + ' did not implements ' + Service.class.getName());
            }
            Object serviceObject = serviceClass.newInstance();
            Service service = (Service)serviceObject;
 
            registerShutdownHook(service);
 
            logger.debug('Starting service ' + service);
            service.init();
            service.start();
            logger.info(service + ' started.');
 
            synchronized(this) {
                this.wait();
            }
        } catch (Exception e) {
            throw new RuntimeException('Failed to create and run ' + serviceClassName, e);
        }
    }
 
    private void registerShutdownHook(final Service service) {
        Runtime.getRuntime().addShutdownHook(new Thread() {
            public void run() {
                logger.debug('Stopping service ' + service);
                service.stop();
                service.destroy();
                logger.info(service + ' stopped.');
            }
        });
    }
}

С abover runner вы сможете запустить его с помощью этой команды:

1
$ java demo.ServiceRunner servicedemo.HelloService

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

  1. Улучшите вышеперечисленное напрямую и сделайте все args для каждого нового имени класса обслуживания, а не только для первого элемента.
  2. Или напишите MultiLoaderService который будет загружать несколько служб, которые вы хотите. Вы можете управлять передачей аргументов, используя Системные свойства.

Можете ли вы придумать другие способы улучшить этого бегуна?

Запуск сервисов с Spring

Среда Spring представляет собой контейнер IoC, и, как известно, она проста в работе с POJO, а Spring позволяет связывать ваше приложение вместе. Это было бы идеально подходит для использования в наших сервисах POJO. Тем не менее, со всеми возможностями Spring, он упустил простую в использовании основную программу для начальной загрузки контекстных файлов Spring config xml. Но с тем, что мы построили до сих пор, это на самом деле легко сделать. Давайте напишем один из наших сервисов POJO для начальной загрузки файла контекста Spring.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package servicedemo;
 
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
 
public class SpringService extends AbstractService {
    private ConfigurableApplicationContext springContext;
 
    public void startService() {
        String springConfig = System.getProperty('springContext', 'spring.xml);
        springContext = new FileSystemXmlApplicationContext(springConfig);
        logger.info(this + ' started.');
    }
    public void stopService() {
        springContext.close();
        logger.info(this + ' stopped.');
    }
}

С этим простым SpringService вы можете запустить и загрузить любой XML-файл Spring. Например попробуйте это:

1
$ java -DspringContext=config/service-demo-spring.xml demo.ServiceRunner servicedemo.SpringService

Внутри файла config/service-demo-spring.xml вы можете легко создать наш контейнер, в котором размещается один или несколько сервисов в компонентах Spring.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
 
    <bean id='helloService' class='servicedemo.HelloService'>
    </bean>
 
    <bean id='serviceContainer' class='servicedemo.ServiceContainer' init-method='start' destroy-method='destroy'>
        <property name='services'>
            <list>
                <ref bean='helloService'/>
            </list>
        </property>
    </bean>
 
</beans>

Обратите внимание, что мне нужно только один раз установить init-method и destroy-method для bean-компонента serviceContainer . Затем вы можете добавить один или несколько других сервисов, таких как helloService сколько захотите. Все они будут запускаться, управляться, а затем выключаться при закрытии контекста Spring.

Обратите внимание, что у контейнера контекста Spring явно не было того же жизненного цикла, что и у наших сервисов. Контекст Spring автоматически создаст все ваши bean-компоненты зависимостей, а затем вызовет все bean-компоненты, для которых установлен init-method . Все это делается внутри конструктора FileSystemXmlApplicationContext . Нет явного метода инициализации от пользователя. Однако в конце, во время остановки службы, Spring предоставляет container#close() для очистки. Опять же, они не различают stop от destroy . Из-за этого мы должны объединить наш init и start в состояние init Spring, а затем объединить stop и destroy в close состояние Spring. Напомним, что наше AbstractService#destory будет автоматически вызывать stop если это еще не сделано. Так что это уловка, которую мы должны понять, чтобы эффективно использовать Spring.

Запуск служб с сервером приложений JEE

В корпоративной среде у нас обычно нет свободы для запуска того, что мы хотим, в качестве отдельной программы. Вместо этого у них обычно уже есть некоторая инфраструктура и более строгий стандартный технологический стек, такой как использование сервера приложений JEE. В этой ситуации наиболее переносимым для запуска сервисов POJO является веб-приложение war . В веб-приложении contextInitialized вы можете написать класс, который реализует javax.servlet.ServletContextListener и это предоставит вам хук жизненных циклов через contextInitialized и contextDestroyed . Там вы можете создать ServiceContainer объекта ServiceContainer и соответственно вызвать методы start и destroy .

Вот пример, который вы можете изучить:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package servicedemo;
import java.util.*;
import javax.servlet.*;
public class ServiceContainerListener implements ServletContextListener {
    private static Logger logger = LoggerFactory.getLogger(ServiceContainerListener.class);
    private ServiceContainer serviceContainer;
 
    public void contextInitialized(ServletContextEvent sce) {
        serviceContainer = new ServiceContainer();
        List<Service> services = createServices();
        serviceContainer.setServices(services);
        serviceContainer.start();
        logger.info(serviceContainer + ' started in web application.');
    }
 
    public void contextDestroyed(ServletContextEvent sce) {
        serviceContainer.destroy();
        logger.info(serviceContainer + ' destroyed in web application.');
    }
 
    private List<Service> createServices() {
        List<Service> result = new ArrayList<Service>();
        // populate services here.
        return result;
    }
}

Вы можете настроить выше в WEB-INF/web.xml следующим образом:

1
2
3
4
5
<listener>
        <listener-class>servicedemo.ServiceContainerListener</listener-class>
    </listener>
 
</web-app>

Демонстрация предоставила заполнитель, который вы должны добавить свои услуги в коде. Но вы можете легко сделать это настраиваемым, используя web.xml для параметров контекста.

Если вы должны использовать Spring в контейнере org.springframework.web.context.ContextLoaderListener , вы можете напрямую использовать их класс org.springframework.web.context.ContextLoaderListener который делает то же самое, что и выше, за исключением того, что он позволяет вам указать их файл конфигурации xml с помощью параметра контекста contextConfigLocation . Вот как настраивается типичное приложение на основе Spring MVC. Получив эту настройку, вы можете поэкспериментировать с нашей службой POJO так же, как пример Spring xml, приведенный выше, чтобы проверить все. Вы должны увидеть наш сервис в действии на выходе вашего регистратора.

PS: На самом деле то, что мы здесь описали, просто относится к веб-приложению сервлета, а не к JEE. Таким образом, вы также можете отлично использовать сервер Tomcat.

Важность жизненных циклов Сервиса и его использование в реальном мире

Вся информация, которую я здесь представил, не новость и не шаблон дизайна. Фактически они использовались во многих популярных проектах с открытым исходным кодом. Тем не менее, по моему прошлому опыту на работе, людям всегда удавалось сделать это чрезвычайно сложным, и хуже всего то, что они полностью игнорировали важность жизненных циклов при написании сервисов. Это правда, что не все, что вы собираетесь написать, должно быть встроено в сервис, но если вы обнаружите необходимость, пожалуйста, обратите на них внимание и позаботьтесь о том, чтобы они вызывались правильно. Последнее, что вам нужно, — это выйти из JVM без очистки служб, для которых вы выделили драгоценные ресурсы. Это станет более катастрофическим, если вы разрешите динамически перезагружать ваше приложение во время развертывания без выхода из JVM, что приведет к утечке системных ресурсов.

Вышеуказанная Сервисная практика была применена в проекте TimeMachine . На самом деле, если вы посмотрите на timemachine.scheduler.service.SchedulerEngine , это будет просто контейнер многих служб, работающих вместе. И именно так пользователь может расширить функциональность планировщика, написав Сервис . Вы можете загрузить эти сервисы динамически с помощью простого файла свойств.

Ссылка: Как лучше написать Услуги POJO от нашего партнера JCG Земьяна Дена в блоге A Programmer’s Journal .