如果我们使用的是PostgreSQL数据库,那么我们可以使用LIKE和ILIKE做模糊查询,LIKE语法是SQL标准而ILIKE是PostgreSQL的一个扩展。
先创建一张表,然后插入一些数据;
1 | create table test( |
在使用 LIKE/ILIKE 时,有两个通配符:百分号 (%) 和下划线 (_)
先看一下这个的查询:
1 | my_test_db=#select * from test where name like 'O%'; |
这个语句匹配所有以 O 打头的数据。
下面查询使用通配符 _
1 | my_test_db=#select * from test where name like '_n_'; |
上面都是使用LIKE做模糊匹配,现在我们用一下ILIKE(除了不区分大小写外,和LIKE一样)。
1 | my_test_db=#select * from test where name ilike 'O%'; |
使用 lower() 函数和 LIKE 也能实现上面的效果
1 | my_test_db=#select * from test where lower(name) like 'o%'; |
结果和上面sql的执行结果一样。
我推荐使用lower like,因为它的性能比ILIKE高出15%以上,下面做个测试:
首先创建一个表,然后通过脚本插入1000000行随机数据:
1 | require 'securerandom' |
验证数据行数:
1 | my_test_db=# select count(id) from books ; |
做个查询,看一下返回数据
1 | my_test_db=# SELECT "books".* FROM "books" WHERE "books"."published" = 'f' |
分别查看LOWER LIKE和ILIKE的执行计划
1 | my_test_db=# EXPLAIN ANALYZE SELECT "books".* FROM "books" WHERE "books"."published" = 'f' and (LOWER(description) LIKE '%abcde%') ; |
1 | my_test_db=# EXPLAIN ANALYZE SELECT "books".* FROM "books" WHERE "books"."published" = 'f' and (description iLIKE '%abcde%') ; |
从结果可以看到LOWER LIKE比ILIKE快了差不多17%,好了言归正传,继续聊LIKE和ILIKE。
使用默认的转义字符(反斜杠)来转义
1 | select * from test where name like '%\%'; |
上面SQL语句查询以%结尾的数据。
1 | select * from test where name like '%\_%'; |
这个SQL语句查询含有 _ 的数据。
1 | Notice:~~ 和 LIKE、~~* 和 ILIKE、!~~ 和 NOT LIKE、!~~* 和 NOT ILIKE是可以互换的。 |
索引用于加快搜索速度。 PostgreSQL自动为主键、唯一键等列创建索引。或者我们可以显式创建索引。
如果某列有可用的索引(指定text_pattern_ops或varchar_pattern_ops),如果不以 % 或 _ 开头,则 LIKE 会使用该索引(name LIKE ‘one%’ 会走索引,而 name like ‘%one’ 不会走索引)。
对于ILIKE,当且仅当以非字母字符(不受大小写转换影响的字符)开头时,才会走索引。
如果在使用LIKE的时候,查询通配符以 % 或 _ 开头,有没有什么办法走索引?
可以通过一下两步操作,让它走索引:
举个例子,下面的查询不走索引
1 | select * from test where name like '%wo'; |
那么,我们在name列上创建个索引
1 | create index rev_idx on test(reverse(name)); |
将上面的查询SQL变成下面的SQL语句
1 | select * from test where reverse(name) like reverse('%wo'); |
那么现在,就会走索引。
]]>这篇文章主要讲解TransactionalEventListener是怎样工作的?适合在什么场景,能解决哪些问题?以及和EventListener不同之处。
这里举个业务场景,假如我们有个需求,用户创建成功后给用户发送一个邮件。这里有两个事情要做:
对于这种需求,我们可能会不假思索的有以下实现。
1 |
|
为User创建个Repository
1 | public interface UserRepository extends JpaRepository<User, Long> {} |
1 |
|
1 |
|
对于上面的实现,是最容易实现的,但这种实现是有问题的。我们想一下,这个功能的核心是创建用户,而发送邮件是一个副作用(发送邮件不能影响用户的创建),如果把这两个操作放在一个事务中会有什么问题?其实很明显,如果创建用户时抛出异常,事务回滚,方法提前退出,那么也不会发送邮件,这是正常的。但是下面两个场景是不可接受的:
虽然这些情况出现的概率很小,但作为对自己有要求的程序猿,这是不可容忍的,我们要对自己写的业务负责。
好了,我们对上面的实现做个重构,直接将创建用户和发送邮件的业务代码拆开,使用Spring application event的方式解耦实现。
修改后的Service是这样的
1 |
|
从上面的代码,我们知道UserService依赖两个beans:
1 | public class UserCreatedEvent { |
注意这个类只是个简单POJO对象,自从Spring 4.2,我们不用继承ApplicationEvent而能发布任何对象,Spring会把它们包装成PayloadApplicationEvent。
我们需要一个Event Listener处理上面的事件。
1 |
|
通过上面的重构,我们将创建用户和发送邮件的业务代码拆开来了,但是有解决上面提到的问题吗?答案是没有,虽然我们用EventListener的方式解耦了业务代码,可是这在底层两个功能还是在同一个事务中执行(有人可能想问在Listener方法上加@Async让异步执行可以吗?当然不行,邮件必须在用户创建成功后发送,这里有业务依赖),意思就是,上面的两种情况依然会发生。那么问题来了,有没有解决方案呢?
当然有,就是用@TransactionalEventListener替换@EventListener,结果就是在创建用户并提交事务后发送邮件通知。
TransactionalEventListener是对EventListener的增强,被注解的方法可以在事务的不同阶段去触发执行,如果事件未在激活的事务中发布,除非显式设置了 fallbackExecution() 标志为true,否则该事件将被丢弃;如果事务正在运行,则根据其 TransactionPhase 处理该事件。
Notice:你可以通过注解@Order去排序所有的Listener,确保他们按自己的设定的预期顺序执行。
我们先看看TransactionPhase有哪些:
改造后的Listener是这样的
1 |
|
好了,现在我们能保证我们的业务正常的运行,创建用户不会受发送邮件的影响。接下来我们深挖一下,看看TransactionalEventListener是怎么做到的。
下面给出Spring的处理源码,大家会一目了然:
1 |
|
解释一下上面的代码:
既然将TransactionSynchronization存放了起来,那么什么时机触发执行呢?
这里以AFTER_COMMIT为例(其他阶段实现差不多),看这段代码:
AbstractPlatformTransactionManager类
1 | private void processCommit(DefaultTransactionStatus status) throws TransactionException { |
接着看triggerAfterCommit的实现
1 | /** |
这里调用了TransactionSynchronizationUtils的triggerAfterCommit方法,继续往下跟
1 | public static void triggerAfterCommit() { |
看到了吧,先是拿到所有的TransactionSynchronization,然后调用他们的afterCommit方法,就会真正开始处理该Event。
现在我们做一个总结,如果你遇到这样的业务,操作B需要在操作A事务提交后去执行,那么TransactionalEventListener是一个很好地选择。这里需要特别注意的一个点就是:当B操作有数据改动并持久化时,并希望在A操作的AFTER_COMMIT阶段执行,那么你需要将B事务声明为PROPAGATION_REQUIRES_NEW。这是因为A操作的事务提交后,事务资源可能仍然处于激活状态,如果B操作使用默认的PROPAGATION_REQUIRED的话,会直接加入到操作A的事务中,但是这时候事务A是不会再提交,结果就是程序写了修改和保存逻辑,但是数据库数据却没有发生变化,解决方案就是要明确的将操作B的事务设为PROPAGATION_REQUIRES_NEW。
]]>Redis 6.0.0稳定版(GA)终于发布了。这个版本提供了很多令人振奋的新特性和功能改进,比如新的网络协议RESP3、新的集群代理、ACL等,其中最受关注的应该是 多线程,带着诸多疑问,让我们一起开始《Redis 6.0新特性》。
Redis在处理客户端请求时,包括 获取(socket读)、解析、执行、内容返回(socket写),都是由一个顺序的串行主线程来处理的,这就是所谓的 单线程。但如果严格来说,从Redis 4.0开始就不是单线程了,除了主线程外,它还有后台线程,这些线程在处理一些比较慢的操作,比如清理脏数据、释放无用连接、删除大量keys等。
官方也对类似的问题做出了回应。在使用Redis时,几乎不存在CPU瓶颈,Redis主要受到内存和网络的限制。比如在普通的Linux系统上,Redis在使用pipelineing时每秒可以处理100万个请求,所以如果程序主要使用O(N)或O(log(N))级别的操作,几乎不会占用太多CPU。
并且使用单线程的话,可维护性高。多线程模型虽然在某些方面表现良好,但它会引入一些问题需要解决,比如执行顺序的不确定性,并发读写的一系列问题,增加了系统的复杂度,可能会出现线程切换甚至解锁和死锁造成的性能损失。Redis通过AE事件模型和IO多路复用技术,具有非常高的处理性能,所以不需要使用多线程。单线程机制大大降低了Redis内部实现的复杂性。Hash的lazy Rehash、Lpush和其他”线程不安全”的命令可以在没有锁的情况下执行。
Redis把所有的数据都放在内存中,内存的响应时间约为100纳秒。对于小数据包,Redis服务器可以处理8万到10万个QPS。这也是Redis处理的极限。对于80%的公司来说,单台Redis进行处理就足够了。
但随着业务场景越来越复杂,有些公司的交易量动辄上亿,因此需要更大的QPS。常见的解决方案是在分布式架构中对数据进行分区,使用多台服务器,但这种方案有非常大的弊端,比如需要管理的Redis服务器太多,维护成本高;有些适合单台Redis服务器的命令不适用于数据分区;数据分区无法解决热读写问题;数据偏斜、重分布和放大/缩小变得更加复杂等。
从Redis本身的角度来看,由于Redis执行过程中,网络的读写系统调用占据了大部分CPU时间,瓶颈主要是网络的IO消耗。优化的方向主要有两个。
这种协议栈优化的方法与Redis关系不大。支持多线程是最有效、最方便的操作方式。所以总结起来,redis支持多线程主要有两个原因。
Redis 6.0的多线程默认是禁用的,只使用主线程。要启用它,需要修改redis.conf配置文件:
1 | io-threads-do-reads yes |
启用多线程后,需要设置线程数,否则不会生效。同时修改redis.conf配置文件
1 | io-threads 4 |
关于线程数的设置,官方是有推荐的。4核建议设置为2或3线程,8核建议设置为6线程。线程数必须小于服务器的线程数。同时需要注意的是,线程数并非越多越好。官方认为,超过8个基本没有意义。
Redis作者antirez在RedisConf 2019分享中提到。Redis 6引入的多线程IO功能,性能至少提升了一倍。也有国内大牛在阿里巴巴云esc用不稳定版测试过。在4线程IO中,GET/SET命令的性能比单线程提升了近一倍。
测试环境:
1 | Redis Server: Alibaba Cloud Ubuntu 18.04, 8 CPU 2.5 GHZ, 8G memory, host model ecs.ic5.2xlarge |
测试结果:
这些性能验证测试没有进行严格的延迟控制和不同并发场景的压力测试。数据仅供验证和参考,不能作为在线指标使用。
如果启用了多线程,至少需要一台4核机器,而且Redis实例已经占用了相当多的CPU时间,很耗时。否则,使用多线程是没有意义的。所以,估计80%的公司开发人员都只会看看。
现将该过程简单介绍如下:
本设计具有以下特点。
从上面的实现机制可以看出,Redis的多线程部分只用于处理网络数据读写和协议分析,命令的执行仍然是单线程顺序执行。所以我们不需要考虑并发和线程安全问题。
]]>在Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。
守护线程,是指程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
将线程转换为守护线程可以通过调用Thread对象的**setDaemon(true)**方法来实现。在使用守护线程时需要注意以下几点:
thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
在Daemon线程中产生的新线程也是Daemon的。
守护线程应该永远不要去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
void setDaemon(boolean status):此方法用于将当前线程标记为守护线程或用户线程。例如:如果我有一个用户线程tU,则tU.setDaemon(true)将该线程设为守护线程。相反,如果我有一个守护线程tD,则通过调用tD.setDaemon(false)将其设置为用户线程。
语法:
1 | public final void setDaemon(boolean on) |
语法:
1 | public final boolean isDaemon() |
1 | public class DaemonThread extends Thread |
输出:
1 | t1 is Daemon thread |
这里的结果会有多种情况:
通过结果就可以验证前面说的守护线程的特性
如果在启动线程后调用setDaemon()方法,它将抛出IllegalThreadStateException
。
1 | public class DaemonThread extends Thread { |
运行时异常:
1 | Exception in thread "main" java.lang.IllegalThreadStateException |
输出:
1 | Thread name: Thread-0 |
注意:不要在启动线程后调用setDaemon()方法。
Java 8中引入了新的Date-Time
API,解决了旧的日期时间API的以下缺点:
java.util.Date
是线程不安全的,而 新的 Date-Time API
是不可变的且没有提供setter方法Java 8在java.time
包下引入了新的Date-Time API,其中最重要的类是:
1 | // Java code for LocalDate |
输出:
1 | the current date is 2020-04-09 |
1 | // Java code for Zoned date-time API |
输出:
1 | formatted current Date and Time : 09-04-2020 06:21:13 |
1 | LocalDate startDate = LocalDate.of(2018, 2, 20); |
可以从Period对象中获取日期单元,使用getYears()
、getMonhs()
、getDays()
方法。
还可以通过Period的isNegative()
来判断日期的大小。
1 | Instant start = Instant.parse("2020-04-03T10:15:30.00Z"); |
那么我们能使用getSeconds() 或 getNanoseconds() 方法获取时间单元的值
1 | duration.getSeconds(); |
1 | // Java code for period and duration |
输出:
1 | gap between dates is a period of P3Y3M28D |
java.time.temporal.ChronoUnit
枚举,以替换旧API中用于表示日、月等等的整数值。1 | // Java code for ChronoUnits Enum |
输出:
1 | current date is :2020-04-09 |
1 | // Java code Temporal Adjuster |
输出
1 | the current date is 2020-04-09 |
在介绍Spring整合Mybatis原理之前,我们得先来稍微介绍Mybatis的工作原理。
在Mybatis中,我们可以使用一个接口去定义要执行sql,简化代码如下:
定义一个接口,@Select
表示要执行查询sql语句。
1 | public interface UserMapper { |
以下为执行sql代码:
1 | InputStream inputStream = Resources.getResourceAsStream("mybatis.xml"); |
Mybatis的目的是:使得程序员能够以调用方法的方式执行某个指定的sql,将执行sql的底层逻辑进行了封装。
这里重点思考以下mapper这个对象,当调用SqlSession的getMapper方法时,会对传入的接口生成一个代理对象,而程序要真正用到的就是这个代理对象,在调用代理对象的方法时,Mybatis会取出该方法所对应的sql语句,然后利用JDBC去执行sql语句,最终得到结果。
Spring和Mybatis时,我们重点要关注的就是这个代理对象。因为整合的目的就是:把某个Mapper的代理对象作为一个bean放入Spring容器中,使得能够像使用一个普通bean一样去使用这个代理对象,比如能被@Autowire自动注入。
比如当Spring和Mybatis整合之后,我们就可以使用如下的代码来使用Mybatis中的代理对象了:
1 |
|
UserService中的userMapper属性就会被自动注入为Mybatis中的代理对象。如果你基于一个已经完成整合的项目去调试即可发现,userMapper的类型为:org.apache.ibatis.binding.MapperProxy@41a0aa7d。证明确实是Mybatis中的代理对象。
好,那么现在我们要解决的问题的就是:如何能够把Mybatis的代理对象作为一个bean放入Spring容器中?
要解决这个,我们需要对Spring的bean生成过程有一个了解。
Spring启动过程中,大致会经过如下步骤去生成bean
假设有一个A类,假设有如下代码:
一个A类:
1 |
|
一个B类,不存在@Component注解
1 | public class B { |
执行如下代码:
1 | AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); |
输出结果为:com.luban.util.A@6acdbdf5
A类对应的bean对象类型仍然为A类。但是这个结论是不确定的,我们可以利用BeanFactory后置处理器来修改BeanDefinition,我们添加一个BeanFactory后置处理器:
1 |
|
这样就会导致,原本的A类对应的BeanDefiniton被修改了,被修改成了B类,那么后续正常生成的bean对象的类型就是B类。此时,调用如下代码会报错:
1 | context.getBean(A.class); |
但是调用如下代码不会报错,尽管B类上没有@Component注解:
1 | context.getBean(B.class); |
并且,下面代码返回的结果是:com.luban.util.B@4b1c1ea0
1 | AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); |
之所以讲这个问题,是想说明一个问题:在Spring中,bean对象跟class没有直接关系,跟BeanDefinition才有直接关系。
那么回到我们要解决的问题:如何能够把Mybatis的代理对象作为一个bean放入Spring容器中?
在Spring中,如果你想生成一个bean,那么得先生成一个BeanDefinition,就像你想new一个对象实例,得先有一个class。
继续回到我们的问题,我们现在想自己生成一个bean,那么得先生成一个BeanDefinition,只要有了BeanDefinition,通过在BeanDefinition中设置bean对象的类型,然后把BeanDefinition添加给Spring,Spring就会根据BeanDefinition自动帮我们生成一个类型对应的bean对象。
所以,现在我们要解决两个问题:
注意:上文中我们使用的BeanFactory后置处理器,他只能修改BeanDefinition,并不能新增一个BeanDefinition。我们应该使用Import技术来添加一个BeanDefinition。后文再详细介绍如果使用Import技术来添加一个BeanDefinition,可以先看一下伪代码实现思路。
假设:我们有一个UserMapper接口,他的代理对象的类型为UserMapperProxy。
那么我们的思路就是这样的,伪代码如下:
1 | BeanDefinitoin bd = new BeanDefinitoin(); |
但是,这里有一个严重的问题,就是上文中的UserMapperProxy是我们假设的,他表示一个代理类的类型,然而Mybatis中的代理对象是利用的JDK的动态代理技术实现的,也就是代理对象的代理类是动态生成的,我们根本无法确定代理对象的代理类到底是什么。
所以回到我们的问题:Mybatis的代理对象的类型是什么?
本来可以有两个答案:
那么答案1就相当于没有了,因为是代理类是动态生成的,那么我们来看答案2:代理对象对应的接口
如果我们采用答案2,那么我们的思路就是:
1 | BeanDefinition bd = new BeanDefinitoin(); |
但是,实际上给BeanDefinition对应的类型设置为一个接口是行不通的,因为Spring没有办法根据这个BeanDefinition去new出对应类型的实例,接口是没法直接new出实例的。
那么现在问题来了,我要解决的问题:Mybatis的代理对象的类型是什么?
两个答案都被我们否定了,所以这个问题是无解的,所以我们不能再沿着这个思路去思考了,只能回到最开始的问题:如何能够把Mybatis的代理对象作为一个bean放入Spring容器中?
总结上面的推理:我们想通过设置BeanDefinition的class类型,然后由Spring自动的帮助我们去生成对应的bean,但是这条路是行不通的。
那么我们还有没有其他办法,可以去生成bean呢?并且生成bean的逻辑不能由Spring来帮我们做了,得由我们自己来做。
有,那就是Spring中的FactoryBean。我们可以利用FactoryBean去自定义我们要生成的bean对象,比如:
1 |
|
我们定义了一个LubanFactoryBean,它实现了FactoryBean,getObject方法就是用来自定义生成bean对象逻辑的。
执行如下代码:
1 | public class Test { |
将打印:
lubanFactoryBean: com.luban.util.LubanFactoryBean$1@4d41cee
&lubanFactoryBean: com.luban.util.LubanFactoryBean@3712b94
lubanFactoryBean-class: class com.sun.proxy.$Proxy20
从结果我们可以看到,从Spring容器中拿名字为”lubanFactoryBean”的bean对象,就是我们所自定义的jdk动态代理所生成的代理对象。
所以,我们可以通过FactoryBean来向Spring容器中添加一个自定义的bean对象。上文中所定义的LubanFactoryBean对应的就是UserMapper,表示我们定义了一个LubanFactoryBean,相当于把UserMapper对应的代理对象作为一个bean放入到了容器中。
但是作为程序员,我们不可能每定义了一个Mapper,还得去定义一个LubanFactoryBean,这是很麻烦的事情,我们改造一下LubanFactoryBean,让他变得更通用,比如:
1 |
|
改造LubanFactoryBean之后,LubanFactoryBean变得灵活了,可以在构造LubanFactoryBean时,通过构造传入不同的Mapper接口。
实际上LubanFactoryBean也是一个Bean,我们也可以通过生成一个BeanDefinition来生成一个LubanFactoryBean,并给构造方法的参数设置不同的值,比如伪代码如下:
1 | BeanDefinition bd = new BeanDefinitoin(); |
特别说一下注意二,表示表示当前BeanDefinition在生成bean对象时,会通过调用LubanFactoryBean的构造方法来生成,并传入UserMapper的Class对象。那么在生成LubanFactoryBean时就会生成一个UserMapper接口对应的代理对象作为bean了。
到此为止,其实就完成了我们要解决的问题:把Mybatis中的代理对象作为一个bean放入Spring容器中。只是我们这里是用简单的JDK代理对象模拟的Mybatis中的代理对象,如果有时间,我们完全可以调用Mybatis中提供的方法区生成一个代理对象。这里就不花时间去介绍了。
到这里,我们还有一个事情没有做,就是怎么真正的定义一个BeanDefinition,并把它添加到Spring中,上文说到我们要利用Import技术,比如可以这么实现:
定义如下类:
1 | public class LubanImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { |
并且在AppConfig上添加@Import注解:
1 |
|
这样在启动Spring时就会新增一个BeanDefinition,该BeanDefinition会生成一个LubanFactoryBean对象,并且在生成LubanFactoryBean对象时会传入UserMapper.class对象,通过LubanFactoryBean内部的逻辑,相当于会自动生产一个UserMapper接口的代理对象作为一个bean。
总结一下,通过我们的分析,我们要整合Spring和Mybatis,需要我们做的事情如下:
这样就可以基本完成整合的需求了,当然还有两个点是可以优化的
第一,单独再定义一个@LubanScan的注解,如下:
1 |
|
这样在AppConfig上直接使用@LubanScan即可
第二,在LubanImportBeanDefinitionRegistrar中,我们可以去扫描Mapper,在LubanImportBeanDefinitionRegistrar我们可以通过AnnotationMetadata获取到对应的@LubanScan注解,所以我们可以在@LubanScan上设置一个value,用来指定待扫描的包路径。然后在LubanImportBeanDefinitionRegistrar中获取所设置的包路径,然后扫描该路径下的所有Mapper,生成BeanDefinition,放入Spring容器中。
所以,到此为止,Spring整合Mybatis的核心原理就结束了,再次总结一下:
以上这个三个要素分别对象org.mybatis.spring中的:
Stream(流)是一个来自数据源的元素队列并支持聚合操作
Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
以上的流程转换成Java代码为:
1 | List<Integer> transactionsIds = |
在java 8中, 集合接口有两个方法来生成流:
1 | List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl"); |
1 | List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl"); |
1 | List number = Arrays.asList(2,3,4,5); |
1 | List names = Arrays.asList("Reflection","Collection","Stream"); |
1 | List names = Arrays.asList("Reflection","Collection","Stream"); |
1 | Random random = new Random(); |
1 | List number = Arrays.asList(2,3,4,5,3); |
1 | Random random = new Random(); |
1 | //reduce方法将BinaryOperator用作参数 |
1 | //a simple program to demonstrate the use of stream in java |
输出:
1 | [4, 9, 16, 25] |
双冒号(::)操作,也被称为方法引用运算符,用于直接调用指定类的方法。它的行为与lambda表达式完全相同。它与lambda表达式的唯一区别在于,它使用名称直接引用方法,而不是提供方法的委托。
语法:
1 | <Class name>::<method name> |
示例:打印Stream
的所有元素:
1 | stream.forEach(s-> System.out.println(s)); |
完整示例:
1 | // Java code to print the elements of Stream |
输出:
1 | Geeks |
1 | stream.forEach(System.out::println(s)); |
完整示例:
1 | // Java code to print the elements of Stream |
输出:
1 | Geeks |
方法引用或双冒号运算符可用于以下方法:
如何在Java中使用方法引用:
语法:
1 | (ClassName::methodName) |
示例:
1 | SomeClass::someStaticMethod |
完整示例:
1 | // Java code to show use of double colon operator |
输出:
1 | Geeks |
语法:
1 | (objectOfClass::methodName) |
示例:
1 | System.out::println |
完整示例:
1 | // Java code to show use of double colon operator |
输出:
1 | Geeks |
语法:
1 | (super :: methodName) |
示例:
1 | super::someSuperClassMethod |
完整示例:
1 | // Java code to show use of double colon operator |
输出:
1 | Hello Geeks |
语法:
1 | (ClassName::methodName) |
示例:
1 | SomeClass::someInstanceMethod |
完整示例:
1 | // Java code to show use of double colon operator |
输出:
1 | Geeks |
语法:
1 | (ClassName::new) |
示例:
1 | ArrayList::new |
完整示例:
1 | // Java code to show use of double colon operator |
输出:
1 | Hello Geeks |
在Java 8之前,接口只能定义抽象方法。这些方法的实现必须在单独的类中提供。因此,如果要在接口中添加新方法,则必须在实现接口的类中提供其实现代码。为了克服此问题,Java 8引入了默认方法的概念,允许接口定义具有实现体的方法,而不会影响实现接口的类。
1 | // A simple program to Test Interface default |
输出:
1 | 16 |
引入默认方法可以提供向后兼容性,以便现有接口可以使用lambda表达式,而无需在实现类中实现这些方法。
接口也可以定义静态方法,类似于类的静态方法。
1 | // A simple Java program to TestClassnstrate static |
输出:
1 | 16 |
如果一个类实现了多个接口且这些接口中包含了一样的方法签名,则实现类需要显式的指定要使用的默认方法,或者应重写默认方法。
1 | // A simple Java program to demonstrate multiple |
输出:
1 | Default TestInterface1 |
一个functional interface
是仅包含一个抽象方法的接口。他们只能做一个操作。从Java 8开始,lambda表达式可用来表示functional interface
的实例。functional interface
可以有多个默认方法或静态方法。Runnable、ActionListener和Comparable都是functional interface
的一些示例。
在Java 8之前,我们必须创建匿名内部类对象或实现这些接口。
1 | // Java program to demonstrate functional interface |
输出:
1 | New thread created |
从Java 8开始,我们可以使用lambda表达式来代表functional interface
的实例,如下所示:
1 | // Java program to demonstrate Implementation of |
输出:
1 | New thread created |
标注了@FunctionalInterface
注解的接口可以确保不能有多个抽象方法。如果存在多个抽象方法,则编译器会报“Unexpected @FunctionalInterface annotation”错误。然而,并不强制使用该注解。
1 | // Java program to demonstrate lamda expressions to implement |
输出:
1 | 25 |
Java 8中的java.util.function
包 包含许多内置的functional interface
,例如-
test
,该方法返回一个Boolean值。1 | public interface Predicate |
apply
,该方法传入两个参数并返回相同类型的结果。1 | public interface BinaryOperator |
apply
,该方法传入类型为T的参数并返回类型为R的结果。1 | public interface Function |
1 | // A simple program to demonstrate the use |
输出:
1 | Geek |
functional interface
只有一个抽象方法,但可以有多个默认方法或静态方法。@FunctionalInterface
注解用于确保接口不能具有多个抽象方法。此注释的使用是可选的。java.util.function
包内包含Java 8中许多内置的functional interface
。网上有很多关于Java线程生命周期
的文章,大部分都说Java线程有5种状态,你认可他们的说法吗?反正我不认可,java中的线程实际上有6种状态。贴出证据:
1 | public enum State { |
Thread.class 中定义了线程状态的State枚举类.
官方的注释:A thread can be in only one state at a given point in time. These states are virtual machine states which do not reflectany operating system thread states.
翻译出来为:一个线程在某一时刻只能有一种状态,这些状态是虚拟机状态,它不反映任何操作系统的线程状态。
再看一下RUNNABLE状态的注释:A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.
翻译出来为:处于runnable状态下的线程正在Java虚拟机中执行,但它可能正在等待操作系统的其他资源,比如处理器。
Java中的RUNNABLE状态实际上包含了Ready与Running的状态,所以你可以完全无视网上那些不准确的说法,这种问题的答案往往就在源码与javadoc中。
接下来,我们将详细讨论Java的核心概念-线程的生命周期。
在Java语言中,多线程由Thread的核心概念驱动。在其生命周期中,线程会经历各种状态:
java.lang.Thread类含一个State状态枚举。在任何给定的时间点,线程只能处于以下某一个状态:
一个新线程是指已创建但尚未启动的线程。它保持这种状态,直到我们使用start()方法启动它为止。
1 | Runnable runnable = new NewState(); |
由于我们尚未启动提到的线程,因此方法t.getState()会输出:
1 | NEW |
当我们创建了一个新线程并调用start()方法时,它会从NEW变为RUNNABLE状态。处于此状态的线程正在运行或可运行(正在等待系统分配资源)。
在多线程环境中,线程调度程序为每个线程分配固定的时间。因此它会运行特定的时间,然后将资源让给其他RUNNABLE线程。
例如,让我们在之前的代码中添加t.start()方法,然后尝试访问其当前状态:
1 | Runnable runnable = new NewState(); |
此代码极大概率会输出:
1 | RUNNABLE |
请注意,在此示例中,并不能保证在执行t.getState()时,它仍然处于RUNNABLE状态。
可能是线程调度程序立即对其进行了调度,并已经完成了执行。在这种情况下,我们可能会得到不同的输出。
当前线程等待锁并尝试访问被其他线程持有锁的一段代码时,它将进入此状态。
1 | public class BlockedState { |
解释代码:
在这种状态下,我们调用t2.getState()并获得以下输出:
1 | BLOCKED |
一个线程在等待其他线程执行特定操作时处于WAITING状态。 根据JavaDocs的说法,任何线程都可以通过调用以下任何一种方法进入此状态:
请注意,在wait()和join()中,我们没有定义任何超时期限,下一节将介绍这种情况。
1 | public class WaitingState implements Runnable { |
解释代码:
如您所料,这里的输出是:
1 | WAITING |
当一个线程在规定的时间内等待另一个线程执行特定操作时,该线程处于TIMED_WAITING状态。
根据JavaDocs的介绍,有五种方法可以将线程置于TIMED_WAITING状态:
1 | public class TimedWaitingState { |
我们创建并启动了线程t1,该线程进入睡眠状态的超时时间为5秒。输出将是:
1 | TIMED_WAITING |
完成执行或异常终止时,它处于TERMINATED状态。
1 | public class TerminatedState implements Runnable { |
在这里,当我们启动线程t1时,下一条语句Thread.sleep(1000)为t1的完成提供了足够的时间,因此该程序将输出显示为:
1 | TERMINATED |
通过本文,我们了解了Java线程的生命周期。讲解了Thread.State枚举定义的六个状态,并通过示例演示出来。文中有不对的地方,欢迎大家指正,谢谢!
]]>如果你在使用Ubuntu操作系统,想要将一个用户添加到一个新的组里面,例如将自己添加到Docker组,以便不用携带sudo而直接输入docker ps 查看启动的镜像信息,下面介绍两个方法:
第一种方法是通过命令行的方式将用户添加到组:
1 | sudo usermod -a -G group username |
替换 group 为要添加的组名
替换 username 为要操作的账号
例如,添加用户 foo 到 docker 组,使用下面的命令
1 | sudo usermod -a -G docker foo |
如果你想要将某个账号移出某个组,可以使用下面的命令
1 | sudo deluser foo docker |
如果你更喜欢GUI,你可以用 gnome-system-tools 应用将用户添加到组中
安装应用命令
1 | sudo apt-get install gnome-system-tools |
安装成功后,打开”Users and Groups”应用
点击管理组按钮
向下滚动以查找要添加/删除用户的组。 选中后单击属性
选中/取消用户名以添加/删除用户
]]>匿名类存在的问题是: 如果匿名类的实现非常简单,例如仅包含一个方法的接口,则匿名类的语法可能看起来很笨拙且不清楚。在这些情况下,您通常 new一个匿名内部类对象
作为参数传递给方法,例如,当某人单击按钮时应采取什么措施。Lambda表达式
能实现这样的需求,它可以更紧凑更简洁的表达单方法类的实例。
本篇文章从以下几点介绍一下Lambda表达式:
假设您正在开发一个社交网络程序。您想新增一个功能,使管理员可以对满足特定条件的社交网络用户执行任何类型的操作,例如发送消息。
用户用以下Person类表示:
1 | public class Person { |
并且用户存储在一个List<Person>
实例中。
我们先从最笨的实现开始,然后使用本地和匿名类对该方法进行改进,最后再使用lambda表达式以一种高效而简洁的方式实现。
一种简单的实现是:创建几个方法,每个方法都会搜索和一个特征(例如性别或年龄)相匹配的成员。以下方法将打印出超过指定年龄的成员:
1 | public static void printPersonsOlderThan(List<Person> roster, int age) { |
这样做可以满足业务需求,但是他的可扩展性非常差,并且每个特征的一种搜索需要写一个方法,很麻烦。可以考虑以下几个问题:
以下方法比前面的 printPersonsOlderThan
方法更通用,它会打印指定年龄范围内的成员:
1 | public static void printPersonsWithinAgeRange(List<Person> roster, int low, int high) { |
考虑以下几个问题:
printPersonsOlderThan
通用,但尝试为每个特征创建单独的方法仍会导致代码脆弱。下面的方法打印和指定的搜索条件匹配的用户:
1 | public static void printPersons(List<Person> roster, CheckPerson tester) { |
这个方法会遍历List中的Person对象,通过CheckPerson检查每个Person,如果满足搜索条件,就会输出Person信息。
要指定搜索条件,实现以下 CheckPerson接口:
1 | interface CheckPerson { |
下面的类实现了CheckPerson接口并且实现了接口中的test的方法,这个方法筛选男性且年龄在18至25岁之间的用户。
1 | class CheckPersonEligibleForSelectiveService implements CheckPerson { |
使用CheckPerson
1 | printPersons( |
以下方法传入的第二个参数是一个匿名类,该类筛选男性且年龄在18至25岁之间的用户:
1 | printPersons( |
这种方法减少了代码量,因为您不必为每个搜索条件创建一个类。但是,考虑到CheckPerson
接口仅包含一个方法,并且匿名类的代码相当庞大,在这种情况下,您可以使用lambda表达式
代替匿名类。
CheckPerson
接口是一个functional interface
。functional interface
是仅包含一个抽象方法的接口 。(functional interface
可能包含一个或多个 默认方法
或 静态方法
。)由于functional interface
仅包含一个抽象方法,因此在实现该方法时可以省略该方法的名称。因此,可以使用lambda表达式
(而不是使用匿名类表达式)。
1 | printPersons( |
可以使用functional interface
来代替CheckPerson
,这可以进一步减少所需的代码量。
1 | interface CheckPerson { |
CheckPerson
一个非常简单的接口,是一个functional interface
。因为JDK已经提供了一些通用的functional interface
,可以在java.util.function
包中找到它们。所以,我们可以直接使用这些functional interface
,如果能够满足我们的需求,我们没必要再定义这样的接口。
例如,可以使用 Predicate<T>
接口代替CheckPerson
。该接口包含方法boolean test(T t)
:
1 | interface Predicate<T> { |
此接口仅包含一个参数类型T。当使用实际参数声明或实例化泛型类型时,您将拥有一个参数化类型。例如,参数化类型Predicate
1 | interface Predicate<Person> { |
此参数化类型包含一个方法,该方法的参数和返回类型与CheckPerson.boolean test(Person p)
相同。因此,可以用Predicate<T>
代替CheckPerson
:
1 | public static void printPersonsWithPredicate( |
因此,以下方法调用和在类中指定搜索条件有相同的效果:
1 | printPersonsWithPredicate( |
这不是使用lambda表达式
的唯一方式。
重新看一下printPersonsWithPredicate
方法,看看在什么地方还可以使用lambda表达式:
1 | public static void printPersonsWithPredicate( |
我们接下来将printPerson
方法用Lambda表达式代替,那么我们需要一个functional interface
,该方法可以传入一个Person类型的参数并返回void。 很幸运,JDK提供的 Consumer<T>
接口满足这样的需求。
1 | public static void processPersons( |
调用该方法时的写法如下:
1 | processPersons( |
如果想对个人资料进行更多处理而不仅仅打印出来,该怎么办?假设要验证用户的个人资料或检索他们的联系信息?在这种情况下,需要一个functional interface
,其中包含一个有返回值的抽象方法。很幸运,JDK提供的 Function<T,R>
接口接口满足这样的需求。
1 | public static void processPersonsWithFunction( |
以下调用先从符合条件的Person中获取电子邮件信息,然后打印出来:
1 | processPersonsWithFunction( |
Lambda表达式包含以下内容:
CheckPerson.test
方法包含一个参数p, 它代表Person类的一个实例。注意:可以省略lambda表达式中参数的数据类型。此外,如果只有一个参数,则可以省略括号。例如,以下lambda表达式也有效:
1 | p-> p.getGender()== Person.Sex.MALE |
箭头标记 ->
由单个表达式或语句块组成。本示例使用以下表达式:
1 | p.getGender()== Person.Sex.MALE |
如果指定单个表达式,将计算表达式并返回其值。另外,可以使用return语句:
1 | p -> { |
return语句
不是表达式。在lambda表达式
中,必须将语句括在大括号{}中。但是,对void方法
的调用不用括在大括号中。例如,以下是有效的lambda表达式:
1 | email -> System.out.println(email) |
注意,lambda表达式看起来很像方法声明。可以将lambda表达式视为匿名方法,即没有名称的方法。
以下示例, Calculator定义多个参数
的lambda表达式
示例:
1 | public class Calculator { |
该方法operateBinary
对两个整数进行数学运算。由IntegerMath
的具体实现来计算。示例中定义了两个Lambda表达式:addition
和 subtraction
。示例输出以下内容:
1 | 40 + 2 = 42 |
像本地和匿名类一样,lambda表达式
可以访问变量。它们对局部变量具有相同的访问权限。
1 | import java.util.function.Consumer; |
本示例输出:
1 | x = 23 |
如果在myConsumer声明里用参数x代替y,编译器将报错:
1 | Consumer<Integer> myConsumer = (x) -> { |
错误信息:methodInFirstLevel(int)
方法已经定义了变量x 。这是因为lambda表达式未引入新的作用域级别。因此,您可以直接访问该范围的字段、方法和局部变量。例如,lambda表达式直接访问methodInFirstLevel
方法的x参数。要访问类中的变量,请使用关键字this。在此示例中,this.x引用成员变量FirstLevel.x。
与本地和匿名类一样,lambda表达式
只能访问用final或effectively final的局部变量和参数。例如,假设您在methodInFirstLevel
方法内部添加以下赋值语句:
1 | void methodInFirstLevel(int x){ |
由于改变了x的值,所以该变量不再是final或实际上final类型的变量。由于lambda表达式myConsumer
会访问FirstLevel.x
变量,结果Java编译器报一条错误信息,类似于lambda表达式引用的本地变量必须是final或实际上是final。
1 | System.out.println(“ x =” + x); |
您如何确定Lambda表达式的类型?回忆一下选择年龄在18至25岁之间的男性用户的lambda表达式:
1 | p -> p.getGender() == Person.Sex.MALE |
此lambda表达式用在以下两个方法:
1 | public static void printPersons(List<Person> roster, CheckPerson tester) |
当调用printPersons
方法时,它期望的数据类型为CheckPerson
,因此lambda表达式为该类型。但是,当调用printPersonsWithPredicate
方法时,它期望的数据类型为Predicate<Person>
,因此lambda表达式就是这种类型。
这些方法期望的数据类型称为目标类型。
]]>对于企业应用程序,正确的对数据库并发访问至关重要。这意味着我们应该能够以有效且防错的方式处理多个事务。
此外,我们需要确保并发读取和更新之间的数据保持一致。
为此,我们可以使用Java Persistence API提供的乐观锁定机制。这导致在同一时间对同一数据进行的多次更新不会相互干扰。
为了使用乐观锁,我们需要一个带有 @Version
注解属性的实体。使用它时,每个读取数据的事务都会保留 version
属性的值。
在事务要进行更新之前,它会再次检查 version
属性。
如果与此同时值已被更改,则将引发 OptimisticLockException
。否则,事务将提交更新并增加值版本属性。
JPA同时提供悲观锁机制,这是处理数据并发访问的另一种机制。
如前所述,乐观锁基于通过检查实体的 version
属性来检测其变化。如果发生任何并发更新,则会发生 OptmisticLockException
异常。之后,我们可以重试更新数据。
乐观锁适合读多写少的应用场景。此外,它在必须分离实体一段时间并且无法持有锁的情况下很有用。
相反,悲观锁机制涉及在数据库级别锁实体。每个事务都可以获取数据锁,只要它持有锁定,任何事务都不能读取,删除或对锁定的数据进行任何更新。
我们可以假设使用悲观锁可能会导致死锁。但是,与乐观锁定相比,它可以确保更高的数据完整性。
版本属性是带有 @Version
注解的属性。它对于启用乐观锁定是必需的。让我们看一个示例实体类:
1 |
|
声明版本属性时,应遵循几个规则:
我们可以通过实体来获取版本属性值,但是我们不能对其进行更新或增加,version
的值由Jpa帮我们来维护。
Jpa可以为没有版本属性的实体支持乐观锁。但是,官方推荐在使用乐观锁时自定义一个版本属性。
JPA为我们提供了两种不同的乐观锁定模式(和两种别名):
我们可以在 LockModeType
类中找到上面列出的所有类型。
众所周知,OPTIMISTIC
和 READ
锁模式是同义词。但是,JPA规范建议我们在新的应用程序中使用 OPTIMISTIC
。
每当我们请求 OPTIMISTIC
锁模式时,Jpa可以皮面数据的脏读和不可重复读。
简而言之,它应确保任何事务都无法提交对另一事务的数据所做的任何修改:
与前面一样,OPTIMISTIC_INCREMENT
和 WRITE
是同义词,但是前者是更可取的。
OPTIMISTIC_INCREMENT
必须满足与 OPTIMISTIC
锁模式相同的条件。此外,它增加了版本属性的值。
要请求乐观锁,我们可以将 LockModeType
作为参数传递给 EntityManager
的 find方法:
1 | entityManager.find(Student.class, studentId, LockModeType.OPTIMISTIC); |
启用锁定的另一种方法是使用 Query
对象的 setLockMode
方法:
1 | Query query = entityManager.createQuery("from Student where id = :id"); |
我们可以通过调用 EnitityManager
的lock方法来设置锁:
1 | Student student = entityManager.find(Student.class, id); |
我们可以使用与先前方法相同的方式来调用 refresh
方法:
1 | Student student = entityManager.find(Student.class, id); |
####6.5.NamedQuery
将 @NamedQuery
与 lockMode
属性一起使用:
1 |
每当发现实体上的乐观锁冲突时,它将引发 OptimisticLockException
异常,我们的服务需要回滚。
我们应该重新加载或刷新实体,之后,我们可以尝试再次更新。
文章主要讲了Spring Data Jpa使用乐观锁,它确保任何更新或删除都不会被覆盖或静默丢失。与悲观锁定相反,它不会在数据库级别锁定实体。
]]>JVM(Java Virtual Machine)是运行Java应用程序的运行时引擎,运行在操作系统之上的,它与硬件没有直接的交互。JVM是JRE(Java Runtime Environment)的一部分。
Java应用程序被称为WORA(Write Once Run Anywhere)。这意味着程序员可以在一个系统上编写Java代码,然后在任何其他系统上运行,无需进行任何调整。
我们都知道Java源文件,通过编译器,能够生产相应的**.Class**文件,也就是字节码文件,而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码。
每种平台的解释器是不同的,但实现的虚拟机是相同的,这也是Java为什么能够跨平台的原因。
主要负责三个活动:
Loading:类加载器读取 .class 文件,生成相应的二进制数据并将其保存在方法区(JAVA8后叫元数据区,文章后面统一叫方法区)。
对于每个.class文件,JVM将以下信息存储在方法区域中:
加载.class文件之后,JVM会为 .class文件 创建Class类型的对象并存储在堆内存中。注意,该对象的类型为java.lang
包中定义的Class 。我们可以通过这个Class对象来获取类级别的信息,例如类名称、父类名称、方法和变量信息等。可以使用Object类的getClass()方法获取此对象的引用。
1 | // A Java program to demonstrate working of a Class type |
输出:
1 | Student |
注意:对每个被加载的.class文件,仅创建一个 Class对象。
1 | Student s2 = new Student(); |
Linking:执行验证(verification)、准备(preparation)解析(resolution)。
java.lang.VerifyError
。注意这里所说的初始化,比如一个类变量定义为:
1 | public static int a = 1314; |
实际上变量a
在准备阶段过后的初始值为0而不是1314,将a
赋值为1314的put static
指令是程序被编译后,存放在类构造器
如果声明a变量为final类型:
1 | public static final int a = 1314; |
在编译阶段为a
生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将a赋值为1314。
Initialization:在这个阶段,所有的静态变量都会初始化为代码中定义的值。顺序是从父类到子类、从上到下执行。
通常,有三种类加载器:
Bootstrap class loader:每个JVM实现都必须具有一个引导类加载器,加载受信任的类。它加载JAVA_HOME/jre/lib
目录中的Java API核心类。该路径通常称为引导路径。它以C、C++等本地语言实现。
Extension class loader:它是引导类加载器的子级。它将加载扩展目录JAVA_HOME/jre/lib/ext
(扩展路径)或java.ext.dirs
系统属性指定的任何其他目录中存在的类。它由Java中sun.misc.Launcher$ExtClassLoader
类实现。
System/Application class loader:它是扩展类加载器的子级。它负责从应用程序类路径(java.class.path
环境变量指定的路径)加载类。它由Java中sun.misc.Launcher$AppClassLoader
类实现。
1 | // Java code to demonstrate Class Loader subsystem |
输出:
1 | null |
注意: JVM通过双亲委派模型进行类的加载。应用程序类加载器将加载请求委托给扩展类加载器,扩展类加载器将请求委托给引导类加载器。如果在引导路径中找到了类,则将装入该类,否则再次将请求传输到扩展类加载器,然后再传输到应用程序类加载器。最后,如果应用程序类加载器无法加载类,则将获得运行时异常java.lang.ClassNotFoundException
。
方法区:方法区中,将存储所有类级别信息,例如类名称、父类名称、方法和变量信息等,包括静态变量。它是共享资源。
堆区:所有对象的信息存储在堆区中。它也是共享资源。
虚拟机栈: JVM为每个线程创建一个运行时栈,该栈存储在此处。此栈的每个块都称为栈帧,用于存储方法调用。该方法的所有局部变量都存储在其相应的栈帧中。线程终止后,它的运行时栈将被JVM销毁。它不是共享资源。
程序计数器:存储当前线程执行指令的地址。显然,每个线程都有单独的程序计数器。
本机方法栈:为每个线程创建单独的本机方法栈。它存储本地方法信息。
执行引擎执行.class(字节码)。它逐行读取字节码,使用各个数据区中的数据和信息并执行指令。它可以分为三个部分:
与本地方法库交互并为执行提供本地库(C、C++)的接口。它使JVM可以调用C/C++库,并可以被特定硬件的C/C++库调用。
它是执行引擎所需的本地库(C、C++)的集合。
]]>精通spring - Mastering Spring.pdf
罗时飞.精通spring.pdf
SPRING开发指南.pdf
spring框架,技术详解及使用指导.pdf
Spring 3.x企业应用开发实战[完整版+书签].pdf
精通Spring:Java轻量级架构开发实践.孟劼.高清文字版.pdf
Spring基础教程.pdf
Spring3_权威开发指南.pdf
SpringBoot实战第4版清晰版.pdf
3.gRPC 在 Spring Cloud 中的应用.pdf
SpringBoot实战第4版清晰版.pdf
获取方式: 关注【Java提升营】,回复书籍获取。
]]>设计模式:Java语言中的应用.pdf
大话设计模式(带目录完整版).pdf
数据结构与算法(JAVA语言版)-中文.pdf
研磨设计模式.pdf
算法(第四版).pdf
Head First 设计模式.pdf
Java数据结构和算法.(第二版).pdf
统一建模语言(UML)参考手册——基本概念.pdf
数据结构与算法分析C++描述.pdf
算法图解 - [美] Aditya Bhargava
《设计模式》中文版.pdf
《面向模式的软件体系结构 卷2:用于并发和网络化对象的模式》.pdf
算法设计与分析_算法导论(中文版第二版).pdf
集体智慧编程-python算法应用.pdf
获取方式: 关注【Java提升营】,回复书籍获取。
]]>Java高手真经(编程基础卷):Java核心编程技术.pdf
Java2核心技术II卷.高级特性7th.pdf
Java极限编程.pdf
JAVA网络编程第3版.pdf
Java2网络协议技术内幕.pdf
Head First Java 中文高清版.pdf
JAVA优化编程.pdf
JDK1.5的泛型实现.pdf
深入剖析Tomcat.pdf
JAVA并发编程实践.pdf
Java解惑(中文).pdf
重构-改善既有代码的设计.pdf
Java并发程序设计教程.pdf
分布式系统原理与范型.pdf
J2EE核心模式.pdf
Java语言编码规范.PDF
Java网络编程精解.pdf
Java深度历险.pdf
Java案例开发.pdf
Java线程.pdf
获取方式: 关注【Java提升营】,回复书籍获取。
]]>获取方式: 关注公众号,回复书籍,获取下载连接。
]]>1 | upstream backend |
1 | if ($http_user_agent ~ "COOCAREHTTPULPADAGENT|WinHttp|WebZIP|FetchURL|node-superagent|FeedDemon|Jullo|JikeSpider|Indy Library|Alexa Toolbar|AskTbFXTV|AhrefsBot|CrawlDaddy|Feedly|Apache-HttpAsyncClient|UniversalFeedParser|ApacheBench|Microsoft URL Control|Swiftbot|ZmEu|oBot|jaunty|Python-urllib|lightDeckReports Bot|YYSpider|DigExt|HttpClient|MJ12bot|heritrix|EasouSpider|Ezooms|BOT/0.1|YandexBot|FlightDeckReports|Linguee Bot|python-requests|HeadlessChrome|^$" ) { |