springBoot(2)--初步理解
一、定时任务
1、步骤:
1:在启动类上写@EnableScheduling注解
2:在要定时任务的类上写@component
3:在要定时执行的方法上写@Scheduled(fixedRate=毫秒数)。
2、示例
主类
@SpringBootApplication @EnableScheduling //开启定时任务 public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } }
定时任务类
import java.util.Date; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class Jobs { //表示方法执行完成后5秒 @Scheduled(fixedDelay=5000) public void fixedDelayJob() throws InterruptedException{ System.out.println("fixedDelay 每隔5秒"+new Date()); } //表示每隔3秒 @Scheduled(fixedRate=3000) public void fixedRateJob(){ System.out.println("fixedRate 每隔3秒"+new Date()); } //表示每天8时30分0秒执行 @Scheduled(cron="0 0,30 0,8 ? * ? ")
public void cronJob(){ System.out.println(new Date()+" ...>>cron...."); } }
效果:
3.总结
1.fixedDelay和fixedRate,单位是毫秒,这里这里就是5秒和3秒,它们的区别就是:
,fixedRate就是每多次分钟一次,不论你业务执行花费了多少时间。我都是1分钟执行1次,而fixedDelay是当任务执行完毕后1分钟在执行。所以根据实际业务不同,我们会选择不同的方式。
2.cron表达式:比如你要设置每天什么时候执行,就可以用它
cron表达式,有专门的语法,而且感觉有点绕人,不过简单来说,大家记住一些常用的用法即可,特殊的语法可以单独去查。
cron一共有7位,但是最后一位是年,可以留空,所以我们可以写6位:
* 第一位,表示秒,取值0-59 * 第二位,表示分,取值0-59 * 第三位,表示小时,取值0-23 * 第四位,日期天/日,取值1-31 * 第五位,日期月份,取值1-12 * 第六位,星期,取值1-7,星期一,星期二...,注:不是第1周,第二周的意思 另外:1表示星期天,2表示星期一。 * 第7为,年份,可以留空,取值1970-2099
cron中,还有一些特殊的符号,含义如下:
(*)星号:可以理解为每的意思,每秒,每分,每天,每月,每年... (?)问号:问号只能出现在日期和星期这两个位置。 (-)减号:表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12 (,)逗号:表达一个列表值,如在星期字段中使用“1,2,4”,则表示星期一,星期二,星期四 (/)斜杠:如:x/y,x是开始值,y是步长,比如在第一位(秒) 0/15就是,从0秒开始,每15秒,最后就是0,15,30,45,60 另:*/y,等同于0/y
下面列举几个例子供大家来验证:
0 0 3 * * ? 每天3点执行 0 5 3 * * ? 每天3点5分执行 0 5 3 ? * * 每天3点5分执行,与上面作用相同 0 5/10 3 * * ? 每天3点的 5分,15分,25分,35分,45分,55分这几个时间点执行 0 10 3 ? * 1 每周星期天,3点10分 执行,注:1表示星期天 0 10 3 ? * 1#3 每个月的第三个星期,星期天 执行,#号只能出现在星期的位置
二、异步任务
1、步骤
1: 启动类里面使用@EnableAsync注解开启功能,自动扫描
2:在要异步任务的类上写@component
3:在定义异步任务类写@Async(写在类上代表整个类都是异步,在方法加上代表该类异步执行)
2、示例
主类
@SpringBootApplication @EnableAsync //开启异步任务 public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } }
异步类
import java.util.concurrent.Future; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.stereotype.Component; /** * 功能描述:异步任务业务类 */ @Component @Async public class AsyncTask { //获取异步结果 public Future<String> task4() throws InterruptedException{ long begin = System.currentTimeMillis(); Thread.sleep(2000L); long end = System.currentTimeMillis(); System.out.println("任务4耗时="+(end-begin)); return new AsyncResult<String>("任务4"); } public Future<String> task5() throws InterruptedException{ long begin = System.currentTimeMillis(); Thread.sleep(3000L); long end = System.currentTimeMillis(); System.out.println("任务5耗时="+(end-begin)); return new AsyncResult<String>("任务5"); } public Future<String> task6() throws InterruptedException{ long begin = System.currentTimeMillis(); Thread.sleep(1000L); long end = System.currentTimeMillis(); System.out.println("任务6耗时="+(end-begin)); return new AsyncResult<String>("任务6"); } }
controller类
import java.util.concurrent.Future; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.jincou.task.AsyncTask; import com.jincou.util.JsonData; @RestController @RequestMapping("/api/v1") public class UserController { @Autowired private AsyncTask task; @GetMapping("async_task") public JsonData exeTask() throws InterruptedException{ long begin = System.currentTimeMillis(); Future<String> task4 = task.task4(); Future<String> task5 = task.task5(); Future<String> task6 = task.task6(); //如果都执行往就可以跳出循环,isDone方法如果此任务完成,true for(;;){ if (task4.isDone() && task5.isDone() && task6.isDone()) { break; } } long end = System.currentTimeMillis(); long total = end-begin; System.out.println("执行总耗时="+total); return JsonData.buildSuccess(total); } }
结果:
页面:
后台:
3、总结
从上面示例我们可以看出:如果同步方法,那我们需要6秒,而异步执行,我们只需要3秒左右,这就是异步的作用。
1)要把异步任务封装到类里面,不能直接写到Controller
2)增加Future<String> 返回结果 AsyncResult<String>("task执行完成");
3)如果需要拿到结果 需要判断全部的 task.isDone()
logback日志
一、概述
和log4j优点:
实际上,这两个日志框架都出自同一个开发者之手,Logback 相对于 Log4J 有更多的优点
(1)logback不仅性能提升了,初始化内存加载也更小了。
(2)内容更丰富的文档
(3)更强大的过滤器
二、步骤
1、maven配置jar包
logback默认就在spring-boot-starter-web包中,所以我们只要有spring-boot-starter-web
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
二、新建logback-spring.xml
springboot官方建议命名为logback-spring.xml,在src/main/resources新建logback-spring.xml,这里不需要在application.properties中配置
默认加载加载配置顺序 logback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovy
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <!-- 打印到控制台 --> <appender name="consoleApp" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <pattern> %date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n </pattern> </layout> </appender> <!-- 打印到文件 --> <appender name="fileInfoApp" class="ch.qos.logback.core.rolling.RollingFileAppender"> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>DENY</onMatch> <onMismatch>ACCEPT</onMismatch> </filter> <encoder> <pattern> %date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n </pattern> </encoder> <!-- 滚动策略 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 路径 --> <fileNamePattern>app_log/log/app.info.%d.log</fileNamePattern> </rollingPolicy> </appender> <!-- 打印到文件 --> <appender name="fileErrorApp" class="ch.qos.logback.core.rolling.RollingFileAppender"> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level> </filter> <encoder> <pattern> %date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n </pattern> </encoder> <!-- 设置滚动策略 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 路径,因为有 %d所以每天都会意时间来创建新的文件 --> <fileNamePattern>app_log/log/app.err.%d.log</fileNamePattern> <!-- 控制保留的归档文件的最大数量,超出数量就删除旧文件,假设设置每个月滚动, 且<maxHistory> 是1,则只保存最近1个月的文件,删除之前的旧文件 --> <MaxHistory>1</MaxHistory> <!-- 保持30天的历史记录上限为3GB总大小 --> <totalSizeCap>3GB</totalSizeCap> </rollingPolicy> </appender>
<!--这里选择INFO就代表,进行INFO级别输出记录,那么在控制台也好,log文件也好只记录INFO及以上级别的日志,这里相当于第一道过滤器--> <root level="INFO"> <appender-ref ref="consoleApp"/> <appender-ref ref="fileInfoApp"/> <appender-ref ref="fileErrorApp"/> </root> </configuration>
有关配置文件详细介绍放到下面讲,这里先跳过
3.controller类
@RestController @RequestMapping("/api/v1") public class UserController { private Logger logger = LoggerFactory.getLogger(this.getClass()); @GetMapping("log") public Object testLog(){ logger.debug("---debug---debug---"); logger.info("---info---信息---"); logger.warn("---warn ---警告--- "); logger.error("---err---错误---"); //这也是个错误的日志级别 int i=1/0; return "yes"; } }
4、效果:
我们发现,在项目中自动创建app_log/log/app.info(当天日期).log和app.err(当天日期).log文件
然后我们看
app.err.2018-05-21.log
它只存放err级别日志
app.info.2018-05-21.log
它只存放的info和warn两种基本日志,这是为什么呢?详细介绍下配置文件
三、配置文件介绍
一:根节点<configuration>
它的三个子节点
(1)<appender></appender> 格式化日志输出节点,有两个必要属性name和class。
(2) <logger></logger> 用来设置某一个包或具体的某一个类的日志打印级别、以及指定<appender>。
(3)<root></root>(要加在最后)(必选,而且只有一个)(用来指定最基础的日志输出级别,他有俩个自己点可以用来应用appender,格式化日志输出)
二、appender节点
它有两个属性name和class
(1) class="ch.qos.logback.core.ConsoleAppender":把日志输出到
(2)class="ch.qos.logback.core.FileAppender":把日志添加到文件
(3)class="ch.qos.logback.core.rolling.RollingFileAppender":滚动记录文件,先将日志文件指定到文件,当符合某个条件时,将日志记录到其他文件
所以上面第一个appender代表输出到控制台,同时并没有进行任何过滤,所以只要是INFO级别以上的都会在控制台输出。
四、filter过滤器
其它的一看就懂,就这里,一开始一直想不明白,为什么上面的info文件里只有info和warn,而err文件中只有err日志。后来明白了。
他有几个常用的过滤器
(1)LevelFilter 过滤器
级别过滤器,根据日志级别进行过滤。如果日志级别等于配置级别,过滤器会根据onMath 和 onMismatch接收或拒绝日志。
<filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>DENY</onMatch> <onMismatch>ACCEPT</onMismatch> </filter>
<level>:设置过滤级别
将过滤器的日志级别配置为ERROR,所有ERROR级别的日志交给appender处理,非ERROR级别的日志,被过滤掉。
(这样的话那上面不是只显示ERROR日志,怎么最后显示的是INFO和WARN日志呢?看下面)
<onMatch>: 用于配置符合过滤条件的操作
<onMismatch>:用于配置不符合过滤条件的操作
再看有这么几个单词DENY(拒绝),NEUTRAL(中性),ACCEPT(接受)
(1)返回DENY,日志将立即被抛弃不再经过其他过滤器;
(2)返回NEUTRAL,有序列表里的下个过滤器过接着处理日志;
(3)返回ACCEPT,日志会被立即处理,不再经过剩余过滤器
在理解下:
<level>ERROR</level> <!-- 所有ERROR级别的日志交给appender处理,非ERROR级别的日志,被过滤掉 --> <onMatch>DENY</onMatch> <!-- 代表符合过滤条件的拒绝输出 --> <onMismatch>ACCEPT</onMismatch> <!-- 不符合过滤条件的接受输出 -->
我靠,这么绕一圈不还是表示非ERROR级别的输出嘛,DEBUG除外,因为DUBUG在root标签,已经过滤掉,都不会走到appender标签这里
(2)ThresholdFilter 过滤器
临界值过滤器,过滤掉低于指定临界值的日志。当日志级别等于或高于临界值时,过滤器返回NEUTRAL;当日志级别低于临界值时,日志会被拒绝。
<filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level> </filter>
这里只会访问ERROR及以上级别的过滤器,而ERROR已经是最高级了,所以就只显示ERROR日志在errer文件中了,所以一切都通了
Springboot整合Active消息队列
简单理解:
Active是Apache公司旗下的一个消息总线,ActiveMQ是一个开源兼容Java Message Service(JMS) 面向消息的中件间. 是一个提供松耦合的应用程序架构.
主要用来在服务与服务之间进行异步通信的。
一、搭建步骤
1、相应jar包
<!-- 整合消息队列ActiveMQ --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-activemq</artifactId> </dependency> <!-- 如果配置线程池则加入 --> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-pool</artifactId> </dependency>
2、application.properties文件
#整合jms测试,安装在别的机器,防火墙和端口号记得开放 spring.activemq.broker-url=tcp://47.96.44.110:61616 spring.activemq.user=admin spring.activemq.password=admin #下列配置要增加依赖 spring.activemq.pool.enabled=true spring.activemq.pool.max-connections=100 #集群配置(后续需要在配上) #spring.activemq.broker-url=failover:(tcp://localhost:61616,tcp://localhost:61617) #消息队列默认是点对点的,如果需要发布/订阅模式那么需要加上下面注解(如果同时需要点对点发布订阅这里也需注释掉) # spring.jms.pub-sub-domain=true
3、Springboot主类
<!-- 主类需要多加一个@EnableJms注解,不过貌似我没有加的时候,也能运行,为安全起见姑且加上 --> @SpringBootApplication @EnableJms
4.5.......根据不同消息模式来写了。
二、点对点案例
我在这里案例中创建了两个点对点队列,所以他会有两个queue对象,同样对应每个queue对象,都会有单一对应的消费者。
1、Springboot主类
@SpringBootApplication @EnableJms public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); } //新建一个的Queue对象,交给sringboot管理,这个queue的名称叫"first.queue". @Bean public Queue queue(){ return new ActiveMQQueue("first.queue"); } }
2.1、first.queue对应消费者
@Component public class FirstConsumer { //名为"first.queue"消息队列的消费者,通过JmsListener进行监听有没有消息,有消息会立刻读取过来 @JmsListener(destination="first.queue") public void receiveQueue(String text){ System.out.println("FirstConsumer收到的报文为:"+text); } }
2.2、two.queue对应消费者(后面会创建)
@Component public class TwoConsumer { //名为"two.queue"消息队列的消费者 @JmsListener(destination="two.queue") public void receiveQueue(String text){ System.out.println("TwoConsumer收到的报文为:"+text); } }
3、Service类
/** * 功能描述:消息生产 */ public interface ProducerService { // 功能描述:指定消息队列,还有消息 public void sendMessage(Destination destination, final String message); // 功能描述:使用默认消息队列, 发送消息 public void sendMessage( final String message); }
4、ServiceImpl实现类
/** * 功能描述:消息生产者实现类 */ @Service public class ProducerServiceImpl implements ProducerService{ //这个队列就是Springboot主类中bean的对象 @Autowired private Queue queue; //用来发送消息到broker的对象,可以理解连接数据库的JDBC @Autowired private JmsMessagingTemplate jmsTemplate; //发送消息,destination是发送到的队列,message是待发送的消息 @Override public void sendMessage(Destination destination, String message) { jmsTemplate.convertAndSend(destination, message); } //发送消息,queue是发送到的队列,message是待发送的消息 @Override public void sendMessage(final String message) { jmsTemplate.convertAndSend(this.queue, message); } }
5.QueueController类
/** * 功能描述:点对点消息队列控制层 */ @RestController @RequestMapping("/api/v1") public class QueueController { @Autowired private ProducerService producerService; // 这里后面调用的是Springboot主类的quene队列 @GetMapping("first") public Object common(String msg){ producerService.sendMessage(msg); return "Success"; } // 这个队列是新建的一个名为two.queue的点对点消息队列 @GetMapping("two") public Object order(String msg){ Destination destination = new ActiveMQQueue("two.queue"); producerService.sendMessage(destination, msg); return "Success"; } }
6、案例演示:
从演示效果可以得出以下结论:
1:当springboot启动时候,就生成了这两个队列,而且他们都会有一个消费者
2:当我通过页面访问的时候,就相当于生产者把消息放到队列中,一旦放进去就会被消费者监听到,就可以获取生产者放进去的值并在后台打印出
顺便对页面中四个单词进行解释:
Number Of Pending Messages :待处理消息的数量。我们每次都会被监听处理掉,所以不存在待处理,如果存在就说这里面哪里出故障了,需要排查
Number Of Consumers : 消费者数量
Messages Enqueued: 消息排列,这个只增不见,代表已经处理多少消息
Messages Dequeued: 消息出队。
三、发布/订阅者模式
在上面点对点代码的基础上,添加发布/订阅相关代码
1.appliaction.properties文件
#消息队列默认是点对点的,如果需要发布/订阅模式那么需要加上下面注解(如果同时需要点对点发布订阅这里也需注释掉) spring.jms.pub-sub-domain=true
2.Springboot主类添加
//新建一个topic队列 @Bean public Topic topic(){ return new ActiveMQTopic("video.topic"); }
3.添加多个消费者类
//这里定义了三个消费者 @Component public class TopicSub { @JmsListener(destination="video.topic") public void receive1(String text){ System.out.println("video.topic 消费者:receive1="+text); } @JmsListener(destination="video.topic") public void receive2(String text){ System.out.println("video.topic 消费者:receive2="+text); } @JmsListener(destination="video.topic") public void receive3(String text){ System.out.println("video.topic 消费者:receive3="+text); } }
4.Service类
//功能描述:消息发布者 public void publish(String msg);
5.ServiceImpl实现类
//=======发布订阅相关代码========= @Autowired private Topic topic; @Override public void publish(String msg) { this.jmsTemplate.convertAndSend(this.topic, msg); }
6.Controller类
// 这个队列是新建的一个名为two.queue的点对点消息队列 @GetMapping("topic") public Object topic(String msg){ producerService.publish(msg); return "Success"; }
7.演示效果:
从演示效果总结如下:
1:Springboot启动的时候,在Topics目录下,一共出现了5个消费者。first.queue一个消费者、two.queue一个消费者、video.topic三个消费者
2:当我在控制台输入信息后,video.topic的三个消费者都会监听video.topic发布的消息,并在控制台打印。
四、如何让点对点和发布订阅同时有效
为什么这么说呢,因为当我向上面一样同时开启,会发现点对点模式已经失效了。
效果演示
从演示效果,可以得出如下结论:
1:我们发现我们在页面输入..../two?msg=555消息后,后台并没有成功打印消息。再看Active界面发现,这个queue对象,确实有一条待处理的消息,但是我们发现,它对应的消费者数量是为0.
2:然而我们在打开topic页面发现,这里却存在一个消费者。
所以我个人理解是,当同时启动的时候,所产生的消费者默认都是Topic消费者,没有Queue消费者,所以它监听不到queue所待处理的消息。
当配置文件不加:spring.jms.pub-sub-domain=true 那么系统会默认支持quene(点对点模式),但一旦加上这段配置,系统又变成只支持发布订阅模式。
那如何同时都可以成功呢?
思路如下:
第一步:还是需要去掉配置文件中的:
#消息队列默认是点对点的,如果需要发布/订阅模式那么需要加上下面注解(如果同时需要点对点发布订阅这里也需注释掉) #spring.jms.pub-sub-domain=true
第二步:在发布订阅者的中消费者中指定独立的containerFactory
因为你去掉上面的配置,那么系统就默认是queue,所以@JmsListener如果不指定独立的containerFactory的话是只能消费queue消息
@JmsListener(destination="video.topic", containerFactory="jmsListenerContainerTopic") public void receive1(String text){ System.out.println("video.topic 消费者:receive1="+text); } @JmsListener(destination="video.topic", containerFactory="jmsListenerContainerTopic") public void receive2(String text){ System.out.println("video.topic 消费者:receive2="+text); } //第三步我不添加containerFactory="jmsListenerContainerTopic"看等下是否会打印出 @JmsListener(destination="video.topic") public void receive3(String text){ System.out.println("video.topic 消费者:receive3="+text); }
第三步:定义独立的topic定义独立的JmsListenerContainer
在springboot主类中添加:
@Bean public JmsListenerContainerFactory<?> jmsListenerContainerTopic(ConnectionFactory activeMQConnectionFactory) { DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory(); bean.setPubSubDomain(true); bean.setConnectionFactory(activeMQConnectionFactory); return bean; }
效果:
得出结论:
1:点对点,和发布订阅都有用
2:receive3没有指定独立的containerFactory一样没有打印出来。
SpringBoot整合ElasticSearch
一、基于spring-boot-starter-data-elasticsearch整合
开发环境:springboot版本:2.0.1,elasticSearch-5.6.8.jar版本:5.6.8,服务器部署ElasticSearch版本:6.3.2
1、application.properties
spring.data.elasticsearch.cluster-name=elasticsearch spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300 spring.data.elasticsearch.repositories.enabled=true
2、pom.xml
<!--spring整合elasticsearch包--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <!--实体工具包--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--集合工具包--> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </dependency>
3、Notice实体
@Data @AllArgsConstructor @NoArgsConstructor //indexName代表所以名称,type代表表名称 @Document(indexName = "wantu_notice_info", type = "doc") public class Notice { //id @JsonProperty("auto_id") private Long id; //标题 @JsonProperty("title") private String title; //公告标签 @JsonProperty("exchange_mc") private String exchangeMc; //公告发布时间 @JsonProperty("create_time") private String originCreateTime; //公告阅读数量 @JsonProperty("read_count") private Integer readCount; }
4、NoticeRepository类
import com.jincou.elasearch.domain.Notice; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Component; @Component public interface NoticeRepository extends ElasticsearchRepository<Notice, Long> { }
5、NoticeController
@RestController @RequestMapping("/api/v1/article") public class NoticeController { @Autowired private NoticeRepository nticeRepository; @GetMapping("save") public CommandResult<Void> save(long id, String title){ Notice article = new Notice(); article.setId(id); article.setReadCount(123); article.setTitle("springboot整合elasticsearch,这个是新版本 2018年"); nticeRepository.save(article); return CommandResult.ofSucceed(); } /** * @param title 搜索标题 * @param pageable page = 第几页参数, value = 每页显示条数 */ @GetMapping("search") public CommandResult<List<Notice>> search(String title,@PageableDefault(page = 1, value = 10) Pageable pageable){
//按标题进行搜索 QueryBuilder queryBuilder = QueryBuilders.matchQuery("title", title);
//如果实体和数据的名称对应就会自动封装,pageable分页参数 Iterable<Notice> listIt = nticeRepository.search(queryBuilder,pageable);
//Iterable转list List<Notice> list= Lists.newArrayList(listIt); return CommandResult.ofSucceed(list); } }
6、查看运行结果
它会进行中文分词查询,然后安装相识度进行排序
总体步骤还是蛮清晰简单的,因为有spring-boot-starter-data-elasticsearch进行了整合,所以我们可以少敲很多代码。
二、 基于TransportClient整合
首先明白:如果项目SpringBoot1.5.X以下的,那么elasticSearch.jar最高是2.4.4版本的,只有SpringBoot2.X+,elasticSearch.jar才是5.X+
如果你的SpringBoot是1.5.X以下,那你又想用elasticSearch.jar5.X+怎么办呢,那就不要用spring-boot-starter-data-elasticsearch,用原生的TransportClient实现即可。
这个相当于用原生的去使用elasticsearch,这里面并没有用到spring-boot-starter-data-elasticsearch相关jar包,因为我们公司的springBoot版本是1.5.9。
如果用spring-boot-starter-data-elasticsearch的话,那么elasticsearch版本最高只有2.4.4,这也太落后了,现在elasticsearch已经到6.3.2了,为了用更好的版本有两个方案:
1、提高springboot版本到2.X(不过不现实,船大难掉头),2、用原生的TransportClient实现。最终落地实现是通过TransportClient实现
把关键代码展示出来。
1、pom.xml
<dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>5.6.8</version> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>transport</artifactId> <version>5.6.8</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.7</version> </dependency>
2、创建实体
@Configuration public class ServerModule { @Bean public TransportClient transportClient() { Settings settings = Settings.builder().put("cluster.name", "biteniuniu").build(); //我用6.3.2版本的时候这里一直报异常说找不到InetSocketTransportAddress类,这应该和jar有关,当我改成5.6.8就不报错了 TransportClient client = new PreBuiltTransportClient(settings);//6.3.2这里TransportAddress代替InetSocketTransportAddress client.addTransportAddress(new InetSocketTransportAddress( new InetSocketAddress(InetAddresses.forString("127.0.0.1"), 9300))); return client; } }
3、NoticeController类
@RestController @RequestMapping("/api/v1/notice") public class NoticeController { @Autowired private TransportClient transportClient; /** *利用TransportClient实现搜索功能 * @param title 搜索标题 * @param page = 从第几条结果返回 | Integer(比如一次size=20,page=0,如果要显示下一页20条记录则需要size=20,page=20)这个和之前有点区别, size = 每页显示条数 */ @RequestMapping(value = "trsearch", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public CommandResult<List<Notice>> search(@RequestParam(value = "title", defaultValue = "比特币")String title, @RequestParam(value = "page", defaultValue = "0")Integer page, @RequestParam(value = "size", defaultValue = "20")Integer size) { BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //按标题进行查找 boolQueryBuilder.must(QueryBuilders.matchQuery("title", title));
//在这里输入索引名称和type类型 SearchResponse response = transportClient.prepareSearch("wantu_notice_info").setTypes("doc") // 设置查询类型java .setSearchType(SearchType.DFS_QUERY_THEN_FETCH) // 设置查询关键词 .setQuery(boolQueryBuilder) // 设置查询数据的位置,分页用 .setFrom(page) // 设置查询结果集的最大条数 .setSize(size) // 设置是否按查询匹配度排序 .setExplain(true) // 最后就是返回搜索响应信息 .get(); SearchHits searchHits = response.getHits(); List<Notice> list = Lists.newArrayListWithCapacity(size); for (SearchHit searchHit : searchHits) { Map<String, Object> sourceAsMap = searchHit.getSourceAsMap(); //获得titie数据 String titles = (String) sourceAsMap.get("title"); //获得阅读量数据 Integer readCount = (Integer) sourceAsMap.get("read_count"); //把数据装入对象中 Notice notice=new Notice(); notice.setTitle(titles); notice.setReadCount(readCount); list.add(notice); } return CommandResult.ofSucceed(list); } }
4、运行结果
总结下:第一种整合相对简单很多,因为本身封装很多东西,比如分页,封装数据等。第二种的话可以在不用spring的情况下使用它。
Spingboot整合Swagger2
随着互联网技术的发展,一般开发都是前后端分离,那么前端和后端的唯一联系,变成了API接口;API文档变成了前后端开发人员联系的纽带,变得越来越重要,没有API
文档工具之前,大家都是手写API文档的,在什么地方书写的都有,有在confluence上写的,有在对应的项目目录下readme.md上写的,每个公司都有每个公司的玩法,无所谓好
坏。但都有一个很大的诟病就是,如果你的接口改动了,那你必须记得去改你的API文档,而Swagger并不需要。swagger就是一款让你更好的书写API文档的框架。
一、项目搭建
1、pom.xml
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>
2、application.yml
server: port: 8086 #代表当前环境是dev环境 spring: profiles: active: dev
3、Swagger2DevConfig(配置类)
注意添加@EnableSwagger2注解,这个注解也可以加在springboot启动类上
@Configuration @EnableSwagger2 public class Swagger2DevConfig { //默认pro环境 @Profile({"default", "pro"}) @Bean public Docket createWebApi() { return new Docket(DocumentationType.SWAGGER_2).enable(false).select().build(); } //dev环境接口访问地址:http://localhost:8086/swagger-ui.html @Profile("dev") @Bean public Docket createWebApiDev() { return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfoDev()).select() .apis(RequestHandlerSelectors.withClassAnnotation(Api.class)).paths(PathSelectors.any()).build(); } //测试环境接口访问地址: test.jincou.com/user/swagger-ui.html @Profile("test") @Bean public Docket createWebApiTest() { return new Docket(DocumentationType.SWAGGER_2).host("test.jincou.com/user") .protocols(Sets.newHashSet("https")).apiInfo(apiInfoDev()).select() .apis(RequestHandlerSelectors.withClassAnnotation(Api.class)).paths(PathSelectors.any()).build(); } private ApiInfo apiInfoDev() { return new ApiInfoBuilder().title("用户模块API").description( "用户api接口文档\n" + "\n" + "测试环境:https://test.jincou.com/user\n").contact(new Contact("张三", "", "")) .version("1.0").build(); } }
4、PersonController
这里提供了两个接口
@Api(tags = "用户相关接口") @RestController @RequestMapping(value = "/web/api/person") public class PersonController { @ApiOperation(value = "用户详细信息", notes = "通过id获得用户详细信息") @ApiImplicitParams({ @ApiImplicitParam(name = "id", value = "用户ID", required = true, paramType = "query", defaultValue = "0")}) @RequestMapping(value="page",method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE ) public Person getById(@RequestParam(value="id",defaultValue = "0") Long id){ return new Person(id,2,"小小","杭州"); } //大咖分页列表 @ApiOperation(value = "删除用户信息", notes = "通过ID删除用户信息") @ApiImplicitParams({ @ApiImplicitParam(name = "id", value = "用户ID", required = true, paramType = "query", defaultValue = "0")}) @RequestMapping(value="delete",method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE ) public String delete(@RequestParam(value="id",defaultValue = "0") Long id){ return "删除成功"; } }
5、Person实体类
@ApiModel(description = "用户详细信息") public class Person { @ApiModelProperty(value = "用户Id", position = 1) private Long id; @ApiModelProperty(value = "用户年龄", position = 2) private int age; @ApiModelProperty(value = "用户姓名", position = 3) private String name; @ApiModelProperty(value = "用户所在城市", position = 4) private String city; public Person(Long id,int age,String name,String city){ this.id=id; this.age=age; this.city=city; this.name=name; } //提供get和set方法 }
6、本地启动测试
访问:http://localhost:8086/swagger-ui.html。
先献上github
代码地址:https://github.com/yudiandemingzi/spring-boot-many-data-source
代码拉下来换下自己的mysql数据库地址,就可以直接运行。Druid的优点很明显,它的可视化界面可以监控Sql语句和URI执行情况在开发中真的很需要。
先说优点吧:
1) 替换DBCP和C3P0。Druid提供了一个高效、功能强大、可扩展性好的数据库连接池。
2) 数据库密码加密。直接把数据库密码写在配置文件中,这是不好的行为,容易导致安全问题。
3) 可以监控数据库访问性能,能够详细统计SQL的执行性能,这对于线上分析数据库访问性能有帮助。
4) SQL执行日志,Druid提供了不同的LogFilter,监控你应用的数据库 访问情况。
5)扩展JDBC,如果你要对JDBC层有编程的需求,可以通过Druid提供的Filter-Chain机制,很方便编写JDBC层的扩展插件。
二、配置多数据源
1、pom.xml
只需要添加druid这一个jar就行了,有关springboot项目他还有个整合包,用那个整合包也一样。
<!-- Druid 数据连接池依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.8</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.6</version>
</dependency>
对于springboot项目来讲,上面任选一个都是可以的,亲测有效。
2、application.yml
我这里是采用application.yml进行添加配置,这里面配置了两个数据源,其实在application.yml也可以不配置这些东西,它的主要作用是给数据源配置类读取数据用的。
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
initialSize: 10
minIdle: 10
maxActive: 200
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 30000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters
filters: stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
#配置了两个数据源
master:
datasource:
url: jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password: root
driverClassName: com.mysql.jdbc.Driver
cluster:
datasource:
url: jdbc:mysql://localhost:3306/student?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password: root
driverClassName: com.mysql.jdbc.Driver
3、主数据源配置类(MasterDataSourceConfig)
/**
* 主数据源配置
*/
@Configuration
@MapperScan(basePackages = MasterDataSourceConfig.PACKAGE, sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterDataSourceConfig {
/**
* 配置多数据源 关键就在这里 这里配置了不同的数据源扫描不同mapper
*/
static final String PACKAGE = "com.binron.multidatasource.mapper.master";
static final String MAPPER_LOCATION = "classpath:mapper/master/*.xml";
/**
* 连接数据库信息 这个其实更好的是用配置中心完成
*/
@Value("${master.datasource.url}")
private String url;
@Value("${master.datasource.username}")
private String username;
@Value("${master.datasource.password}")
private String password;
@Value("${master.datasource.driverClassName}")
private String driverClassName;
/**
* 下面的配置信息可以读取配置文件,其实可以直接写死 如果是多数据源的话 还是考虑读取配置文件
*/
@Value("${spring.datasource.initialSize}")
private int initialSize;
@Value("${spring.datasource.minIdle}")
private int minIdle;
@Value("${spring.datasource.maxActive}")
private int maxActive;
@Value("${spring.datasource.maxWait}")
private int maxWait;
@Value("${spring.datasource.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.minEvictableIdleTimeMillis}")
private int minEvictableIdleTimeMillis;
@Value("${spring.datasource.validationQuery}")
private String validationQuery;
@Value("${spring.datasource.testWhileIdle}")
private boolean testWhileIdle;
@Value("${spring.datasource.testOnBorrow}")
private boolean testOnBorrow;
@Value("${spring.datasource.testOnReturn}")
private boolean testOnReturn;
@Value("${spring.datasource.poolPreparedStatements}")
private boolean poolPreparedStatements;
@Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize}")
private int maxPoolPreparedStatementPerConnectionSize;
@Value("${spring.datasource.filters}")
private String filters;
@Value("{spring.datasource.connectionProperties}")
private String connectionProperties;
@Bean(name = "masterDataSource")
@Primary //标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean 优先被考虑。
public DataSource masterDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverClassName);
//具体配置
dataSource.setInitialSize(initialSize);
dataSource.setMinIdle(minIdle);
dataSource.setMaxActive(maxActive);
dataSource.setMaxWait(maxWait);
dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
dataSource.setValidationQuery(validationQuery);
dataSource.setTestWhileIdle(testWhileIdle);
dataSource.setTestOnBorrow(testOnBorrow);
dataSource.setTestOnReturn(testOnReturn);
dataSource.setPoolPreparedStatements(poolPreparedStatements);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
/**
* 这个是用来配置 druid 监控sql语句的 非常有用 如果你有两个数据源 这个配置哪个数据源就监控哪个数据源的sql 同时配置那就都监控
*/
try {
dataSource.setFilters(filters);
} catch (SQLException e) {
e.printStackTrace();
}
dataSource.setConnectionProperties(connectionProperties);
return dataSource;
}
@Bean(name = "masterTransactionManager")
@Primary
public DataSourceTransactionManager masterTransactionManager() {
return new DataSourceTransactionManager(masterDataSource());
}
@Bean(name = "masterSqlSessionFactory")
@Primary
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource masterDataSource)
throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(masterDataSource);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MasterDataSourceConfig.MAPPER_LOCATION));
return sessionFactory.getObject();
}
}
这里说明几点
1) @Primary: 多数据源配置的时候注意,必须要有一个主数据源, 用 @Primary 标志该 Bean。标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean优先被考虑。
2) dataSource.setFilters(filters): 这个是用来配置 druid 监控sql语句的, 如果你有两个数据源 这个配置哪个数据源就监控哪个 数据源的sql,同时配置那就都监控。
3) 能够做到多个数据源的关键点 就是每个数据源所扫描的mapper包不一样,谁扫描到哪个mapper那么该mapper就用哪个数据源,同时都扫到了呢,
那当然就得用主数据源咯,也就是添加@Primary 的数据源。
4、次数据源(ClusterDataSourceConfig)
这里省略了部分代码,因为和主是一样的,完整代码在github代码里有。
/**
* 次数据源 另一个数据源配置
*/
@Configuration
@MapperScan(basePackages = ClusterDataSourceConfig.PACKAGE, sqlSessionFactoryRef = "clusterSqlSessionFactory")
public class ClusterDataSourceConfig {
/**
* 配置多数据源 关键就在这里 这里配置了不同数据源扫描不同的mapper
*/
static final String PACKAGE = "com.binron.multidatasource.mapper.cluster";
static final String MAPPER_LOCATION = "classpath:mapper/cluster/*.xml";
@Value("${cluster.datasource.url}")
private String url;
@Value("${cluster.datasource.username}")
private String username;
@Value("${cluster.datasource.password}")
private String password;
@Value("${cluster.datasource.driverClassName}")
private String driverClass;
@Bean(name = "clusterDataSource")
public DataSource clusterDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverClass);
//具体配置
try {
dataSource.setFilters("stat,wall,slf4j");
} catch (SQLException e) {
e.printStackTrace();
}
return dataSource;
}
@Bean(name = "clusterTransactionManager")
public DataSourceTransactionManager clusterTransactionManager() {
return new DataSourceTransactionManager(clusterDataSource());
}
@Bean(name = "clusterSqlSessionFactory")
public SqlSessionFactory clusterSqlSessionFactory(@Qualifier("clusterDataSource") DataSource clusterDataSource)
throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(clusterDataSource);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(ClusterDataSourceConfig.MAPPER_LOCATION));
return sessionFactory.getObject();
}
}
这里说明几点:
1)发现次数据源所扫描的mapper和主是完全不一样的,说明每个数据源负责自己的mapper
2) 次数据源是没有加@Primary。
3)这里也添加了dataSource.setFilters(filters):说明 次数据源也需要监听sql语句。
三、配置可视化界面
直接上代码:
/**
* druid监控界面设置
*/
@Configuration
public class DruidConfiguration {
@Bean
public ServletRegistrationBean druidStatViewServle() {
//注册服务
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(
new StatViewServlet(), "/druid/*");
// 白名单(为空表示,所有的都可以访问,多个IP的时候用逗号隔开)
servletRegistrationBean.addInitParameter("allow", "127.0.0.1");
// IP黑名单 (存在共同时,deny优先于allow) (黑白名单就是如果是黑名单,那么该ip无法登陆该可视化界面)
servletRegistrationBean.addInitParameter("deny", "127.0.0.2");
// 设置登录的用户名和密码
servletRegistrationBean.addInitParameter("loginUsername", "root");
servletRegistrationBean.addInitParameter("loginPassword", "123456");
// 是否能够重置数据.
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean druidStatFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(
new WebStatFilter());
// 添加过滤规则
filterRegistrationBean.addUrlPatterns("/*");
// 添加不需要忽略的格式信息
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
System.out.println("druid初始化成功!");
return filterRegistrationBean;
}
}
这里说明几点
(1)登陆地址:http://127.0.0.1:8080/druid/index.html(端口号看自己设置的端口号)
(2)一旦配置黑名单,那么该ip访问是没有权限的登陆的
(3)如果想看SQL执行结果,那么上面数据源配置一定要添加dataSource.setFilters(filters),我之前就没有添加,所以其它都能正常使用,就是无法监控Sql语句。
效果图
确实很好用。顺便讲下,我遇到其它功能都有用,唯独Sql语句无法监控的问题,之后看界面才看出端倪来。
我发现界面中,filter类名为空,可是在yml确实配置了filters,怎么还是空,原因是数据源里没有配置setFilters(filters)
。
SpringBoot(14)—注解装配Bean
SpringBoot装配Bean方式主要有两种
- 通过Java配置文件
@Bean
的方式定义Bean。 - 通过注解扫描的方式
@Component/@ComponentScan
。
一、当前项目装配Bean
创建项目名称为create-bean
。
1、@Component方式
@Component("componentBean")
public class ComponentBean {
private String type = "@Component实例化bean";
public String getName(String name) {
return name + " ___ " + type;
}
}
说明
注解@Component 表明这个类将被Spring IoC容器扫描装配,bean的名称为componentBean。 如果不配置这个值 ,那IoC 容器就会把类名第一个字母作为小写,其他的不变作为 Bean 名称放入到 IoC 容器中。
2、@Bean方式
1)、POJO类
public class ConfigBean {
private String type = "Configuration注解生成bean实体";
public String getName(String name) {
return name + " ___ " + type;
}
}
2)BeanConfig类
/**
* @Description: 生成Bean
*/
@Configuration
public class BeanConfig {
@Bean
public ConfigBean configBean() {
return new ConfigBean();
}
}
@Configuration
代表是一个 Java 配置文件 , Spring会根据它来生成 IoC 容器去装配 Bean。
@Bean
代表将 configBean方法返回的 POJO 装配到 IoC 容器中, name为Bean 的名称,如果没有配置它,则会将方法名称作为 Bean 的名称保存到 Spring IoC 容器中 。
3、测试
/**
* @Description: 当前工程下的bean实体测试
*/
@RestController
public class BeanController {
/**
* 1、经典的注解引入方式 就是在@Configuration注解下生存bean
*/
@Autowired
private ConfigBean configBean;
/**
* 2、通过@Component方式注入bean 这里通过构造方法引入方式(也可和同上通过@Autowired注入)
*/
private ComponentBean componentBean;
public BeanController(ComponentBean componentBean) {
this.componentBean = componentBean;
}
@GetMapping(path = "/bean")
public String show(String name) {
Map<String, String> map = new HashMap(16);
map.put("ComponentBean", componentBean.getName(name));
map.put("ConfigBean", configBean.getName(name));
return JSON.toJSONString(map);
}
}
示例
bean实体成功
二、装配第三方 Bean
上面介绍的Bean,在一个项目中可能不会出现什么问题,可如果你提供了一个Jar包供第三方用户使用,那么你这个jar包中的Bean,就不能被第三方加载,那么如何才能被加载呢?
创建项目名称为third-bean
。
1、通过@Bean方式
POJO实体
/**
* @Description: 通过Configuration注解生成bean实体
*/
@Slf4j
public class ThirdConfigBean {
private String type = "第三方 ThirdConfigBean注解生成bean实体";
public String getName(String name) {
return name + " ___ " + type;
}
}
BeanConfig
@Configuration
//@ComponentScan("com.jincou.third") 当前项目包名
public class BeanConfig {
@Bean
public ThirdConfigBean thirdConfigBean() {
return new ThirdConfigBean();
}
}
这个时候在将配置放在指定的文件中即可,使用者会自动加载,从而避免的代码的侵入
- 在资源目录下新建目录 META-INF
- 在 META-INF 目录下新建文件
spring.factories
- 在文件中添加
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.jincou.third.config.BeanConfig
这个时候thirdConfigBean就可以能被第三方jar使用了。
2、@Component方式
@Component
public class ThirdComponentBean {
private String type = "第三方ThirdComponent注解生成bean实体";
public String getName(String name) {
return name + " ___ " + type;
}
}
说明
如果是@Component方式,那么第三方jar包依然无法实例化当前Bean的,除非,在上面的BeanConfig将@ComponentScan注解打开,同时满足@ComponentScan注解能够扫描到ThirdComponentBean实体,那么久可以实例化该Bean。
3、测试
说明
我创建了两个项目 create-bean
和third-bean
同时third-bean当作jar被create-bean引用,这个时候测上面的Bean能否被实例化。
在create-bean项目新建ThirdBeanController
/**
* @Description: 引入第三方的bean实体测试
*/
@RestController
public class ThirdBeanController {
/**
* 1、通过@Component方式注入bean
*/
@Autowired
private ThirdComponentBean thirdComponentBean;
/**
* 2、经典的注解引入方式 就是在@Configuration注解下生存bean
*/
@Autowired
private ThirdConfigBean thirdConfigBean;
@GetMapping(path = "/third-bean")
public String show(String name) {
Map<String, String> map = new HashMap(16);
map.put("ThirdComponentBean", thirdComponentBean.getName(name));
map.put("ThirdConfigBean", thirdConfigBean.getName(name));
return JSON.toJSONString(map);
}
}
测试如下
上面讲的可能并没有那么好理解,这边提供GitHub项目源码: https://github.com/yudiandemingzi/spring-boot-study,
涉及到两个项目 autobean
和thirdbean
SpringBoot(15)—@Conditional注解
作用
@Conditional是Spring4新提供的注解,它的作用是按照一定的条件进行判断,满足条件的才给容器注册Bean。
一、概述
1、@Conditional注解定义
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}
2、Condition
我们点进去看后,发现它是一个接口,有一个方法。
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}
3、ConditionContext
它持有不少有用的对象,可以用来获取很多系统相关的信息,来丰富条件判断,接口定义如下
public interface ConditionContext {
/**
* 获取Bean定义
*/
BeanDefinitionRegistry getRegistry();
/**
* 获取Bean工程,因此就可以获取容器中的所有bean
*/
@Nullable
ConfigurableListableBeanFactory getBeanFactory();
/**
* environment 持有所有的配置信息
*/
Environment getEnvironment();
/**
* 资源信息
*/
ResourceLoader getResourceLoader();
/**
* 类加载信息
*/
@Nullable
ClassLoader getClassLoader();
}
二、案例
需求
根据当前系统环境的的不同实例不同的Bean,比如现在是Mac
那就实例一个Bean,如果是Window
系统实例另一个Bean。
1、SystemBean
首先创建一个Bean类
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class SystemBean {
/**
* 系统名称
*/
private String systemName;
/**
* 系统code
*/
private String systemCode;
}
2、通过Configuration配置实例化Bean
@Slf4j
@Configuration
public class ConditionalConfig {
/**
* 如果WindowsCondition的实现方法返回true,则注入这个bean
*/
@Bean("windows")
@Conditional({WindowsCondition.class})
public SystemBean systemWi() {
log.info("ConditionalConfig方法注入 windows实体");
return new SystemBean("windows系统","002");
}
/**
* 如果LinuxCondition的实现方法返回true,则注入这个bean
*/
@Bean("mac")
@Conditional({MacCondition.class})
public SystemBean systemMac() {
log.info("ConditionalConfig方法注入 mac实体");
return new SystemBean("Mac ios系统","001");
}
}
3、WindowsCondition和MacCondition
这两个类都实现了Condition接口, 只有matches方法返回true才会实例化当前Bean
。
1)WindowsCondition
@Slf4j
public class WindowsCondition implements Condition {
/**
* @param conditionContext:判断条件能使用的上下文环境
* @param annotatedTypeMetadata:注解所在位置的注释信息
*/
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
//获取ioc使用的beanFactory
ConfigurableListableBeanFactory beanFactory = conditionContext.getBeanFactory();
//获取类加载器
ClassLoader classLoader = conditionContext.getClassLoader();
//获取当前环境信息
Environment environment = conditionContext.getEnvironment();
//获取bean定义的注册类
BeanDefinitionRegistry registry = conditionContext.getRegistry();
//获得当前系统名
String property = environment.getProperty("os.name");
//包含Windows则说明是windows系统,返回true
if (property.contains("Windows")){
log.info("当前操作系统是:Windows");
return true;
}
return false;
}
}
2) MacCondition
@Slf4j
public class MacCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
Environment environment = conditionContext.getEnvironment();
String property = environment.getProperty("os.name");
if (property.contains("Mac")) {
log.info("当前操作系统是:Mac OS X");
return true;
}
return false;
}
}
4、测试类测试
/**
* @author xub
* @date 2019/6/13 下午10:42
*/
@SpringBootTest(classes = Application.class)
@RunWith(SpringRunner.class)
public class TestConditionOn {
@Autowired
private SystemBean windows;
@Autowired
private SystemBean mac;
@Test
public void test() {
if (windows != null) {
System.out.println("windows = " + windows);
}
if (mac != null) {
System.out.println("linux = " + mac);
}
}
}
运行结果
通过运行结果可以看出
1、虽然配置两个Bean,但这里只实例化了一个Bean,因为我这边是Mac电脑,所以实例化的是mac的SystemBean
2、注意一点,我们可以看出 window
并不为null,而是mac实例化的Bean。说明 只要实例化一个Bean的,不管你命名什么,都可以注入这个Bean。
修改一下
这里做一个修改,我们把ConditionalConfig
中的这行代码注释掉。
// @Conditional({WindowsCondition.class})
再运行下代码
通过运行结果可以看出,配置类的两个Bean都已经注入成功了。
注意
当同一个对象被注入两次及以上的时候,那么你在使用当前对象的时候,名称一定要是两个bean名称的一个,否则报错。比如修改为
@Autowired
private SystemBean windows;
@Autowired
private SystemBean mac;
@Autowired
private SystemBean linux;
在启动发现,报错了。
意思很明显,就是上面只实例化成功一个SystemBean的时候,你取任何名字,反正就是把当前已经实例化的对象注入给你就好了。
但是你现在同时注入了两个SystemBean,你这个时候有个名称为linux,它不知道应该注入那个Bean,所以采用了报错的策略。
GitHub源码
https://github.com/yudiandemingzi/spring-boot-study
项目名称 03-conditional
@ConditionalOnBean与@ConditionalOnClass
上一篇讲的@Conditional
可以通过条件控制是否注入Bean,这篇讲下有关Bean其它几个常用的注解使用方式
@ConditionalOnBean // 当给定的在bean存在时,则实例化当前Bean
@ConditionalOnMissingBean // 当给定的在bean不存在时,则实例化当前Bean
@ConditionalOnClass // 当给定的类名在类路径上存在,则实例化当前Bean
@ConditionalOnMissingClass // 当给定的类名在类路径上不存在,则实例化当前Bean
下面我通过案例深入讲下@ConditionalOnBean
注解,这个理解其它也就理解了。
一、@ConditionalOnBean概念
需求场景
比如下面一种场景,我在实例化People对象的时候,需要注入一个City对象。这个时候问题来了,如果city没有实例化,那么下面就会报空指针或者直接报错。
所以这里需求很简单,就是当前city存在则实例化people,如果不存在则不实例化people,这个时候@ConditionalOnBean
的作用来了。
@Bean
public People people(City city) {
//这里如果city实体没有成功注入 这里就会报空指针
city.setCityName("千岛湖");
city.setCityCode(301701);
return new People("小小", 3, city);
}
1、@ConditionalOnBean注解定义
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {
/**
* 需要作为条件的类的Class对象数组
*/
Class<?>[] value() default {};
/**
* 需要作为条件的类的Name,Class.getName()
*/
String[] type() default {};
/**
* (用指定注解修饰的bean)条件所需的注解类
*/
Class<? extends Annotation>[] annotation() default {};
/**
* spring容器中bean的名字
*/
String[] name() default {};
/**
* 搜索容器层级,当前容器,父容器
*/
SearchStrategy search() default SearchStrategy.ALL;
/**
* 可能在其泛型参数中包含指定bean类型的其他类
*/
Class<?>[] parameterizedContainer() default {};
}
下面举例说明。
二、@ConditionalOnBean示例
1、Bean实体
1)City类
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class City {
/**
* 城市名称
*/
private String cityName;
/**
* 城市code
*/
private Integer cityCode;
}
2)People类
这里City作为People一个属性字段。
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class People {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 城市信息
*/
private City city;
}
2、Config类
这里写个正常的配置类,City成功注入到IOC容器中。
@Slf4j
@Configuration
public class Config {
@Bean
public City city() {
City city = new City();
city.setCityName("千岛湖");
return city;
}
@Bean
public People people(City city) {
//这里如果city实体没有成功注入 这里就会报空指针
city.setCityCode(301701);
return new People("小小", 3, city);
}
}
3、Test测试类
@SpringBootTest(classes = Application.class)
@RunWith(SpringRunner.class)
public class TestConditionOn {
@Autowired(required=false)
private People people;
@Test
public void test() {
System.out.println("= = = = = = = = = = = = = ");
System.out.println("people = " + people);
System.out.println("= = = = = = = = = = = = = ");
}
}
运行结果
一切正常,这个很符合我们实际开发中的需求。但是如果有一种情况,就是我的city并没有被注入。我把上面这部分注视掉。
// @Bean
// public City city() {
// City city = new City();
// city.setCityName("千岛湖");
// return city;
// }
再运行测试类
发现启动直接报错了,这当然不是我们希望看到的,我们是要当city已经注入那么实例化people,如果没有注入那么不实例化people。
@Slf4j
@Configuration
public class Config {
// @Bean
// public City city() {
// City city = new City();
// city.setCityName("千岛湖");
// return city;
// }
/**
* 这里加了ConditionalOnBean注解,就代表如果city存在才实例化people
*/
@Bean
@ConditionalOnBean(name = "city")
public People people(City city) {
//这里如果city实体没有成功注入 这里就会报空指针
city.setCityCode(301701);
return new People("小小", 3, city);
}
}
再运行测试类
很明显,上面因为city已经注释调,所以也导致无法实例化people,所以people为null。
注意
有点要注意的,就是一旦使用@Autowired
那就默认代表当前Bean一定是已经存在的,如果为null,会报错。所以这里要修改下。
@Autowired(required=false) //required=false 的意思就是允许当前的Bean对象为null。
总结
讲了这个注解,其它三个注解的意思大致差不多,在实际开发过程中可以根据实际情况使用该注解。
SpringBoot整合RocketMQ
上篇博客讲解了服务器集群部署RocketMQ 博客地址:RocketMQ(2)---Docker部署RocketMQ集群
这篇在上篇搭建好的基础上,将SpringBoot整合RocketMQ实现生产消费。
GitHub地址
: https://github.com/yudiandemingzi/spring-boot-study
一、搭建步骤
先说下技术大致架构
SpringBoot2.1.6 + Maven3.5.4 + rocketmq4.3.0 + JDK1.8 +Lombok(插件)
1、添加rocketmq包
<!--注意: 这里的版本,要和部署在服务器上的版本号一致-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.3.0</version>
</dependency>
2、JmsConfig(配置类)
连接RocketMQ服务器配置类,这里为了方便直接写成常量。
/**
* @Description: 安装实际开发这里的信息 都是应该写在配置里,来读取,这里为了方便所以写成常量
*/
public class JmsConfig {
/**
* Name Server 地址,因为是集群部署 所以有多个用 分号 隔开
*/
public static final String NAME_SERVER = "127.12.15.6:9876;127.12.15.6:9877";
/**
* 主题名称 主题一般是服务器设置好 而不能在代码里去新建topic( 如果没有创建好,生产者往该主题发送消息 会报找不到topic错误)
*/
public static final String TOPIC = "topic_family";
}
3、Producer (生产者)
@Slf4j
@Component
public class Producer {
private String producerGroup = "test_producer";
private DefaultMQProducer producer;
public Producer(){
//示例生产者
producer = new DefaultMQProducer(producerGroup);
//不开启vip通道 开通口端口会减2
producer.setVipChannelEnabled(false);
//绑定name server
producer.setNamesrvAddr(JmsConfig.NAME_SERVER);
start();
}
/**
* 对象在使用之前必须要调用一次,只能初始化一次
*/
public void start(){
try {
this.producer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}
public DefaultMQProducer getProducer(){
return this.producer;
}
/**
* 一般在应用上下文,使用上下文监听器,进行关闭
*/
public void shutdown(){
this.producer.shutdown();
}
}
4、Consumer (消费者)
@Slf4j
@Component
public class Consumer {
/**
* 消费者实体对象
*/
private DefaultMQPushConsumer consumer;
/**
* 消费者组
*/
public static final String CONSUMER_GROUP = "test_consumer";
/**
* 通过构造函数 实例化对象
*/
public Consumer() throws MQClientException {
consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
consumer.setNamesrvAddr(JmsConfig.NAME_SERVER);
//消费模式:一个新的订阅组第一次启动从队列的最后位置开始消费 后续再启动接着上次消费的进度开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
//订阅主题和 标签( * 代表所有标签)下信息
consumer.subscribe(JmsConfig.TOPIC, "*");
// //注册消费的监听 并在此监听中消费信息,并返回消费的状态信息
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
// msgs中只收集同一个topic,同一个tag,并且key相同的message
// 会把不同的消息分别放置到不同的队列中
try {
for (Message msg : msgs) {
//消费者获取消息 这里只输出 不做后面逻辑处理
String body = new String(msg.getBody(), "utf-8");
log.info("Consumer-获取消息-主题topic为={}, 消费消息为={}", msg.getTopic(), body);
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
System.out.println("消费者 启动成功=======");
}
}
大致就是这边简单,下面就是测试。
二、测试
先写个测试接口进行测试。
1、Controller
@Slf4j
@RestController
public class Controller {
@Autowired
private Producer producer;
private List<String> mesList;
/**
* 初始化消息
*/
public Controller() {
mesList = new ArrayList<>();
mesList.add("小小");
mesList.add("爸爸");
mesList.add("妈妈");
mesList.add("爷爷");
mesList.add("奶奶");
}
@RequestMapping("/text/rocketmq")
public Object callback() throws Exception {
//总共发送五次消息
for (String s : mesList) {
//创建生产信息
Message message = new Message(JmsConfig.TOPIC, "testtag", ("小小一家人的称谓:" + s).getBytes());
//发送
SendResult sendResult = producer.getProducer().send(message);
log.info("输出生产者信息={}",sendResult);
}
return "成功";
}
}
2、测试结果
很明显生产发送消息已经成功,二消费者也成功接收了消息!
另外我们再来看下RocketMQ控制台是否也有消费记录
很明显在控制台这边也会有消费记录!
通过Lua脚本批量插入数据到Redis布隆过滤器
在实际开发过程中经常会做的一步操作,就是判断当前的key是否存在。
那这篇博客主要分为三部分:
1、几种方式判断当前key是否存在的性能进行比较。
2、Redis实现布隆过滤器并批量插入数据,并判断当前key值是否存在。
3、针对以上做一个总结。
一、性能对比
主要对以下方法进行性能测试比较:
1、List的 contains 方法
2、Map的 containsKey 方法
3、Google布隆过滤器 mightContain 方法
前提准备
在SpringBoot项目启动的时候,向 List集合、Map集合、Google布隆过滤器 分布存储500万条
,长度为32位的String
字符串。
1、演示代码
@Slf4j
@RestController
public class PerformanceController {
/**
* 存储500万条数据
*/
public static final int SIZE = 5000000;
/**
* list集合存储数据
*/
public static List<String> list = Lists.newArrayListWithCapacity(SIZE);
/**
* map集合存储数据
*/
public static Map<String, Integer> map = Maps.newHashMapWithExpectedSize(SIZE);
/**
* guava 布隆过滤器
*/
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.unencodedCharsFunnel(), SIZE);
/**
* 用来校验的集合
*/
public static List<String> exist = Lists.newArrayList();
/**
* 计时工具类
*/
public static Stopwatch stopwatch = Stopwatch.createUnstarted();
/**
* 初始化数据
*/
@PostConstruct
public void insertData() {
for (int i = 0; i < SIZE; i++) {
String data = UUID.randomUUID().toString();
data = data.replace("-", "");
//1、存入list
list.add(data);
//2、存入map
map.put(data, 0);
//3、存入本地布隆过滤器
bloomFilter.put(data);
//校验数据 相当于从这500万条数据,存储5条到这个集合中
if (i % 1000000 == 0) {
exist.add(data);
}
}
}
/**
* 1、list 查看value是否存在 执行时间
*/
@RequestMapping("/list")
public void existsList() {
//计时开始
stopwatch.start();
for (String s : exist) {
if (list.contains(s)) {
log.info("list集合存在该数据=============数据{}", s);
}
}
//计时结束
stopwatch.stop();
log.info("list集合测试,判断该元素集合中是否存在用时:{}", stopwatch.elapsed(MILLISECONDS));
stopwatch.reset();
}
/**
* 2、查看map 判断k值是否存在 执行时间
*/
@RequestMapping("/map")
public void existsMap() {
//计时开始
stopwatch.start();
for (String s : exist) {
if (map.containsKey(s)) {
log.info("map集合存在该数据=============数据{}", s);
}
}
//计时结束
stopwatch.stop();
//获取时间差
log.info("map集合测试,判断该元素集合中是否存在用时:{}", stopwatch.elapsed(MILLISECONDS));
stopwatch.reset();
}
/**
* 3、查看guava布隆过滤器 判断value值是否存在 执行时间
*/
@RequestMapping("/bloom")
public void existsBloom() {
//计时开始
stopwatch.start();
for (String s : exist) {
if (bloomFilter.mightContain(s)) {
log.info("guava布隆过滤器存在该数据=============数据{}", s);
}
}
//计时结束
stopwatch.stop();
//获取时间差
log.info("bloom集合测试,判断该元素集合中是否存在用时:{}", stopwatch.elapsed(MILLISECONDS));
stopwatch.reset();
}
}
2、测试输出结果
测试结果
这里其实对每一个校验是否存在的方法都执行了5次,如果算单次的话那么,那么在500万条数据,且每条数据长度为32位的String类型情况下
,可以大概得出。
1、List的contains方法执行所需时间,大概80毫秒左右。
2、Map的containsKey方法执行所需时间,不超过1毫秒。
3、Google布隆过滤器 mightContain 方法,不超过1毫秒。
总结
Map比List效率高的原因这里就不用多说,没有想到的是它们速度都这么快。我还测了100万条数据通过list遍历key时间竟然也不超过1毫秒
。这说明在实际开发过程中,如果数据
量不大的话,用哪里其实都差不多。
3、占用内存分析
从上面的执行效率来看,Google布隆过滤器 其实没什么优势可言,确实如果数据量小,完全通过上面就可以解决,不需要考虑布隆过滤器,但如果数据量巨大,千万甚至亿级
别那种,用集合肯定不行,不是说执行效率不能接受,而是占内存不能接受。
我们来算下key值为32字节的500万条条数据,存放在List集合需要占多少内存。
500万 * 32 = 16000000字节 ≈ 152MB
一个集合就占这么大内存,这点显然无法接受的。
那我们来算算布隆过滤器所需要占内存
-
设bit数组大小为m,样本数量为n,失误率为p。
-
由题可知 n = 500万,p = 3%(Google布隆过滤器默认为3%,我们也可以修改)
通过公式求得:
m ≈ 16.7MB
是不是可以接收多了。
那么Google布隆过滤器也有很大缺点
1、每次项目启动都要重新将数据存入Google布隆过滤器,消费额外的资源。
2、分布式集群部署架构中,需要在每个集群节点都要存储一份相同数据到布隆过滤器中。
3、随着数据量的加大,布隆过滤器也会占比较大的JVM内存,显然也不够合理。
那么有个更好的解决办法,就是用redis作为分布式集群的布隆过滤器。
二、Redis布隆过滤器
1、Redis服务器搭建
如果你不是用docker,那么你需要先在服务器上部署redis,然后单独安装支持redis布隆过滤器的插件rebloom
。
如果你用过docker那么部署就非常简单了,只需以下命令:
docker pull redislabs/rebloom # 拉取镜像
docker run -p 6379:6379 redislabs/rebloom # 运行容器
这样就安装成功了。
2、Lua批量插入脚本
SpringBoot完整代码我这里就不粘贴出来了,文章最后我会把整个项目的github地址附上,这里就只讲下脚本的含义:
bloomFilter-inster.lua
local values = KEYS
local bloomName = ARGV[1]
local result_1
for k,v in ipairs(values) do
result_1 = redis.call('BF.ADD',bloomName,v)
end
return result_1
1)参数说明
这里的 KEYS
和 ARGV[1]
都是需要我们在java代码中传入,redisTemplate有个方法
execute(RedisScript<T> script, List<K> keys, Object... args)
- script实体中中封装批量插入的lua脚本。
- keys 对于脚本的 KEYS。
- ARGV[1]对于可变参数第一个,如果输入多个可变参数,可以可以通过ARGV[2].....去获取。
2)遍历
Lua遍历脚本有两种方式一个是ipairs
,另一个是pairs
它们还是有差别的。这里也不做展开,下面有篇博客可以参考。
注意
Lua的遍历和java中遍历还有有点区别的,我们java中是从0开始,而对于Lua脚本 k是从1开始的。
3)插入命令
BF.ADD
是往布隆过滤器中插入数据的命令,插入成功返回 true。
3、判断布隆过滤器元素是否存在Lua脚本
bloomFilter-exist.lua
local bloomName = KEYS[1]
local value = KEYS[2]
-- bloomFilter
local result_1 = redis.call('BF.EXISTS', bloomName, value)
return result_1
从这里我们可以很明显看到, KEYS[1]对于的是keys集合的get(0)位置,所以说Lua遍历是从1开始的。
BF.EXISTS
是判断布隆过滤器中是否存在该数据命令,存在返回true。
4、测试
我们来测下是否成功。
@Slf4j
@RestController
public class RedisBloomFilterController {
@Autowired
private RedisService redisService;
public static final String FILTER_NAME = "isMember";
/**
* 保存 数据到redis布隆过滤器
*/
@RequestMapping("/save-redis-bloom")
public Object saveReidsBloom() {
//数据插入布隆过滤器
List<String> exist = Lists.newArrayList("11111", "22222");
Object object = redisService.addsLuaBloomFilter(FILTER_NAME, exist);
log.info("保存是否成功====object:{}",object);
return object;
}
/**
* 查询 当前数据redis布隆过滤器是否存在
*/
@RequestMapping("/exists-redis-bloom")
public void existsReidsBloom() {
//不存在输出
if (!redisService.existsLuabloomFilter(FILTER_NAME, "00000")) {
log.info("redis布隆过滤器不存在该数据=============数据{}", "00000");
}
//存在输出
if (redisService.existsLuabloomFilter(FILTER_NAME, "11111")) {
log.info("redis布隆过滤器存在该数据=============数据{}", "11111");
}
}
}
这里先调插入接口,插入两条数据,如果返回true则说明成功,如果是同一个数据第一次插入返回成功,第二次插入就会返回false,说明重复插入相同值会失败。
然后调查询接口,这里应该两条日志都会输出,因为上面"00000"是取反的,多了个!号。
我们来看最终结果。
符合我们的预期,说明,redis布隆过滤器从部署到整合SpringBoot都是成功的。
三、总结
下面个人对整个做一个总结吧。主要是思考下,在什么环境下可以考虑用以上哪种方式来判断该元素是否存在。
1、数据量不大,且不能有误差。
那么用List或者Map都可以,虽然说List判断该元素是否存在采用的是遍历集合的方式,在性能在会比Map差,但就像上面测试一样,100万的数据,
List遍历和Map都不超过1毫秒,选谁不都一样,何必在乎那0.几毫秒的差异。
2、数据量不大,且允许有误差。
这就可以考虑用Google布隆过滤器
了,尽管查询数据效率都差不多,但关键是它可以减少内存的开销,这就很关键。
3、数据量大,且不能有误差。
如果说数量大,为了提升查询元素是否存在的效率,而选用Map的话,我觉得也不对,因为如果数据量大,所占内存也会更大,所以我更推荐用
Redis的map数据结构来存储数据,这样可以大大减少JVM内存开销,而且不需要每次重启都要往集合中存储数据。
4、数据量大,且允许有误差。
如果是单体应用,数据量内存也可以接收,那么可以考虑Google布隆过滤器,因为它的查询速度会比redis要快。毕竟它不需要网络IO开销。
如果是分布式集群架构,或者数据量非常大,那么还是考虑用redis布隆过滤器吧,毕竟它不需要往每一节点都存储数据,而且不占用JVM虚拟机内存。
Github地址
:https://github.com/yudiandemingzi/spring-boot-redis-lua
SpringBoot整合Apollo
有关Apollo之前已经写了两篇文章:
这篇文章分为两部分:
1、跟着官网步骤,快速搭建apollo环境。
2、SpringBoot整合apollo,实现配置中心。
一、Apollo快速搭建
apollo环境的搭建主要参考 官方文档 ,我们就直接一步一步跟着官方文档来
1、下载Quick Start安装包
2、创建数据库
之前有说过,apollo会有两个数据库: ApolloPortalDB
和 ApolloConfigDB
3、更新数据库连接信息
在下载下来的Quick Start项目中,有个叫demo.sh
的文件,我们需要 修改数据库连接信息,修改里头数据库连接地址/用户名/密码
4、 启动Apollo配置中心
上面三部就已经将环境搭建好,现在我们开始启动Apollo配置中心,来看具体效果。
注意
这里默认暂用三个端口: 8070
,8080
,8090
,所以要先看下这三个端口有木有被暂用。
启动项目
./demo.sh start
5、登录Apollo
登录界面
![](https://img2020.cnblogs.com/blog/1090617/202008/1090617-20200806212210988-465753948.jpg)
初始账号密码
: apollo/admin
进入首页之后,我们可以看到: 默认的环境只有dev,m默认有一个SampleApp项目
![](https://img2020.cnblogs.com/blog/1090617/202008/1090617-20200806212448642-1729276800.jpg)
我们在这里添加两个配置
6. 测试客户代码
上面已经设置了两个配置,这个时候我们就需要通过客户端去获取,我们先不通过SpringBoot去获取,而是官方提供的客户端去获取配置
运行客户端
./demo.sh client
启动后去查询配置
![](https://img2020.cnblogs.com/blog/1090617/202008/1090617-20200806212544234-1737767734.jpg)
可以看到客户端都能够拿到配置中心的配置。
注意
这里有一点在上面我设了 gougou:4岁。而实际客户端获得的是4?,所以中文没有转义成功,在实际开发中,我们也尽量去避免有中文汉字来当配置。
这样下来,官方给的快速启动apollo是成功了,下面我们把客户端换成SpringBoot,来实现获取配置中心的数据。
二、SpringBoot整合Apollo
从上面我们可以知道在apollo官方给了我们一个默认项目叫:SampleApp。默认的用户名称:apollo。所以这里灵活一点,不用它自己的,而是我们手动新增。
1、Apollo创建新新用户和项目
1)新增新用户
http://{portal地址}/user-manage.html //我们这里设置的是http://localhost:8070/user-manage.html
![](https://img2020.cnblogs.com/blog/1090617/202008/1090617-20200806212631433-995089617.jpg)
这里新增一个用户名叫:liubei
2)新增项目
访问http://localhost:8070 登录后,选择创建项目。
![](https://img2020.cnblogs.com/blog/1090617/202008/1090617-20200806212714665-1375105632.jpg)
这里新创建了项目,一个AppId名称叫: springboot-test-apollo
。
配置说明
:
部门:选择应用所在的部门。(想自定义部门,参照官方文档,这里就选择样例)
应用AppId:用来标识应用身份的唯一id,格式为string,需要和客户端。application.properties中配置的app.id对应。
应用名称:应用名,仅用于界面展示。
应用负责人:选择的人默认会成为该项目的管理员,具备项目权限管理、集群创建、Namespace创建等权限。
提交配置后会出现如下项目配置的管理页面。
3)添加配置
![](https://img2020.cnblogs.com/blog/1090617/202008/1090617-20200806212752731-941570644.jpg)
这里先添加一个新配置: date.value = 2020.08.04 。
2、springboot项目搭建
上面把apollo环境搭建好,也创建了新项目,那么这里就去读取新项目的配置信息。
1)pom.xml
添加 Apollo 客户端的依赖,其它依赖这里就不展示了,完整项目会放在github上。
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>1.3.0</version>
</dependency>
2)application.yml
server:
port: 8123
app:
id: springboot-test-apollo
apollo:
meta: http://127.0.0.1:8080
bootstrap:
enabled: true
eagerLoad:
enabled: true
#这里说明在将该项目 com目录下的日志,都采用info模式输出
logging:
level:
com: info
配置说明
app.id :AppId是应用的身份信息,是配置中心获取配置的一个重要信息。
apollo.bootstrap.enabled:在应用启动阶段,向Spring容器注入被托管的application.properties文件的配置信息。
apollo.bootstrap.eagerLoad.enabled:将Apollo配置加载提到初始化日志系统之前。
logging.level.com :调整 com 包的 log 级别,为了后面演示在配置中心动态配置日志级别。
3) TestController
@Slf4j
@RestController
public class TestController {
@Value( "${date.value}" )
String dateValue;
@GetMapping("test")
public String test() {
return "打印配置中心的 dateValue 值: "+ dateValue;
}
}
4)SpringBoot启动类
@SpringBootApplication
@EnableApolloConfig
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
添加 EnableApolloConfig 配置注解
3、测试
我在apollo配置了一个属性: date.value = 2020.08.04,如果从springBoot能够获取到该配置,那么就说明成功了。
![](https://img2020.cnblogs.com/blog/1090617/202008/1090617-20200806212842556-596536111.jpg)
很明显,我们通过接口已经成功获取配置中心的配置。这样一个最简单的整合已经完成了,下面我们在来验证其它东西。
三、验证
下面我们来验证一些有意思的东西:
1)、配置修改后能否立即生效。
2)、通过配置日志打印级别,我们来验证能够动态的修改日志级别
3)、能够通过修改端口号,能够修改项目运行的端口号。
下面我们一个一个来验证
1、配置修改后能否立即生效?
新增一个接口
@Slf4j
@RestController
public class TestController {
@Value( "${date.value}" )
String dateValue;
@GetMapping("test1")
public void test1() {
log.info("当前配置中心的 dateValue 值 = {}",dateValue);
}
}
步骤
:1)先请求接口一次。2)然后修改apollo配置中心。3)然后在请求一次该接口。
![](https://img2020.cnblogs.com/blog/1090617/202008/1090617-20200806212953674-2083008839.jpg)
结论
从图中很明显看出第一次和第二次请求的接口,返回的配置信息已经不一样了。说明配置是会时时更新的。而且当我在apollo界面修改配置的时候,
控制台日志也会输出相关信息。
2、动态修改日志级别
我们来思考有没有这么一种需求,我们在线上的日志级别一般是INFO级别的,主要记录业务操作或者错误的日志。那么这个时候当线上环境出现问题希望输出DEBUG
日志信息辅助排查的时候怎么办呢?
如果以前,我们可以会修改配置文件,重新打包然后上传重启线上环境,以前确实是这么做的。通过Apollo配置中心我们可以实现不用重启项目,就可以实现让日志
基本从INFO变成DEBUG。下面我们来演示一番。
在项目启动之前,我在application.yml
配置了日志级别是info模式。我们来请求下面接口验证下。
@GetMapping("test2")
public void test2() {
log.debug("我是 debug 打印出的日志");
log.info("我是 info 打印出的日志");
log.error("我是 error 打印出的日志");
}
请求下接口看下控制台
![](https://img2020.cnblogs.com/blog/1090617/202008/1090617-20200806213045076-333419926.jpg)
可以很明显的看出,控制台只打印info级别以上的日志,并没有打印debug日志。
现在我们要做的是将打印日志级别不在交给application.yml
而是交给apollo
来配置。这里其实要做两步:
1)、在apollo配置一条日志数据。
2)、通过监听配置的变化,来达到热更新的效果。
java配置类
@Configuration
public class LoggerConfig {
private static final Logger logger = LoggerFactory.getLogger(LoggerConfig.class);
private static final String LOGGER_TAG = "logging.level.";
@Autowired
private LoggingSystem loggingSystem;
@ApolloConfig
private Config config;
@ApolloConfigChangeListener
private void configChangeListter(ConfigChangeEvent changeEvent) {
refreshLoggingLevels();
}
@PostConstruct
private void refreshLoggingLevels() {
Set<String> keyNames = config.getPropertyNames();
for (String key : keyNames) {
if (StringUtils.containsIgnoreCase(key, LOGGER_TAG)) {
String strLevel = config.getProperty(key, "info");
LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level);
logger.info("{}:{}", key, strLevel);
}
}
}
}
关键点讲解
@ApolloConfig注解:将Apollo服务端的中的配置注入这个类中。
@ApolloConfigChangeListener注解:监听配置中心配置的更新事件,若该事件发生,则调用refreshLoggingLevels方法,处理该事件。
ConfigChangeEvent参数:可以获取被修改配置项的key集合,以及被修改配置项的新值、旧值和修改类型等信息。
从上面可以看出,通过@PostConstruct
项目启动的时候就去获取apollo的日志级别去覆盖application.yml
日志级别。我在apollo配置中心,新增加一条日志配置。
把日志级别设置error,那么在项目启动的时候,因为PostConstruct注解的原因,所以会去执行一次refreshLoggingLevels方法,把当前日志级别改成error。
apollo配置
![](https://img2020.cnblogs.com/blog/1090617/202008/1090617-20200806213146988-383237169.jpg)
我们重启启动项目再来看下,请求上面接口结果。
![](https://img2020.cnblogs.com/blog/1090617/202008/1090617-20200806213238281-1222573864.jpg)
从上图我们就可以看出,我们同时在application.yml
和apollo
都配置了日志打印级别,但实际上打印出来的结果来看,最终是以apollo为准。
现在我们再来尝试下,我们修改apollo配置,把日志级别修改成debug级别。
apollo配置
![](https://img2020.cnblogs.com/blog/1090617/202008/1090617-20200806213311509-605430673.jpg)
因为我们在LoggerConfig中,有个 @ApolloConfigChangeListener
注解,我们在修改配置的时候都会被监听到,监听到之后就会把当前的日志级别,换成最新的。
我们再来请求接口,看下控制台。
![](https://img2020.cnblogs.com/blog/1090617/202008/1090617-20200806213343302-300223462.jpg)
总结
通过上面的验证,可以得出:我在没有重启服务器的情况下,实现了日志级别动态切换的功能。
3、动态修改端口号
这里我就不把验证的过程发出来了,结论就是不可以。如果要验证,在上面的refreshLoggingLevels方法中,加入修改端口的逻辑。
其实理由很简单,因为端口跟进程绑定在一起的,你的进程起来了,端口就无法改变。如果一定要换端口号,那么就需要有一个独立进程,去杀掉你的项目进程,
再起一个新的不同端口的进程。显然apollo没有这样的功能,而且个人觉得也没啥意义。。
好了,整篇文章到这里就结束了,下面把该项目的具体代码放到github上。
SpringBoot---断言(Assert)
我们在写单元测试的时候,除了接口直接抛异常而导致该单元测试失败外,还有种是业务上的错误也代表着该单元测试失败。好比我们在测试接口的时候,
该接口返回是1代表成功,如果是0那就代表是失败的,这个时候可以考虑使用断言。
一、原理
我们知道,我们可以通过断言来校验测试用例的返回值和实际期望值进行比较,以此来判断测试是否通过。那我们先来看下如果失败的情况下它的流程是怎么样的。
对于断言而言,如果是错误最终都会进入下面的方法
static public void fail(String message) {
if (message == null) {
throw new AssertionError();
}
throw new AssertionError(message);
}
很明显,这里有一个AssertionError
对象,我们来看下这个对象。
public class AssertionError extends Error {
//......
}
我们很明显看到,它实继承是Error
,而不是Exception。这说明什么,说明你无法通过try catch去捕获这个异常,只要进入断言fail中,一定会抛出异常。
二、Assert 源码
这里只展示Assert源码中常用的方法,一些不常用或者过期的方法这里就不展示了。
public class Assert {
/**
* 结果 = 预期 则正确
*/
static public void assertEquals(Object expected, Object actual)
/**
* 结果 != 预期 则正确
*/
static public void assertNotEquals(Object unexpected, Object actual)
/**
* condition == true 则正确
*/
static public void assertTrue(boolean condition)
/**
* condition == false 则正确
*/
static public void assertFalse(boolean condition)
/**
* 永远是错误
*/
static public void fail()
/**
* 结果不为空 则正确
*/
static public void assertNotNull(Object object)
/**
* 结果为空 则正确
*/
static public void assertNull(Object object)
/**
* 两个对象引用 相等 则正确(上面相当于equels 这里类似于使用“==”比较两个对象)
*/
static public void assertSame(Object expected, Object actual)
/**
* 两个对象引用 不相等 则正确
*/
static public void assertNotSame(Object unexpected, Object actual)
/**
* 两个数组相等 则正确
*/
public static void assertArrayEquals(Object[] expecteds, Object[] actuals)
/**
* 这个单独介绍
*/
public static <T> void assertThat(T actual, Matcher<? super T> matcher)
}
注意
上面每一个方法,都会有一个多一个参数的方法,这个参数为:String message。意思就是错误的情况下,我们可以输出我们自定义的message
示例
//这个就表示 结果 != 预期 的情况下,抛出的AssertionError 信息是我们指定的message
static public void assertEquals(String message,Object expected, Object actual)
上面还有一个方法需要单独介绍,那就是assertThat方法。
三、assertThat方法
大家习惯把assertThat理解成新断言
,因为上面所以的功能方法,都可以通过assertThat这一个方法来实现。
1、基本语法
assertThat 的基本语法如下:
assertThat( [value], [matcher statement] );
value 是接口返回信息中,我们想要测试的变量值
matcher statement: 是使用Hamcrest
匹配符来表达的对前面变量所期望的值的声明,如果value值与matcher statement所表达的期望值相符,则测试成功,否则测试失败。
2、基本使用
字符串匹配符
String n = "xiao";
// containsString:字符串变量中包含指定字符串时,测试通过
assertThat(n, containsString("xiao"));
// startsWith:字符串变量以指定字符串开头时,测试通过
assertThat(n, startsWith("xi"));
// endsWith:字符串变量以指定字符串结尾时,测试通过
assertThat(n, endsWith("ao"));
// euqalTo:字符串变量等于指定字符串时,测试通过
assertThat(n, equalTo("xiao"));
// equalToIgnoringCase:字符串变量在忽略大小写的情况下等于指定字符串时,测试通过
assertThat(n, equalToIgnoringCase("xiao"));
// equalToIgnoringWhiteSpace:字符串变量在忽略头尾任意空格的情况下等于指定字符串时,测试通过
assertThat(n, equalToIgnoringWhiteSpace(" xiao "));
int匹配符
int s = 1;
// allOf:所有条件必须都成立,测试才通过(大于1同时小于3)
assertThat(s, allOf(greaterThan(1), lessThan(3)));
// anyOf:只要有一个条件成立,测试就通过 (大于1或者小于2)
assertThat(s, anyOf(greaterThan(1), lessThan(2)));
// anything:无论什么条件,测试都通过
assertThat(s, anything());
// is:变量的值等于指定值时,测试通过
assertThat(s, is(2));
// not:和is相反,变量的值不等于指定值时,测试通过
assertThat(s, not(1));
double匹配符
double d = 1D;
// closeTo:浮点型变量的值在3.0±0.5范围内,测试通过
assertThat(d, closeTo(3.0, 0.5));
// greaterThan:变量的值大于指定值时,测试通过
assertThat(d, greaterThan(3.0));
// lessThan:变量的值小于指定值时,测试通过
assertThat(d, lessThan(3.5));
// greaterThanOrEuqalTo:变量的值大于等于指定值时,测试通过
assertThat(d, greaterThanOrEqualTo(3.3));
// lessThanOrEqualTo:变量的值小于等于指定值时,测试通过
assertThat(d, lessThanOrEqualTo(3.4));
集合匹配符
List<String> list = new ArrayList();
// hasItem:Iterable变量中含有指定元素时,测试通过
assertThat(list, hasItem("xiao"));
Map<String, String> m = new HashMap<>();
// hasEntry:Map变量中含有指定键值对时,测试通过
assertThat(m, hasEntry("xi", "xiao"));
// hasKey:Map变量中含有指定key时,测试通过
assertThat(m, hasKey("x"));
// hasValue:Map变量中含有指定value值时,测试通过
assertThat(m, hasValue("x"));
四、测试
这里对于老断言,和新断言各测试5个。
1、老断言
示例
public class TestServiceImplTest {
@Test
public void test1() {
String str = "xiao";
assertEquals(str, "xiaoniao");//不相等,所以错误
}
@Test
public void test2() {
assertFalse(Boolean.TRUE); //不是false,所以错误
}
@Test
public void test3() {
fail("直接是错误"); //直接是错误
}
@Test
public void test4() {
assertNull("xiao"); //不为空所以为错误
}
@Test
public void test5() {
assertNotNull("xiao");//不为空,所以为正确
}
}
这里应该只有第5个测试用例通过,前面4个都不通过的,我们在来看实际运行结果
![](https://img2020.cnblogs.com/blog/1090617/202009/1090617-20200917123651101-973430376.jpg)
与实际相符,前面4个,测试用例不通过。而且可以看到第3个是我们自定义错误信息,在控制台也打印出来了。
2、新断言
示例
import org.assertj.core.util.Lists;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
public class TestServiceImplTest {
@Test
public void test1() {
String str = "xiao";
assertThat("这两字符串不相等",str, is("xiaoniao"));//不相等,所以错误
}
@Test
public void test2() {
int s = 1;
assertThat(s, anyOf(greaterThan(1), lessThan(2)));//满足 大于1或者小于2 所以正确
}
@Test
public void test3() {
double d = 1D;
assertThat(d, allOf(greaterThanOrEqualTo(1D),lessThan(2D)));//满足大于等于1 并且 小于2 所以正确
}
@Test
public void test4() {
List<String> list = Lists.newArrayList("xiao","zhong","da");
assertThat(list, hasItem("xiao")); //包含xiao 所以正确
}
@Test
public void test5() {
Map<String, String> map = new HashMap<>();
map.put("xiao", "xiao");
assertThat(map, hasKey("xiao")); //该map包含该key,所以正确
}
}
这里应该只有第1个测试用例不通过,其它都是通过的,我们在来看实际运行结果
![](https://img2020.cnblogs.com/blog/1090617/202009/1090617-20200917123706030-2091855547.jpg)
符合预期。