1. 概述
本文重点介绍在使用Spring MVC提供静态资源(如Javascript和CSS文件)时的缓存。同时介绍下一 当文件更新时,如何从缓存中替换旧版本,不会从缓存中错误地提供,即所谓的“perfect cache”。
虽日暮途远,仍梦想诗和远方
本文主要介绍Java8中流的创建和使用,该Stream 并不是指 IO 中的输入输出(虽然在某些场景和概念上有些类似),而是Java8中带来的关于集合操作相关的新 API 。
有许多方法可以创建不同源的流实例。一旦创建,实例将不会修改其源,因此允许从单个源创建多个实例。
如果创建空流,则应使用empty()方法:
1 | Stream<String> streamEmpty = Stream.empty(); |
通常情况下,在创建时使用empty()方法以避免为没有元素的流返回null:
1 | public Stream<String> streamOf(List<String> list) { |
Stream也可以创建任何类型的Collection(Collection,List,Set):
1 | Collection<String> collection = Arrays.asList("a", "b", "c"); |
Array也可以是Stream的源:
1 | Stream<String> streamOfArray = Stream.of("a", "b", "c"); |
它们也可以从现有数组或数组的一部分创建:
1 | String[] arr = new String[]{"a", "b", "c"}; |
使用构建器时,应在语句的右侧部分另外指定所需类型,否则build()方法将创建Stream 的实例:
1 | Stream<String> streamBuilder = Stream.<String>builder().add("a").add("b").add("c").build(); |
的generate()方法接受 Supplier
1 | Stream<String> streamGenerated = Stream.generate(() -> "element").limit(10); |
上面的代码创建了一个包含 10 个字符串的序列,其值为“element”。
创建无限流的另一种方法是使用iterate()方法:
1 | Stream<Integer> streamIterated = Stream.iterate(40, n -> n + 2).limit(20); |
结果流的第一个元素是iterate()方法的第一个参数。为了创建每个后续元素,指定的函数将应用于前一个元素。在上面的示例中,第二个元素将是42。
Java 8提供了从三种基本类型创建流的可能性:int,long和double。由于Stream
使用新的接口减少了不必要的自动装箱,从而提高效率:
1 | IntStream intStream = IntStream.range(1, 3); |
range(int startInclusive, int endExclusive)方法创建从所述第一参数与第二参数的有序流。它增加后续元素的值,步长等于1。结果不包括最后一个参数,它只是序列的上限。
所述 rangeClosed(int startInclusive, int endInclusive) 方法做同样的只有一个差别-所述第二元素被包括。这两种方法可用于生成三种类型的基元流中的任何一种。
从Java 8开始,Random类为生成基元流提供了方法。例如,以下代码创建一个DoubleStream,它有三个元素:
1 | Random random = new Random(); |
String也可以用作创建流的源。
借助String类的chars()方法。由于没有接口,CharStream在JDK中用IntStream表示字符流代替。
1 | IntStream streamOfChars = "abc".chars(); |
以下示例根据指定的RegEx将String拆分为子字符串:
1 | Stream<String> streamOfString = Pattern.compile(", ").splitAsStream("a, b, c"); |
Java NIO类Files允许通过lines()方法生成文本文件的Stream
1 | Path path = Paths.get("C:\\file.txt"); |
该Charset 可以被指定为所述的自变量line()方法。
流的特点是顺序的不可逆访问,意思就是当你执行了某些方法(终结操作)导致流的遍历到了最后一个元素时,后续的流将不可用了。如以下的代码所示
1 | Stream<String> stream = Stream.of("a", "b", "c").filter(element -> element.contains("b")); |
findAny()将遍历整个流(直至最后一个元素),之后操作后尝试重用相同的引用将触发IllegalStateException:
1 | Optional<String> firstElement = stream.findFirst(); |
由于IllegalStateException是RuntimeException,编译器不会发出有关问题的信号。因此,记住Java 8 不能重用是非常重要的。
这种行为是合乎逻辑的,因为流被设计为提供将有限的操作序列应用于功能样式中的元素源但不存储元素的能力。
因此,为了使以前的代码正常工作,应该进行一些更改:
1 | List<String> elements = Stream.of("a", "b", "c").filter(element -> element.contains("b")).collect(Collectors.toList()); |
要对数据源的元素执行一系列操作并聚合它们的结果,需要三个部分 - 源,中间操作和终结操作。
中间操作返回新的修改流。例如,要创建一个缺少部分元素的新流,应使用skip()方法:
1 | Stream<String> onceModifiedStream = Stream.of("abcd", "bbcd", "cbcd").skip(1); |
如果需要多个修改,则可以链接中间操作。假设我们还需要用前几个字符的子字符串替换当前Stream
1 | Stream<String> twiceModifiedStream = stream.skip(1).map(element -> element.substring(0, 3)); |
如您所见,map()方法将lambda表达式作为参数。
流本身是没有价值的,用户感兴趣的真实事物是终结操作的结果,它可以是某种类型的值或应用于流的每个元素的动作。每个流只能使用一个终结操作。
使用流的正确和最方便的方式是流管道,它是流源,中间操作和终端操作的链。例如:
1 | List<String> list = Arrays.asList("abc1", "abc2", "abc3"); |
中间操作是懒惰的,这意味着只有在终结操作执行需要时才会调用它们。
为了证明这一点,假设我们有方法wasCalled(),它在每次调用时递增一个内部计数器:
1 | private long counter; |
在filter()方法中调用wasCalled():
1 | List<String> list = Arrays.asList(“abc1”, “abc2”, “abc3”); |
由于我们有三个元素的来源,我们预期方法filter()将被调用三次,counter变量的值将是3。但实际上运行此代码根本不会改变counter,它仍然是零,所以,filter()方法甚至没有被调用过一次。原因 - 缺少终结操作。
让我们通过添加map()操作和终端操作 - findFirst()来重写这段代码。我们还将添加一种能够通过记录来跟踪方法调用顺序的功能:
1 | Optional<String> stream = list.stream().filter(element -> { |
结果日志显示filter()方法被调用两次而map()方法只调用一次。这是因为管道垂直执行。在我们的示例中,流的第一个元素不满足filter的 predicate ,第二次调用filter()方法时满足条件,我们通过管道进入map()方法,同时仅仅只需要一个元素满足findFirst(),所以后缀后续的调用都将中止。在这个特定的例子中,懒调用避免了 filter(),map() 两个方法调用 。
从性能的角度来看,正确的执行顺序是流管道操作中非常重要的一环:
1 | long size = list.stream().map(element -> { |
执行此代码会将计数器的值增加三。这意味着流的map()方法被调用了三次。但是最终我们skip(2),只需要的结果流只有一个元素,我们无故地执行了两次昂贵的map()操作。
如果我们改变了skip() 和map()方法的顺序,计数器将只会增加一次,因此,方法map()将只调用一次:
1 | long size = list.stream().skip(2).map(element -> { |
优化规则:减少流大小的中间操作应该放在应用于每个元素的操作之前。因此,在流管道的顶部保留skip(),filter(),distinct()等方法。
API有许多终结操作,它们将流聚合到类型或基元,例如count(),max(),min(),sum(),但这些操作依赖于预先定义好的reduce()实现机制。我们开发过程中如何实现这一点呢,答案是使用:- reduce()*和collect()*方法。
这种方法有三种变体,它们的签名和返回类型不同。它们可以具有以下参数:
identity:累加器的初始值或者如果流为空且没有任何可累积的默认值;
accumulator:一个指定元素聚合逻辑的函数。当累加器为每个减少步骤创建一个新值时,新值的数量等于流的大小,只有最后一个值是有用的。这对性能不是很好。
combiner:聚合累加器结果的函数。仅在并行模式下调用组合器以减少来自不同线程的累加器的结果。
那么,让我们看看这三种方法:
1 | OptionalInt reduced = IntStream.range(1, 4).reduce((a, b) -> a + b); |
reduced = 6(1 + 2 + 3)
1 | int reducedTwoParams = IntStream.range(1, 4).reduce(10, (a, b) -> a + b); |
reducedTwoParams = 16(10 + 1 + 2 + 3)
1 | int reducedParams = Stream.of(1, 2, 3) |
结果与前面的例子(16)中的结果相同,这意味着没有调用该combiner。要使combiner工作,流应该是并行的:
1 | int reducedParallel = Arrays.asList(1, 2, 3).parallelStream() |
这里的结果是不同的(36),并且combiner被调用两次。在这里,还原的工作方式如下算法:accumulator由流的每一个元素加入到跑了三次identity 到流的每一个元素。这些动作都在并行进行。结果,他们有(10 + 1 = 11; 10 + 2 = 12; 10 + 3 = 13;)。现在combiner可以合并这三个结果。它需要两次迭代(12 + 13 = 25; 25 + 11 = 36)。
还可以通过另一个终结操作collection()方法来执行流的 reduce。它接受Collector类型的参数,该参数指定reduce的机制。已经为大多数常见操作创建了预定义的收集器。
在本节中,我们将使用以下List作为所有流的源:
1 | List<Product> productList = Arrays.asList(new Product(23, "potatoes"), |
将流转换为集合(Collection, List or Set):
1 | List<String> collectorCollection = productList.stream().map(Product::getName).collect(Collectors.toList()); |
Reduce 成字符串:
1 | String listToString = productList.stream().map(Product::getName).collect(Collectors.joining(", ", "[", "]")); |
joiner()方法可以有一至三个参数(delimiter, prefix, suffix)。使用joiner()的最方便的地方在于 开发人员不需要检查流是否到达它的末尾以应用后缀而不是应用分隔符。Collector 将负责这一点。
求 Stream 所有数字元素的平均值:
1 | double averagePrice = productList.stream().collect(Collectors.averagingInt(Product::getPrice)); |
求 Stream 所有数字元素的平均值:
1 | int summingPrice = productList.stream().collect(Collectors.summingInt(Product::getPrice)); |
方法averagingXX(),summingXX()和summarizingXX()可以使用原始类型(int,long,double)也可以使用它们的包装类(Integer,Long,Double)。这些方法的另一个强大功能是提供映射。因此,开发人员不需要在collect()方法之前使用额外的map()操作。
收集有关流元素的统计信息:
1 | IntSummaryStatistics statistics = productList.stream().collect(Collectors.summarizingInt(Product::getPrice)); |
通过使用IntSummaryStatistics类型的结果实例,开发人员可以通过应用toString()方法创建统计报告。结果是:*”IntSummaryStatistics {count = 5,sum = 86,min = 13,average = 17,200000,max = 23}”。
通过应用方法getCount(),getSum(),getMin(),getAverage(),getMax(),从这个对象中提取count,sum,min,average的单独值也很容易*,所有这些值都可以从单个管道中提取。
根据指定的函数对流的元素进行分组:
1 | Map<Integer, List<Product>> collectorMapOfLists = productList.stream().collect(Collectors.groupingBy(Product::getPrice)); |
在上面的示例中,流被 reduce 为Map,按价格对所有产品进行分组。
根据某些 Predicatre 将流的元素分成组:
1 | Map<Boolean, List<Product>> mapPartioned = productList.stream().collect(Collectors.partitioningBy(element -> element.getPrice() > 15)); |
让 Collector 执行额外的转换:
1 | Set<Product> unmodifiableSet = productList.stream() |
在这种特殊情况下,Collector已将流转换为Set,然后从中创建不可修改的Set。
定制 Collector :
如果出于某种原因,应该创建自定义Collector,最简单且的方法是使用Collector.of)方法。
1 | Collector<Product, ?, LinkedList<Product>> toLinkedList = |
在此示例中,Collector的实例已 reduce 为 LinkedList
在Java 8之前,并行化很复杂。新兴的 ExecutorService和ForkJoin 简化了一点点,但他们仍然应该牢记如何创建一个具体的 Executor。Java 8引入了一种在功能风格中实现并行性的方法。
API允许创建并行流,以并行模式执行操作。当流的源是Collection或array 时,可以在parallelStream()方法的帮助下实现:
1 | Stream<Product> streamOfCollection = productList.parallelStream(); |
如果流的源不是Collection或array,则应使用parallel()方法:
1 | IntStream intStreamParallel = IntStream.range(1, 150).parallel(); |
Stream API自动使用ForkJoin框架并行执行操作。默认情况下,将使用公共线程池,并且无法(至少现在)为其分配一些自定义线程池。这可以通过使用一组自定义的并行收集器来克服。
在并行模式下使用流时,避免阻塞操作并在任务需要相同的执行时间时使用并行模式(如果一个任务比另一个任务持续时间长,则可能会减慢整个应用程序的工作流程)。
可以使用sequential()方法将并行模式的流转换回顺序模式:
1 | IntStream intStreamSequential = intStreamParallel.sequential(); |
Stream API是一个功能强大但易于理解的工具集,用于处理元素序列。它允许我们减少大量的模板代码,创建更易读的程序,并在正确使用时提高应用程序的工作效率。
在本文中显示的大多数代码示例中,流是未被消费的(我们没有应用close()方法或终结操作)。在真实的应用程序中,不要留下未被消费的流,因为这将导致内存泄漏。
缓存是我们经常在提高应用性能的手段,Spring 对缓存的支持也非常好,我们通常只需要添加 <cache:annotation-driven />
配置,选择合适的 CacheManager 及底层的缓存实现框架,即可在需要缓存的方法中通过 @Cacheable 来完成缓存接入了,如果你使用 SpringBoot 那么整个接入过程则更加的简单,只需要在 maven 中添加缓存相关的 starter 即可(具体的接入过程可以参考 Spring Cache)。
本文主要关注 Spring Cache 通用的基于注解的的解决方案和手动的缓存操作之间的性能对比。
在本教程中,我们将讨论一个非常有用的JPA功能 - Criteria Queries。
它不仅使我们能够在不执行原始SQL的情况下编写查询,而且还为我们提供了一些对查询的面向对象控制,这是Hibernate的主要特性之一。Criteria API允许我们以编程方式构建条件查询对象,我们可以在其中应用不同类型的过滤规则和逻辑条件。
从Hibernate 5.2开始,不推荐使用Hibernate Criteria API,新开发的重点是JPA Criteria API。我们将探索如何使用Hibernate和JPA来构建Criteria Queries。
为了说明API,我们将使用参考JPA实现 - Hibernate。
要使用Hibernate,请确保将最新版本添加到pom.xml文件中:
1 | <dependency> |
可以在这里找到最新版本的Hibernate。
让我们首先看一下如何使用Criteria查询检索数据。我们将看看如何从数据库中获取特定类的所有实例。
我们有一个Item类,它表示数据库中的元组“ITEM”:
1 | public class Item implements Serializable { |
让我们看一个简单的条件查询,它将从数据库中检索“ ITEM”的所有行:
1 | Session session = HibernateUtil.getHibernateSession(); |
以上查询是如何获取所有项目的简单演示。让我们一步一步看看做了些什么:
现在我们已经介绍了基础知识,让我们继续讨论条件查询的一些功能。
CriteriaBuilder可用于限制基于特定条件的查询结果。 通过使用 CriteriaQuery where()方法并提供CriteriaBuilder 创建的 表达式。
以下是常用表达式的一些示例:
要获得价格超过1000的商品:
1 | cr.select(root).where(cb.gt(root.get("itemPrice"), 1000)); |
接下来,获取itemPrice小于1000的项目:
1 | cr.select(root).where(cb.lt(root.get("itemPrice"), 1000)); |
有项目itemNames中含有 chair:
1 | cr.select(root).where(cb.like(root.get("itemName"), "%chair%")); |
记录itemPrice在100到200之间:
1 | cr.select(root).where(cb.between(root.get("itemPrice"), 100, 200)); |
要检查给定属性是否为null:
1 | cr.select(root).where(cb.isNull(root.get("itemDescription"))); |
要检查给定属性是否为null:
1 | cr.select(root).where(cb.isNotNull(root.get("itemDescription"))); |
您还可以使用方法isEmpty()和isNotEmpty()来测试类中的List是否为空。
现在问题不可避免地出现了,我们是否可以将上述两种或两种以上的比较结合起来。答案当然是肯定的 - Criteria API允许我们轻松地链接表达式:
1 | Predicate[] predicates = new Predicate[2]; |
要使用逻辑运算添加两个表达式:
1 | Predicate greaterThanPrice = cb.gt(root.get("itemPrice"), 1000); |
具有上述定义条件的项目与Logical OR连接:
1 | cr.select(root).where(cb.or(greaterThanPrice, chairItems)); |
要获取与使用Logical AND连接的上述条件匹配的项目:
1 | cr.select(root).where(cb.and(greaterThanPrice, chairItems)); |
现在我们已经了解了Criteria的基本用法,让我们来看看Criteria的排序功能。
在下面的示例中,我们按名称的升序排列列表,然后按价格的降序排列:
1 | cr.orderBy( |
在下一节中,我们将了解如何进行聚合函数。
到目前为止,我们已经涵盖了大部分基本主题。现在让我们看看不同的聚合函数:
行数:
1 | CriteriaQuery<Long> cr = cb.createQuery(Long.class); |
以下是聚合函数的示例:
求 平均 的聚合函数:
1 | CriteriaQuery<Double> cr = cb.createQuery(Double.class); |
其他有用的聚合方法是sum(),max(),min() ,count() 等。
从JPA 2.1开始,支持使用Criteria API 执行数据库更新。
CriteriaUpdate有一个set()方法,可用于为数据库记录提供新值:
1 | CriteriaUpdate<Item> criteriaUpdate = cb.createCriteriaUpdate(Item.class); |
在上面的代码片段中,我们从 CriteriaBuilder创建了CriteriaUpdate
CriteriaDelete,顾名思义,使用Criteria API 启用删除操作。我们所需要的只是创建一个CriteriaDelete实例 并使用 where() 方法来应用限制:
1 | CriteriaDelete<Item> criteriaDelete = cb.createCriteriaDelete(Item.class); |
在前面的部分中,我们介绍了如何使用Criteria Queries。
显然,Criteria查询优于HQL的主要和最强硬的优势是漂亮,干净,面向对象的API。
与普通的HQL相比,我们可以简单地编写更灵活的动态查询。逻辑可以通过IDE重构,并具有Java语言本身的所有类型安全优势。
当然也存在一些缺点,特别是在更复杂的连接处。
因此,一般来说,我们将不得不使用最好的工具 - 在大多数情况下可以使用Criteria API,但肯定有一些情况我们必须降低水平。
在上一篇文章中,我们研究了Hystrix的基础知识,以及它如何帮助构建容错和弹性应用程序。
有许多现有的Spring应用程序可以调用可以从Hystrix中受益的外部系统。 遗憾的是,为了集成Hystrix,可能无法重写这些应用程序,但是在Spring AOP的帮助下,可以采用非侵入性的方式集成Hystrix 。