什么是服务?
今天,“ 服务 ”一词已被过度使用,对不同的人可能意味着很多事情。 当我说Service时 ,我的定义是一个具有最小生命周期(例如init
, start
, stop
和destroy
)的软件组件。 在编写的每个服务中,您可能不需要生命周期的所有这些阶段,但是您可以忽略那些不适用的服务。 在编写旨在长期运行的大型应用程序(例如服务器组件)时,定义这些生命周期并确保按正确的顺序执行它们是至关重要的!
我将引导您完成我准备的Java演示项目。 这是非常基础的,应该独立运行。 它具有的唯一依赖性是SLF4J记录器。 如果您不知道如何使用记录器,则只需将它们替换为System.out.println
。 但是,我强烈建议您学习在应用程序开发期间如何有效使用记录器。 另外,如果您想尝试与Spring相关的演示,那么显然您也将需要其jar。
编写基本的POJO服务
您可以在界面中如下所示快速定义具有生命周期的服务合同。
package servicedemo;
public interface Service {
void init();
void start();
void stop();
void destroy();
boolean isInited();
boolean isStarted();
}
开发人员可以自由地在Service实现中做他们想做的事情,但是您可能想给他们一个适配器类,这样他们就不必在每个Service上重写相同的基本逻辑。 我将提供这样的抽象服务:
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.');
}
}
从上面的代码中,您将注意到一些重要的事情:
- 我们扩展了AbstractService,因此容器本身就是服务。
- 在进入下一个服务之前,我们将调用所有服务的生命周期。 除非启动所有其他服务,否则不会启动任何服务。
- 对于大多数一般用例,我们应该以相反的顺序停止和销毁服务。
上面的容器实现很简单,并且以同步方式运行。 这意味着,您启动容器,然后所有服务将按照您添加它们的顺序启动。 停止应该相同,但顺序相反。
我也希望您能够看到有足够的空间来改进此容器。 例如,您可以添加线程池以异步方式控制服务的执行。
运行POJO服务 通过一个简单的运行程序运行服务。
以最简单的形式,我们可以自己运行POJO服务,而无需任何高级服务器或框架。 Java程序是从静态main
方法开始的,因此我们肯定可以在其中调用init
并start
我们的服务。 但是,当用户关闭程序时(通常通过按CTRL+C
),我们还需要解决stop
和destroy
生命周期的问题。为此,Java具有java.lang.Runtime#addShutdownHook()
功能。 您可以创建一个简单的独立服务器来引导服务,如下所示:
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.');
}
});
}
}
使用优于跑步者,您应该可以使用以下命令运行它:
$ java demo.ServiceRunner servicedemo.HelloService
仔细查看,您会发现您可以使用上述运行器有很多选择来运行多个服务。 让我突出几个:
- 直接改善上述运行器,并为每个新服务类名称(而不是仅第一个元素)使用所有
args
。 - 或编写一个
MultiLoaderService
来加载所需的多个服务。 您可以使用“系统属性”控制参数传递。
您能想到其他改进此跑步者的方法吗?
使用Spring运行服务
Spring框架是一个IoC容器,众所周知,它易于使用POJO,而Spring可让您将应用程序连接在一起。 这将非常适合在我们的POJO服务中使用。 但是,由于Spring提供了所有功能,因此错过了易于使用的现成的主程序来引导spring config xml上下文文件。 但是就目前为止构建的内容而言,这实际上是一件容易的事。 让我们编写一个POJO 服务来引导一个Spring上下文文件。
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
您可以运行和加载任何spring xml文件。 例如,尝试以下操作:
$ java -DspringContext=config/service-demo-spring.xml demo.ServiceRunner servicedemo.SpringService
在config/service-demo-spring.xml
文件中,您可以轻松地创建我们的容器,该容器在Spring Bean中承载一项或多项服务。
<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>
注意,我只需要在serviceContainer
bean上设置一次init-method
和destroy-method
。 然后,您可以根据需要添加一个或多个其他服务,例如helloService
。 关闭Spring上下文时,它们将全部启动,管理,然后关闭。
请注意,Spring上下文容器没有明确地具有与我们的服务相同的生命周期。 Spring上下文将自动实例化您的所有依赖项Bean,然后调用所有设置了init-method
Bean。 所有这些操作都在FileSystemXmlApplicationContext
的构造函数中完成。 用户未调用任何显式的init方法。 但是最后,在服务停止期间,Spring提供了container#close()
来清理内容。 同样,它们不区分stop
destroy
。 因此,我们必须合并我们的init
并start
进入Spring的init
状态,然后合并stop
和destroy
进入Spring的close
状态。 回想一下我们的AbstractService#destory
会自动调用stop
如果尚未执行的话)。 因此,为了有效使用Spring,我们需要了解这一技巧。
使用JEE应用服务器运行服务
在公司环境中,我们通常没有自由运行独立程序所需的内容。 相反,它们通常已经具有一些基础设施和更严格的标准技术堆栈,例如使用JEE应用程序服务器。 在这种情况下,运行POJO服务最可移植的是war
Web应用程序。 在Servlet Web应用程序中,您可以编写一个实现javax.servlet.ServletContextListener
的类,这将通过contextInitialized
和contextDestroyed
为您提供生命周期挂钩。 在其中,您可以实例化ServiceContainer
对象,并相应地调用start
和destroy
方法。
您可以浏览以下示例:
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
配置:
<listener>
<listener-class>servicedemo.ServiceContainerListener</listener-class>
</listener>
</web-app>
该演示提供了一个占位符,您必须在代码中添加服务。 但是您可以使用web.xml
作为上下文参数轻松地将其配置为可配置的。
如果要在Servlet容器中使用Spring,则可以直接使用其org.springframework.web.context.ContextLoaderListener
类,该类与上述功能大致相同,不同之处在于它们允许您使用contextConfigLocation
上下文参数指定其xml配置文件。 这就是典型的基于Spring MVC的应用程序的配置方式。 设置完成后,您可以像上面给出的Spring xml示例一样尝试我们的POJO服务以进行测试。 您应该在记录器的输出中看到我们的服务正在运行。
PS:实际上,我们在此描述的只是与Servlet Web应用程序相关,而不与JEE有关。 因此,您也可以使用Tomcat服务器。
服务生命周期的重要性及其在现实世界中的使用
我在这里提供的所有信息都不是新颖的,也不是杀手级的设计模式。 实际上,它们已在许多流行的开源项目中使用。 但是,根据我过去的工作经验,人们总是设法使这些变得极为复杂,更糟糕的情况是,他们在编写服务时完全无视生命周期的重要性。 的确,并非您要编写的所有内容都需要安装到服务中,但是,如果发现需要,请务必注意它们,并请务必小心它们的正确调用。 您想要的最后一件事是退出JVM,而不清理您为其分配了宝贵资源的服务。 如果您允许在部署过程中动态地重新加载应用程序而不退出JVM,这些操作将变得更加灾难性,这将导致系统资源泄漏。
以上服务实践已在TimeMachine项目中使用。 实际上,如果您查看timemachine.scheduler.service.SchedulerEngine
,它将只是许多运行在一起的服务的容器。 这就是用户可以通过编写Service来扩展调度程序功能的方式。 您可以通过一个简单的属性文件动态加载这些服务。
参考: 如何在A Programmer's Journal博客上从我们的JCG合作伙伴 Zemian Deng 编写更好的POJO服务 。
翻译自: https://www.javacodegeeks.com/2012/09/how-to-write-better-pojo-services.html