《学习Java EE》一书。 大型企业的现代编程”

图片 哈勃!

本书介绍了新一代的Java EE。 您将在微服务和容器的现代世界中踏上Java EE之旅。 这不是API语法的参考指南-这里介绍的概念和技术反映了一个刚走过这条路,密切注意所出现的障碍并准备分享其知识的人的真实经验。 在各种情况下,从创建用于测试和云使用的程序包,本书将是初学者和有经验的开发人员的理想伴侣,他们不仅希望了解API,还希望了解更多内容,并帮助他们重新构建思想,以Java EE创建现代应用程序体系结构。

执行顺序


在企业应用程序中实现的业务流程描述了特定的流程。 对于涉及的业务场景,这可以是同步请求和响应过程,也可以是已启动过程的异步处理。

业务场景在单独的线程中调用,每个请求或调用一个线程。 流是由容器创建的,并在成功处理完调用后放入驱动器中以供重用。 默认情况下,将按顺序执行应用程序类中定义的业务流程以及诸如事务之类的跨领域任务。

同步执行


当HTTP请求需要来自数据库的响应时,典型的情形实现如下。 一个线程通过反转控制原理来处理到达循环的请求,例如JAX-RS UsersResource。 容器调用JAX-RS资源方法。 该资源实现并使用UserManagement EJB,它也被容器隐式调用。 所有操作均由中介同步执行。 User EJB将使用实体管理器来存储新实体,并且一旦启动当前活动事务的业务方法完成,容器就会尝试将事务提交到数据库。 根据交易的结果,电路的资源方法恢复操作并生成对客户端的响应。 一切都是同步发生的,这时客户端被阻塞并等待响应。

同步执行包括处理同步CDI事件。 它们将域事件的触发与处理分开,但是,事件是同步处理的。 有几种监视事务的方法。 如果指示了事务处理阶段,则可以在此阶段处理事件-在事务修复期间,完成之前,完成之后,如果事务不成功或成功,则可以进行处理。 默认情况下,或者如果事务处于非活动状态,则CDI事件发生时将立即处理。 这使工程师能够实施复杂的解决方案-例如,使用仅在将实体成功添加到数据库之后才发生的事件。 尽管如此,在所有情况下处理都是同步执行的。

异步执行


任务的同步执行可以满足许多业务场景的要求,但是有时您需要异步行为。 Java EE环境对线程的使用有很多限制。 容器管理资源和流,并将其放置在驱动器中。 外部并发控制实用程序位于容器外部,并且它们不了解这些流。 因此,应用程序代码不应运行和控制其线程。 为此,它使用Java EE功能。 有几种具有内置异步支持的API。

异步EJB方法

实现异步行为的最简单方法是对EJB或EJB类业务方法使用@Asynchronous批注。 对这些方法的调用会立即返回,有时还会返回Future类型的响应。 它们在由容器控制的单独线程中运行。 此方法适用于简单方案,但仅限于EJB:

@Asynchronous @Stateless public class Calculator { public void calculatePi(long decimalPlaces) { //      } } 

绩效管理服务

为了在托管CDI对象中异步执行任务或使用Java SE并发控制实用程序,Java EE包括ExecutorService和ScheduledExecutorService函数的容器管理版本。 它们用于在容器驱动的线程中实现异步任务。 ManagedExecutorService和ManagedScheduledExecutorService实例嵌入在应用程序代码中。 它们可以用来执行自己的逻辑,但是与Java SE并发控制实用程序(例如补充的未来值)结合使用时,它们最有效。 下面的示例演示如何使用容器驱动的线程创建带填充的将来值:

 import javax.annotation.Resource; import javax.enterprise.concurrent.ManagedExecutorService; import java.util.Random; import java.util.concurrent.CompletableFuture; @Stateless public class Calculator { @Resource ManagedExecutorService mes; public CompletableFuture<Double> calculateRandomPi(int maxDecimalPlaces) { return CompletableFuture.supplyAsync(() -> new Random().nextInt(maxDecimalPlaces) + 1, mes) .thenApply(this::calculatePi); } private double calculatePi(long decimalPlaces) { … } } 

计算器对象返回双精度类型的补充将来值,当调用上下文恢复时仍可以计算该值。 计算完成时可以要求它,并且可以与后续计算结合使用。 无论企业应用程序中需要什么新线程,都应使用Java EE功能来管理它们。

异步CDI事件

CDI事件也可以异步处理。 在这种情况下,容器还提供用于处理事件的流。 为了描述异步事件处理程序,该方法使用@ObservesAsync注释,并使用fireAsync()方法激活该事件。 以下代码段演示了异步CDI事件:

