В Java вы можете легко реализовать некоторую бизнес-логику в классах Plain Old Java Object (POJO), а затем запускать их на необычном сервере или в фреймворке без особых хлопот. Существует множество серверов / платформ, таких как JBossAS, Spring или Camel и т. Д., Которые позволят вам развернуть POJO даже без жесткого программирования их API. Очевидно, что вы получите расширенные возможности, если захотите соединиться с их спецификациями API, но даже если вы это сделаете, вы можете свести их к минимуму, заключив в оболочку свой собственный POJO и их API. Написав и спроектировав собственное приложение как можно более простым POJO, вы получите самые гибкие способы выбора платформы или сервера для развертывания и запуска вашего приложения. Одним из эффективных способов написания вашей бизнес-логики в этих средах является использование Service.составная часть. В этой статье я поделюсь несколькими вещами, которые я узнал при написании Сервисов .
Что такое сервис?
Слово « Сервис» используется сегодня слишком широко, и оно может означать многое для разных людей. Когда я говорю « Сервис» , мое определение — это программный компонент, который имеет минимальный жизненный цикл, такой как инициализация, запуск, остановка и уничтожение. Вам может не понадобиться все эти этапы жизненных циклов в каждой написанной вами услуге, но вы можете просто игнорировать те, которые не применяются. При написании больших приложений, предназначенных для длительной работы, таких как серверные компоненты, крайне важно определить эти жизненные циклы и убедиться, что они выполняются в правильном порядке!
Я проведу вас через демонстрационный проект Java, который я подготовил. Это очень просто и должно работать автономно. Единственная зависимость, которую он имеет — это регистратор SLF4J . Если вы не знаете, как использовать регистратор, просто замените его на System.out.println. Однако я настоятельно рекомендую вам научиться эффективно использовать регистратор во время разработки приложений. Также, если вы хотите попробовать демо, связанные с Spring , то вам, очевидно, понадобятся и их банки.
Написание базового сервиса POJO
Вы можете быстро определить контракт Сервиса с жизненными циклами, как показано ниже в интерфейсе.
package servicedemo; public interface Service { void init(); void start(); void stop(); void destroy(); boolean isInited(); boolean isStarted(); }
Разработчики могут свободно делать то, что они хотят в своей реализации Сервиса, но вы можете предоставить им класс адаптера, чтобы им не приходилось переписывать одну и ту же базовую логику для каждого Сервиса. Я хотел бы предоставить абстрактный сервис, как это:
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, который позже можно будет использовать в нашей демонстрации.
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 с контейнером
Теперь у нас есть определение базового определения сервиса , ваша команда разработчиков может начать писать код бизнес-логики! Вскоре у вас будет библиотека ваших собственных сервисов для повторного использования. Чтобы иметь возможность эффективно группировать и контролировать эти услуги, мы также хотим предоставить контейнер для управления ими. Идея состоит в том, что мы обычно хотим контролировать и управлять несколькими сервисами с контейнером в виде группы на более высоком уровне. Вот простая реализация для начала:
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."); } }
Из приведенного выше кода вы заметите несколько важных вещей:
- Мы расширяем абстрактный сервис, поэтому контейнер сам по себе является сервисом.
- Мы бы задействовали все жизненные циклы сервиса, прежде чем перейти к следующему. Никакие услуги не начнутся, если все другие не будут инициированы.
- Мы должны останавливать и уничтожать службы в обратном порядке для большинства общих случаев использования.
Вышеуказанная реализация контейнера проста и выполняется синхронно. Это означает, что вы запускаете контейнер, тогда все службы будут запускаться в том порядке, в котором вы их добавили. Стоп должен быть таким же, но в обратном порядке.
Я также надеюсь, что вы сможете увидеть, что у вас есть много возможностей для улучшения этого контейнера. Например, вы можете добавить пул потоков для управления выполнением служб в асинхронном режиме.
Запуск Сервисов POJO
Запуск сервисов с помощью простой программы бегуна.
In the simplest form, we can run our POJO services on our own without any fancy server or frameworks. Java programs start its life from a static main method, so we surely can invoke init and start of our services in there. But we also need to address the stop and destroy life-cycles when user shuts down the program (usually by hitting CTRL+C.) For this, the Java has the java.lang.Runtime#addShutdownHook() facility. You can create a simple stand-alone server to bootstrap Service like this:
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."); } }); } }
With abover runner, you should able to run it with this command:
$ java demo.ServiceRunner servicedemo.HelloService
Look carefully, and you’ll see that you have many options to run multiple services with above runner. Let me highlight couple:
- Improve above runner directly and make all args for each new service class name, instead of just first element.
- Or write a MultiLoaderService that will load multiple services you want. You may control argument passing using System Properties.
Can you think of other ways to improve this runner?
Running services with Spring
The Spring framework is an IoC container, and it’s well known to be easy to work POJO, and Spring lets you wire your application together. This would be a perfect fit to use in our POJO services. However, with all the features Spring brings, it missed a easy to use, out of box main program to bootstrap spring config xml context files. But with what we built so far, this is actually an easy thing to do. Let’s write one of our POJO Service to bootstrap a spring context file.
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."); } }
With that simple SpringService you can run and load any spring xml file. For example try this:
$ java -DspringContext=config/service-demo-spring.xml demo.ServiceRunner servicedemo.SpringService
Inside the config/service-demo-spring.xml file, you can easily create our container that hosts one or more service in Spring beans.
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <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>
Notice that I only need to setup init-method and destroy-method once on the serviceContainer bean. You can then add one or more other service such as the helloService as much as you want. They will all be started, managed, and then shutdown when you close the Spring context.
Note that Spring context container did not explicitly have the same life-cycles as our services. The Spring context will automatically instanciate all your dependency beans, and then invoke all beans who’s init-method is set. All that is done inside the constructor of FileSystemXmlApplicationContext. No explicit init method is called from user. However at the end, during stop of the service, Spring provide the springContext#close() to clean things up. Again, they do not differentiate stop from destroy. Because of this, we must merge our init and start into Spring’s init state, and then merge stop and destroy into Spring’s close state. Recall our AbstractService#destory will auto invoke stop if it hasn’t already done so. So this is trick that we need to understand in order to use Spring effectively.
Running services with JEE app server
In a corporate env, we usually do not have the freedom to run what we want as a stand-alone program. Instead they usually have some infrustructure and stricter standard technology stack in place already, such as using a JEE application server. In these situation, the most portable way to run POJO services is in a war web application. In a Servlet web application, you can write a class that implements javax.servlet.ServletContextListener and this will provide you the life-cycles hook via contextInitialized and contextDestroyed. In there, you can instanciate your ServiceContainer object and call start and destroy methods accordingly.
Here is an example that you can explore:
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; } }
You may configure above in the WEB-INF/web.xml like this:
<listener> <listener-class>servicedemo.ServiceContainerListener</listener-class> </listener> </web-app>
The demo provided a placeholder that you must add your services in code. But you can easily make that configurable using the web.xml for context parameters.
If you were to use Spring inside a Servlet container, you may directly use their org.springframework.web.context.ContextLoaderListener class that does pretty much same as above, except they allow you to specify their xml configuration file using the contextConfigLocation context parameter. That’s how a typical Spring MVC based application is configure. Once you have this setup, you can experiment our POJO service just as the Spring xml sample given above to test things out. You should see our service in action by your logger output.
PS: Actually what we described here are simply related to Servlet web application, and not JEE specific. So you can use Tomcat server just fine as well.
The importance of Service’s life-cycles and it’s real world usage
All the information I presented here are not novelty, nor a killer design pattern. In fact they have been used in many popular open source projects. However, in my past experience at work, folks always manage to make these extremely complicated, and worse case is that they completely disregard the importance of life-cycles when writing services. It’s true that not everything you going to write needs to be fitted into a service, but if you find the need, please do pay attention to them, and take good care that they do invoked properly. The last thing you want is to exit JVM without clean up in services that you allocated precious resources for. These would become more disastrous if you allow your application to be dynamically reloaded during deployment without exiting JVM, in which will lead to system resources leakage.
The above Service practice has been put into use in the TimeMachine project. In fact, if you look at the timemachine.scheduler.service.SchedulerEngine, it would just be a container of many services running together. And that’s how user can extend the scheduler functionalities as well, by writing a Service. You can load these services dynamically by a simple properties file.