这篇文章主要是介绍功能点,先看看这个工具包有什么可以用的,目前主要有两个模块——布隆过滤器、基于注解限流。基于redisTemplate
这里用maven作为工具管理包演示,添加jitpack源、添加下面的依赖
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.97lele</groupId>
<artifactId>redis-aux</artifactId>
<version>0.3.8</version>
</dependency>
在启动类上添加启用布隆过滤器的注解
@EnableBloomFilter(bloomFilterPath = "com.example.demo")
2个属性,分别为
1.需要支持lambda表达式添加的实体路径
2.是否开启支持@Trancational注解,需要和数据库事务配合使用
配置好redis
spring:
redis:
port: 6379
host: 127.0.0.1
只有两种,一种是通过构建操作对象来添加,一种是通过解析lambda表达,获取其字段上的注解信息来添加
若要调用SFunction为参数的方法需要在EnableBloomFilter配置扫描路径
主要是exceptedInsertions,fpp,timeOut,local 这里四个参数,分别为预计插入的个数,允许的错误率,过期时间,是否为本地
演示,添加主要分为普通添加
@Test
public void simpleTest() {
boolean isLocal=true;
String key = "testAdd";
//默认local为false
AddCondition addCondition = AddCondition.create().keyName(key).local(isLocal);
BaseCondition baseCondition = addCondition.toBaseCondition();
bloomFilter.add(addCondition, "hello");
System.out.println("contain he:"+bloomFilter.mightContain(baseCondition,"he"));
System.out.println("contain hello:"+bloomFilter.mightContain(baseCondition,"hello"));
//多值操作
bloomFilter.addAll(addCondition,Arrays.asList("h","a","c"));
System.out.println("before reset:"+bloomFilter.mightContains(baseCondition,Arrays.asList("a","b","c")));
//重置
bloomFilter.reset(baseCondition);
System.out.println("after reset:"+bloomFilter.mightContains(baseCondition,Arrays.asList("a","hello","qq")));
System.out.println("before delete:"+bloomFilter.containKey(baseCondition));
//删除
bloomFilter.remove(baseCondition);
System.out.println("after delete:"+bloomFilter.containKey(baseCondition));
}
结果
lambda演示
需要实体类实现getter,并且添加上前缀名,否则默认为类名,需要操作的属性上面添加BloomFilterProperty注解,该注解可填充属性有以下,key如果不填按字段名处理,另外要在enableBloomFilter的注解里填写扫描路径
double fpp() default 0.03;
long exceptionInsert() default 1000;
String key() default "";
long timeout() default -1L;
TimeUnit timeUnit() default TimeUnit.SECONDS;
boolean local() default false;
@BloomFilterPrefix
public class TestEntity {
@BloomFilterProperty(enableGrow = true,exceptionInsert = 5,timeout = 30)
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
基于lambda的测试代码
@Test
public void lambdaTest() throws InterruptedException {
bloomFilter.addAll(TestEntity::getName, Arrays.asList(13, 14, 15, 16));
System.out.println(bloomFilter.mightContain(TestEntity::getName, 15));
System.out.println(bloomFilter.mightContains(TestEntity::getName, Arrays.asList(13, 200)));
}
结果
键过期测试
@Test
void timeOutTest() {
boolean isLocal=true;
bloomFilter.add(AddCondition.create().keyName("a1").timeout(30L).timeUnit(TimeUnit.SECONDS).local(isLocal), 1);
bloomFilter.addAll(AddCondition.create().keyName("a4").timeUnit(TimeUnit.SECONDS).timeout(10L).local(isLocal), Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
bloomFilter.add(AddCondition.create().keyName("a2").timeout(11L).timeUnit(TimeUnit.SECONDS).local(isLocal), 1);
bloomFilter.add(AddCondition.create().keyName("a3").timeout(22L).timeUnit(TimeUnit.SECONDS).local(isLocal), 1);
System.out.println(bloomFilter.mightContain(BaseCondition.create().keyName("a1"), 1));
try {
TimeUnit.SECONDS.sleep(35L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(bloomFilter.containKey(BaseCondition.create().keyName("a1")));
}
结果
开启支持事务
清空redis的键
service代码,一个有错,一个无错
package com.example.demo.service;
import com.example.demo.dao.UserTicketMapper;
import com.example.demo.entity.UserTicket;
import com.trendy.util.redis.aux.bloomfilter.autoconfigure.RedisBloomFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
/**
* @author: lele
* @date: 2020/1/4 下午11:51
*/
@Service
public class TestService {
@Autowired
private UserTicketMapper userTicketMapper;
@Autowired
private RedisBloomFilter redisBloomFilter;
@Transactional(rollbackFor = Exception.class)
public void wrong() {
AddCondition addCondition = AddCondition.create().keyPrefix("news").keyName("user2").exceptionInsert(500L).fpp(0.001);
redisBloomFilter.add(addCondition,"推送1");
userTicketMapper.insert(new UserTicket().setCreateTime(LocalDateTime.now()).setTicketId(1L).setUserId(2L));
int i = 1 / 0;
}
@Transactional(rollbackFor = Exception.class)
public void right() {
AddCondition addCondition = AddCondition.create().keyPrefix("news").keyName("user1").exceptionInsert(500L).fpp(0.001);
redisBloomFilter.add(addCondition,"推送3");
userTicketMapper.insert(new UserTicket().setCreateTime(LocalDateTime.now()).setTicketId(1L).setUserId(2L));
}
}
访问两个接口查看redis、mysql的结果
确实只有一个成功的,即user1有值(EnableBloomFilter的事务默认不开启)
目前支持三种限流模式——滑动窗口限流、漏斗限流、令牌桶限流,这三种模式配置时,要添加fallback方法,否则会抛异常
并在后来加多一个限流组的功能来支持动态配置,但底层核心还是上面三种模式
使用方式
在接口上添加注解@EnableLimiter,会加载对应的类,aop会进行拦截并做相应的处理,通过@Import加载注册类,enableGroup开启限流组
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@LimiterType(mode = LimiterConstants.FUNNEL_LIMITER)
public @interface FunnelLimiter {
/**
* 漏斗容量
*
* @return
*/
double capacity();
/**
* 每秒漏出的速率
*
* @return
*/
double funnelRate() ;
/**
* 时间单位
*
* @return
*/
TimeUnit funnelRateUnit() default TimeUnit.SECONDS;
/**
* 每次请求所需加的水量
*
* @return
*/
double requestNeed() default 1;
String fallback() default "";
boolean passArgs() default false;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@LimiterType(mode = LimiterConstants.TOKEN_LIMITER)
public @interface TokenLimiter {
/**
* 令牌桶容量
*
* @return
*/
double capacity();
/**
* 令牌生成速率
*
* @return
*/
double tokenRate();
/**
* 速率时间单位,默认秒
*
* @return
*/
TimeUnit tokenRateUnit() default TimeUnit.SECONDS;
/**
* 每次请求所需要的令牌数
*
* @return
*/
double requestNeed() default 1;
double initToken() default 0;
String fallback() default "";
boolean passArgs() default false;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@LimiterType(mode = LimiterConstants.WINDOW_LIMITER)
public @interface WindowLimiter {
/**
* 持续时间,窗口间隔
*
* @return
*/
int during() default 1;
TimeUnit duringUnit() default TimeUnit.SECONDS;
/**
* 通过的请求数
*
* @return
*/
long passCount();
String fallback() default "";
boolean passArgs() default false;
}
fallback则是定义于本类的其他public的方法,可以设置是否传参,参数则是被拦截的方法的参数,可以在返回的方法使用这些参数
@RestController
public class TestController2 {
@GetMapping("ha")
@WindowLimiter(during = 10,passCount = 5)
public String test() {
return "hihi1";
}
//每秒通过0.5个请求
@GetMapping("ha2/{userName}")
@FunnelLimiter(capacity = 5,funnelRate = 0.5,requestNeed = 1,fallback = "test",passArgs = true)
public Result<String> test2(@PathVariable("userName")String userName) throws NoSuchMethodException {
return Result.success("ok");
}
//默认为秒,该配置为每秒生成0.5个令牌
@GetMapping("ha3")
@TokenLimiter(capacity = 5,tokenRate = 0.5,requestNeed = 1)
public String test3() {
return "hihi3";
}
//一分钟生产一个令牌
@GetMapping("ha4")
@TokenLimiter(capacity = 5,tokenRate = 1,tokenRateUnit = TimeUnit.MINUTES,initToken = 5)
public String test4() {
return "hihi4";
}
public Result<String> test(String userName){
return Result.success("对不起:"+userName+",挤不进去太多人了");
}
}
这里给出Resul类
@Data
@AllArgsConstructor
public class Result<T> {
private T data;
private String msg;
private Integer code;
public static Result success(Object data){
return new Result(data,"ok",0);
}
}
下面到动态配置的功能介绍,需要在注解@EnableLimiter上配置 enableGroup=true,默认不开启
首先要定义一个限流器,下面这个demo几乎把所有配置都列出来了,其中id是最重要的,用来标记该限流器,做好配置后,需要添加相关的拦截器,本身有四个拦截器,url前缀拦截器,ip黑/白名单,和本身的限流器,按权重执行从大到小执行,可以调用order方法来更改他的权限大小,默认执行顺序为ip-url-限流,也可以自己实现相关的拦截器,但权重要做相关的调整
@Configuration
public class RateLimitConfig implements InitializingBean {
@Autowired
private LimiterGroupService limiterGroupService;
@Override
public void afterPropertiesSet() {
//清除原来的配置
limiterGroupService.clear("1");
//新建
LimiteGroupConfig config = LimiteGroupConfig.of().id("1")
.remark("this.application").tokenConfig(
//令牌桶配置,下面表示令牌桶容量为5,初始桶为3,每1s生产3个令牌,每个请求消耗1个令牌
TokenRateConfig.of()
.capacity(5.0)
.initToken(3.0)
.requestNeed(1.0)
.tokenRate(3.0)
.tokenRateUnit(TimeUnit.SECONDS)
.build()
).
windowConfig(
//滑动窗口配置,下面表示10s内只允许5个通过
WindowRateConfig.of()
.passCount(5L)
.during(10L)
.duringUnit(TimeUnit.SECONDS)
.build()).currentMode(LimiterConstants.TOKEN_LIMITER)
//漏斗配置,容纳量为10,每次请求容纳量-1,每3秒增加1个容纳量
.funnelConfig(FunnelRateConfig.of()
.capacity(10.0)
.funnelRate(3.0)
.funnelRateUnit(TimeUnit.SECONDS)
.requestNeed(1.0)
.build())
//黑白名单,网段 xxx.xxx.xxx./24,类似 192.168.0.0-192.168.2.1 以及 192* 分号分隔
/*.blackRule("127.0.0.1")
.enableBlackList(true)
.enableWhiteList(true).
whiteRule("192.168.0.*")
*/
.blackRuleFallback("ip")
//当前限流模式
.currentMode(LimiterConstants.TOKEN_LIMITER)
//开启统计,是统计复用该配置下的请求数
.enableCount(true)
//统计时间范围,如果没有则从第一次请求开始统计
.countDuring(1L).countDuringUnit(TimeUnit.MINUTES)
//url配置,;号分割
.unableURLPrefix("/user;/qq")
.enableURLPrefix("/test")
//url匹配失败后的执行方法
.urlFallBack("userBlack")
.build();
//保存到redis,也可以保存到本地
limiterGroupService.save(config, true, false);
//读取redis上的配置
// limiterGroupService.reload("1");
//添加对应的拦截器,不然切面中不会执行对应的逻辑,这里也可以实现自己的拦截器并添加上去
limiterGroupService.addHandler(GroupHandlerFactory.limiteHandler())
.addHandler(GroupHandlerFactory.ipBlackHandler())
.addHandler(GroupHandlerFactory.urlPrefixHandler());
;
}
}
内置了一个controller用于动态更改配置,目前主要是ip、url、限流模式、限流器的配置,更改模式时可以选择是否删掉其他限流器在redis上的缓存,url拦截器通过前面urlhanlder配置
@RestController
public class ActuatorController {
@Autowired
private LimiterGroupService limiterGroupService;
@GetMapping("/redis-aux/getIp")
public String getIp(HttpServletRequest request) {
return IpCheckUtil.getIpAddr(request);
}
//更改ip规则
@PostMapping("/redis-aux/changeIpRule")
public LimiteGroupConfig changeRule(@RequestParam("groupId") String groupId,
@RequestParam(value = "rule", required = false) String rule,
@RequestParam(value = "enable", required = false) Boolean enable,
@RequestParam(value = "white", required = false) Boolean white) {
LimiteGroupConfig limiter = limiterGroupService.getLimiterConfig(groupId);
if (white) {
limiter.setWhiteRule(rule);
limiter.setEnableWhiteList(enable);
} else {
limiter.setBlackRule(rule);
limiter.setEnableBlackList(enable);
}
limiterGroupService.save(limiter, true, false);
return limiter;
}
//更改url匹配规则
@PostMapping("/redis-aux/changeUrlRule")
public LimiteGroupConfig changeUrlRule(@RequestParam("groupId") String groupId,
@RequestParam("enableUrl") String enableUrl,
@RequestParam("unableUrl") String unableUrl
) {
LimiteGroupConfig limiter = limiterGroupService.getLimiterConfig(groupId);
if (enableUrl != null) {
limiter.setEnableURLPrefix(enableUrl);
}
if (unableUrl != null) {
limiter.setUnableURLPrefix(unableUrl);
}
limiterGroupService.save(limiter, true, false);
return limiter;
}
//更改模式
@PostMapping("/redis-aux/changeLimitMode")
public LimiteGroupConfig changeMode(@RequestParam("groupId") String groupId, @RequestParam("mode") Integer mode
, @RequestParam("removeOther") Boolean removeOther
) {
LimiteGroupConfig limiter = limiterGroupService.getLimiterConfig(groupId);
if (mode < 4 && mode > 0) {
limiter.setCurrentMode(mode);
}
limiterGroupService.save(limiter, true, removeOther);
return limiter;
}
//更改限流规则
@PostMapping("/redis-aux/changeFunnelConfig")
public LimiteGroupConfig changeFunnelConfig(@RequestParam("groupId") String groupId,
@RequestParam(value = "requestNeed", required = false) Double requestNeed,
@RequestParam("capacity") Double capacity,
@RequestParam("funnelRate") Double funnelRate,
@RequestParam(value = "funnelRateUnit", required = false) Integer funnelRateUnit
) {
FunnelRateConfig config = FunnelRateConfig.of().capacity(capacity)
.funnelRate(funnelRate).requestNeed(requestNeed)
.funnelRateUnit(TimeUnitEnum.getTimeUnit(funnelRateUnit)).build();
LimiteGroupConfig limiter = limiterGroupService.getLimiterConfig(groupId);
limiter.setFunnelRateConfig(config);
limiterGroupService.save(limiter, true, false);
return limiter;
}
@PostMapping("/redis-aux/changeWindowConfig")
public LimiteGroupConfig changeWindowConfig(@RequestParam("groupId") String groupId,
@RequestParam("passCount") Long passCount,
@RequestParam(value = "during", required = false) Long during,
@RequestParam(value = "duringUnit", required = false) Integer mode
) {
WindowRateConfig config = WindowRateConfig.of().passCount(passCount).during(during).duringUnit(TimeUnitEnum.getTimeUnit(mode)).build();
LimiteGroupConfig limiter = limiterGroupService.getLimiterConfig(groupId);
limiter.setWindowRateConfig(config);
limiterGroupService.save(limiter, true, false);
return limiter;
}
@PostMapping("/redis-aux/changeTokenConfig")
public LimiteGroupConfig changeWindowConfig(@RequestParam("groupId") String groupId,
@RequestParam("capacity") Double capacity,
@RequestParam(value = "initToken", required = false) Double initToken,
@RequestParam("tokenRate") Double tokenRate,
@RequestParam(value = "requestNeed", required = false) Double requestNeed,
@RequestParam(value = "duringUnit", required = false) Integer mode
) {
TokenRateConfig config = TokenRateConfig.of().capacity(capacity).initToken(initToken).tokenRate(tokenRate)
.requestNeed(requestNeed).tokenRateUnit(TimeUnitEnum.getTimeUnit(mode)).build();
LimiteGroupConfig limiter = limiterGroupService.getLimiterConfig(groupId);
limiter.setTokenRateConfig(config);
limiterGroupService.save(limiter, true, false);
return limiter;
}
@GetMapping("/redis-aux/getCount/{groupId}")
public Map<String, String> changeCountConfig(@PathVariable("groupId") String groupId
) {
return limiterGroupService.getCount(groupId);
}
}
然后到使用方式,配置好后,可以在类或者方法上使用,如果在类中使用
@RestController
@LimiteGroup(groupId = "1", fallback = "test")
public class TestController {
@GetMapping("/ok")
public String ok() {
return "ok";
}
@GetMapping("/user")
public String user() {
return "user";
}
@GetMapping("/user/t")
@LimiteExclude
public String usert() {
return "usert";
}
public String userBlack() {
return "非法前缀访问";
}
public String ip() {
return "ip错误";
}
public String test() {
return "too much request";
}
}
可以访问ActuatorController 的接口进行相关的配置,下面的统计功能采用滑动窗口+分桶计算该段时间的qps,是对单个应用的数据统计
更改限流模式
此时可以看出,每个接口对应自己的限流器,但是配置是公用的,修改配置,token对应的模式为2,现在改为window模式(1)
如果removeOther=true,则会删除其他限流器在redis上的配置,底层实现是之前的限流器,需要时会重新生成。
此时查看redis
有两个限流器出来了,更改以后返回限流提示的次数更多,因为上面的滑动窗口配置只允许10s内通过5次
url前缀
如果是合法的url前缀则直接通过,默认为"/*"属于继续后面的逻辑过程
ip地址也差不多,在先前的配置项里可以找到,目前支持网段,范围和*通配符
如果在使用了@LimiteGroup注解上的类想排除某些方法,可以用@LimiteExclude取消拦截链处理
主要有两个模块——布隆过滤器、基于注解限流。目前的功能基于redisTemplate 用法: 这里用maven作为工具管理包演示,添加jitpack源、添加下面的依赖 <repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url>
ps aux | grep redis 提示:该文章有不完善的地方, 大家评论区指点, 谢谢, 另外也在不断更新中 一、ps命令作用是什么? 查看linux系统中软件的运行状态 二、结果详解 三、其他用法 1.USER(用户) 2.PID(进程id) 3.CPU(占用cpu内存百分比) 4.MEM(占用物理磁盘百分比) 5.VSZ(总虚拟内存大小, 单位Kbytes字节) 6.RSS(进程使用的总
YAF中封装了redis缓存的一个类 Kv /** * 键值对存储 * Function list: * - set() * - get() * - del() * - flush() */ 4个方法 第一个是存 第二个是取 第三个,第四个都是删 如何把一个数组存进redis中再取出来呢 $array=array(0=>"1996",1=>"1997";2=>"1998");
redis的持久化 Redis的事务 Redis的发布订阅 Redis的复制(Master/Slave) Redis - Jedis
查看redis是否启动使用的是 ps aux | grep redis-server 命令 ps aux | grep redis-server 可以在根目录下使用,也可以在其他目录下使用 [red@RedFace sbin]$ cd / [red@RedFace /]$ ps aux | grep redis-server red 6218 0.0 0.0 4420 76
redis一般都是用来做数据缓存。但是当redis宕机,内存里的数据就会丢失。所以redis里的数据也要做持久化操作。 第一时间想到的是从数据库里获取。但是这个操作首先需要频繁访问数据库,给数据库带来压力,而且数据是从数据库里读取的,性能自然不能跟从redis比。 redis是采用rdb和aof实现数据持久化。 AOF : 写后日志。即先执行命令,再把执行的日志保存到日志文件里。AOF里存的是re
redis的事务 redis推荐学习网站 redis菜鸟教程链接 redis事务 Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证: 批量操作在发送 EXEC 命令前被放入队列缓存。 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。 一个事务从开始到执行会经历以下三个阶段:
redis是什么? wiki redis官方介绍:introduction to redis 安装: install 拉到最下面的install小节 wget http://download.redis.io/releases/redis-4.0.11.tar.gz tar zxvf redis-4.0.11.tar.gz cd redis-4.0.11 make 运行: [root
redis.Redis与redis.StrictRedis区别 redis-py提供两个类Redis和StrictRedis用于实现Redis的命令,StrictRedis用于实现大部分官方的命令,并使用官方的语法和命令(比如,SET命令对应与StrictRedis.set方法)。Redis是StrictRedis的子类,用于向后兼容旧版本的redis-py。 简单说,官方推荐使用StrictRe
Redis-key http://redis.cn/commands.html 官网查询 127.0.0.1:6379> keys * #查看所有key (empty array) 127.0.0.1:6379> set name bowenxu #设置key OK 127.0.0.1:6379> keys * 1) "name" 127.0.0.1:6379> set age 1 OK 127.
1: ASR语言模型在线训练工具 2: TTS在线语音合成工具
由于 Go 标准库的强大支持,Go 可以很容易的进行 Web 开发。为此,Go 标准库专门提供了 httptest 包专门用于进行 http Web 开发测试。 本节我们通过一个社区帖子的增删改查的例子来学习该包。 简单的 Web 应用 我们首先构建一个简单的 Web 应用。 为了简单起见,数据保存在内存,并且没有考虑并发问题。 // 保存 Topic,没有考虑并发问题 var TopicCach
web3.utils属性包含一组辅助函数集。 调用方法: Web3.utils web3.utils
0.1 电脑:辅助人脑的好工具 现在的人们几乎无时无刻都会碰电脑!不管是桌面电脑(台式机)、笔记本电脑(笔记本)、平板电脑、智能手机等等,这些东西都算是电脑。 虽然接触的这么多,但是,你了解电脑里面的元件有什么吗?以台式机来说,电脑的机箱里面含有什么元件?不同的电脑可以应用在哪些工作? 你生活周遭有哪些电器用品内部是含有电脑相关元件的?下面我们就来谈一谈这些东西呢! 所谓的电脑就是一种计算机,而计
本文向大家介绍Java编码辅助工具Lombok用法详解,包括了Java编码辅助工具Lombok用法详解的使用技巧和注意事项,需要的朋友参考一下 前言 在项目开发过程中,经常会涉及到一些调整很少但又必不可少的环节,比如实体类的Getter/Setter方法,ToString方法等。这时可以使用Lombok来避免这种重复的操作,减少非核心代码的臃肿,提高编码效率。 如何在IntelliJ IDEA中引
本文向大家介绍python人民币小写转大写辅助工具,包括了python人民币小写转大写辅助工具的使用技巧和注意事项,需要的朋友参考一下 本文实例为大家分享了python人民币大小写转换的具体代码,供大家参考,具体内容如下 大家应该都知道,银行打印账单有时候会跟上人民币的阿拉伯数字以及人民币汉字大写写法,转换的过程中有一定的逻辑难度,较为麻烦,所以笔者心血来潮,花了点时间简单实现了一下这一转换过程,
Hyperf 提供了大量便捷的辅助类,这里会列出一些常用的好用的,不会列举所有,可自行查看 hyperf/utils 组件的代码获得更多信息。 协程辅助类 Hyperf\Utils\Coroutine 该辅助类用于协助进行协程相关的判断或操作。 id(): int 通过静态方法 id() 获得当前所处的 协程 ID,如当前不处于协程环境下,则返回 -1。 create(callable $call
本文向大家介绍Android辅助功能AccessibilityService与抢红包辅助,包括了Android辅助功能AccessibilityService与抢红包辅助的使用技巧和注意事项,需要的朋友参考一下 推荐阅读:Android中微信抢红包插件原理解析及开发思路 抢红包的原理都差不多,一般是用Android的辅助功能(AccessibilityService类)先监听通知栏事件或窗口变化事