 @Stateless public class CarManufacturer { @Inject CarFactory carFactory; @Inject Event<CarCreated> carCreated; public Car manufactureCar(Specification spec) { Car car = carFactory.createCar(spec); carCreated.fireAsync(new CarCreated(spec)); return car; } } 

在其自己的容器管理线程中调用事件处理程序:

 import javax.enterprise.event.ObservesAsync; public class CreatedCarListener { public void onCarCreated(@ObservesAsync CarCreated event) { //    } } 

出于向后兼容性的原因,同步CDI事件也可以使用异步EJB方法进行处理。 因此,事件和处理程序被定义为同步的,并且处理程序方法是具有@Asynchronous批注的EJB业务方法。 在将异步事件引入Java EE 8的CDI标准之前,这是实现此功能的唯一方法。 为了避免在Java EE 8和更高版本中造成混淆,最好避免这种实现。

异步处理范围

由于容器没有有关异步任务可以执行多长时间的信息,因此在这种情况下限制了作用域的使用。 在启动异步任务时,具有请求或会话范围内的对象的对象不一定会在整个实现过程中处于活动状态-请求和会话可能在完成之前就结束了。 因此,执行异步任务(例如,计划的执行程序服务提供的任务或异步事件)的线程可能无法访问在调用期间处于活动状态的请求或会话中范围内的托管对象的实例。 访问嵌入式实例的链接也是如此,例如在作为同步执行一部分的lambda方法中。

在对异步任务建模时必须考虑到这一点。 任务开始时应提供有关特定呼叫的所有信息。 但是,异步任务可以在有限范围内拥有自己的托管对象实例。

设定时间执行

业务场景不仅可以从外部(例如,通过HTTP请求)调用,还可以从应用程序内部(在特定时间运行的任务)调用。

在Unix世界中,用于运行定期作业的功能很流行-这些是调度程序的任务。 EJB使用EJB计时器提供类似的功能。 计时器在指定的时间间隔或指定的时间后调用业务方法。 以下示例描述了一个每十分钟启动一次的循环计时器:

 import javax.ejb.Schedule; import javax.ejb.Startup; @Singleton @Startup public class PeriodicJob { @Schedule(minute = "*/10", hour = "*", persistent = false) public void executeJob() { //   10  } } 

任何EJB(单调,具有或不具有状态持久性的托管对象)都可以创建计时器。 但是,在大多数情况下,仅为单例创建计时器是有意义的。 将为所有活动对象设置延迟。 通常需要及时启动计划的任务,这就是为什么在单例中使用它的原因。 出于相同的原因,在此示例中,当应用程序启动时,EJB对象必须处于活动状态。 这样可以确保计时器立即开始工作。

如果将计时器描述为常量,则其生存期将延长到JVM的整个生命周期。 容器负责存储持久性计时器,通常在数据库中。 永久计时器(在应用程序不可用时应该工作)在启动时打开。 它还允许您将相同的计时器与对象的多个实例一起使用。 如果您需要在多台服务器上仅一次执行一个业务流程,则使用具有适当服务器配置的恒定计时器是一种适当的解决方案。

使用类Unix的cron表达式描述了使用Schedule注释自动创建的计时器。 为了增加灵活性,使用容器提供的计时器服务以编程方式描述EJB计时器,该服务创建了Timers和Timeout回调方法。

也可以使用容器管理的调度程序服务在EJB外部描述周期性和延迟的任务。 在托管组件中实现了ManagedScheduledExecutorService的实例,该实例在指定的延迟之后或以指定的时间间隔执行任务。 这些任务将在容器驱动的线程中实现:

 @ApplicationScoped public class Periodic { @Resource ManagedScheduledExecutorService mses; public void startAsyncJobs() { mses.schedule(this::execute, 10, TimeUnit.SECONDS); mses.scheduleAtFixedRate(this::execute, 60, 10, TimeUnit.SECONDS); } private void execute() { … } } 

调用startAsyncJobs()方法将在调用后十秒钟在托管线程上执行execute()函数,然后在第一分钟后每十秒钟执行一次。

JAX-RS中的异步性和反应性

JAX-RS支持异步行为,以免不必要地阻止服务器端请求流。 即使HTTP连接正在等待响应,请求流也可能继续处理其他请求,而服务器上运行的过程很长。 请求流聚集在一个容器中,并且此请求存储库具有一定大小。 为了不浪费请求流,JAX-RS异步资源方法创建了一些任务,这些任务在请求流返回时可以执行并可以重用。 HTTP连接恢复并在异步任务完成后或超时后给出响应。 以下示例显示了JAX-RS异步资源方法:

 @Path("users") @Consumes(MediaType.APPLICATION_JSON) public class UsersResource { @Resource ManagedExecutorService mes; … @POST public CompletionStage<Response> createUserAsync(User user) { return CompletableFuture.supplyAsync(() -> createUser(user), mes); } private Response createUser(User user) { userStore.create(user); return Response.accepted().build(); } } 

为了使请求流忙得太久,JAX-RS方法必须快速完成。 这是由于以下事实:通过控制反转从容器调用资源方法。 在完成阶段获得的结果将用于在处理结束时恢复客户端连接。

完成阶段的返回是JAX-RS API中的一种相对较新的技术。 如果您需要描述延迟并同时为异步响应提供更大的灵活性,则可以在方法中包括AsyncResponse类型。 下面的示例演示了这种方法:

 import javax.ws.rs.container.AsyncResponse; import javax.ws.rs.container.Suspended; @Path("users") @Consumes(MediaType.APPLICATION_JSON) public class UsersResource { @Resource ManagedExecutorService mes; … @POST public void createUserAsync(User user, @Suspended AsyncResponse response) { response.setTimeout(5, TimeUnit.SECONDS); response.setTimeoutHandler(r -> r.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE).build())); mes.execute(() -> response.resume(createUser(user))); } } 

由于创建了超时,客户端请求将不会无限期地等待,而仅在收到结果或呼叫超时到期之前。 但是,计算将继续进行,因为它们是异步执行的。 对于实现为EJB的JAX-RS资源,可以应用@Asynchronous批注,以便您不通过服务执行程序显式调用异步业务方法。

JAX-RS客户端还支持异步行为。 根据要求,在HTTP调用期间不要阻止它是有意义的。 前面的示例显示了如何为客户端请求设置延迟。 对于长时间运行的(尤其是并行的)外部系统调用,最好使用异步和响应行为。

考虑几个提供天气信息的服务器应用程序。 客户端组件访问所有这些应用程序并计算平均天气预报。 理想情况下,您可以并行访问系统:

 import java.util.stream.Collectors; @ApplicationScoped public class WeatherForecast { private Client client; private List<WebTarget> targets; @Resource ManagedExecutorService mes; @PostConstruct private void initClient() { client = ClientBuilder.newClient(); targets = … } public Forecast getAverageForecast() { return invokeTargetsAsync() .stream() .map(CompletableFuture::join) .reduce(this::calculateAverage) .orElseThrow(() -> new IllegalStateException("   ")); } private List<CompletableFuture<Forecast>> invokeTargetsAsync() { return targets.stream() .map(t -> CompletableFuture.supplyAsync(() -> t .request(MediaType.APPLICATION_JSON_TYPE) .get(Forecast.class), mes)) .collect(Collectors.toList()); } private Forecast calculateAverage(Forecast first, Forecast second) { … } @PreDestroy public void closeClient() { client.close(); } } 

invokeTargetsAsync()方法异步调用可用对象,从而调用计划的执行程序服务。 返回CompletableFuture描述符,并将其用于计算平均结果。 join()方法的开始将被阻止,直到调用完成并接收到结果为止。

被异步调用的对象会立即启动并等待来自多个资源的响应,这可能会更慢。 在这种情况下,等待来自气象服务资源的响应所花费的时间与预期最慢的响应一样长,而不是所有响应都在一起。

最新版本的JAX-RS内置了对完成阶段的支持,从而减少了应用程序中的原型代码。 与填充值一样,该调用会立即返回完成阶段代码以供将来参考。 以下示例显示了使用rx()调用的JAX-RS反应式客户端功能:

 public Forecast getAverageForecast() { return invokeTargetsAsync() .stream() .reduce((l, r) -> l.thenCombine(r, this::calculateAverage)) .map(s -> s.toCompletableFuture().join()) .orElseThrow(() -> new IllegalStateException("   ")); } private List<CompletionStage<Forecast>> invokeTargetsAsync() { return targets.stream() .map(t -> t .request(MediaType.APPLICATION_JSON_TYPE) .rx() .get(Forecast.class)) .collect(Collectors.toList()); } 

在上面的示例中,您无需搜索计划的执行者的服务-JAX-RS客户端将自行管理该服务。 在出现rx()方法之前,客户端使用了显式的async()调用。 此方法的行为类似,但仅返回Future对象。 对于大多数项目,在客户中使用响应式方法是最佳选择。
如您所见,Java EE使用容器管理的艺术家服务。

现代Java EE中的设计概念和原理


Java EE API基于约定为标准的约定和设计原则。 软件工程师将在其中找到熟悉的API模板和应用程序开发方法。 Java EE的目标是促进API的一致使用。

应用程序的主要原理主要集中在业务场景的实现上,即:技术不应干涉。 如前所述,工程师应该能够专注于实现业务逻辑,而不必将大部分时间花费在技术和基础架构问题上。 理想情况下,域逻辑是用简单的Java实现的,并由公司环境支持的注释和其他属性加以补充,而不会影响域代码或使其复杂化。 这意味着该技术不需要工程师的太多关注,也不会施加太大的限制。 过去,J2EE环境需要许多非常复杂的解决方案。 为了实现接口并扩展基类,我们必须使用托管对象和持久性存储对象。 这使主题区域的逻辑变得复杂,并使测试变得困难。

在Java EE中,领域逻辑是以带有注释的简单Java类的形式实现的,根据该类,容器可以在应用程序执行期间解决某些公司任务。 创建干净代码的做法通常涉及编写比重复使用更漂亮的代码。 Java EE支持这种方法。 如果出于某种原因需要删除技术并保留主题区域的纯逻辑,则只需删除相应的注释即可。

正如我们将在第7章中看到的那样,这种编程方法意味着需要进行测试,因为对于程序员而言,大多数Java EE规范只不过是注释。

在整个API中,采用了一种称为控制反转(IoC)的设计原则-换句话说,“不要叫我们,我们会叫自己”。 这在应用电路(例如JAX-RS资源)中尤其明显。 资源方法使用Java方法注释来描述,Java注释随后由容器在适当的上下文中调用。 对于依赖项注入也是如此,在这种情况下,您必须选择生成器或考虑诸如拦截器之类的跨领域任务。 应用程序开发人员可以专注于实现逻辑和描述关系,而将技术细节的实现留在容器中。 另一个例子(不是很明显)是通过JSON-B注释将Java对象转换为JSON,反之亦然的描述。 对象不仅以显式的编程形式进行转换,而且以声明式的形式进行隐式转换。

允许工程师有效应用此技术的另一个原则是通过协议进行编程。 默认情况下,Java EE定义了一种适合大多数使用情况的特定行为。 如果不够或不满足要求,则可以在多个级别上重新定义行为。
有许多约定编程的例子。 其中之一是使用JAX-RS资源方法,该方法将Java功能转换为HTTP响应。 如果JAX-RS关于响应的标准行为不满足要求,则可以应用响应类型Response。 另一个示例是托管对象的规范,通常使用注释来实现。 要更改此行为,可以使用beans.xml XML描述符。 对于程序员而言,在现代Java EE世界中,企业应用程序以务实而高性能的方式开发,通常不需要像以前那样大量使用XML,这非常方便。

至于程序员的生产力,Java EE上开发的另一个重要原则是该平台需要集成到各种标准的容器中。 因为容器支持一组特定的API(并且确实支持整个Java EE API),所以它确实需要API实现以提供其他API的无缝集成。 这种方法的优点是无需注释即可使用JSON-B转换和Bean验证技术的JAX-RS资源,而无需额外的显式配置。 在前面的示例中,我们看到了如何在不付出额外努力的情况下一起使用各个标准中定义的功能。 这是Java EE平台的最大优势之一。 通用规范可保证各个标准的组合。 程序员可以依靠应用服务器提供的某些功能和实现。

易于使用的高质量代码


程序员通常同意您应该努力编写高质量的代码。 但是,并非所有技术都同样适用于此。

如本书开头所述,应用程序开发的重点应该是业务逻辑。 如果业务逻辑发生变化或出现新知识,则必须更新域模型以及源代码。 要创建和维护高质量的域模型和整个源代码,就需要迭代重构。 问题导向设计的概念描述了加深对主题领域的理解的努力。

关于代码级重构的文献很多。 - , , . , . , .

- . , , — , , - . — , , . . , , .

, . , .

, , , . , , - . , , , - , . , . 7.

, . , , , . Java EE : , , . .

. , , , . . 6 , .

. , , , . , . , , . , . . , . - .

关于作者


(Sebastian Daschner) — Java-, , Java (EE). JCP, Java EE, JSR 370 374 . Java Java - Oracle.

IT-, JavaLand, JavaOne Jfokus. JavaOne Rockstar JavaOne 2016. Java (Steve Chin) Java, . JOnsen — Java, .


(Melissa McKay) — 15- , . Java-, . , , .

JCrete () JOnsen . IT- , JavaOne4Kids JCrete4Kids. JavaOne 2017 Denver Java User Group.

»这本书的更多信息可以在出版商的网站上找到
» 目录
» 摘录

20% — Java EE

Source: https://habr.com/ru/post/zh-CN418025/


All Articles