重试利器Guava Retrying
重试的使用场景 在很多业务场景中,为了排除系统中的各种不稳定因素,以及逻辑上的错误,并最大概率保证获得预期的结果,重试机制都是必不可少的。 尤其是调用远程服务,在高并发场景下,很可能因为服务器响应延迟或者网络原因,造成我们得不到想要的结果,或者根本得不到响应。这个时候,一个优雅的重试调用机制,可以让我们更大概率保证得到预期的响应。 sequenceDiagram Client->>Server:{"msg":"hello,server"} Note right of Server: busying ...... Client->>Server:{"msg":"hello,server"}
Server-->>Client:{"Exception":"500"}
Note right of Server: busying ......loop ServerBlockServer -->> Server: Too busy to deal with so many requests...endClient->>Server:{"msg":"hello,server"}
activate Server
Server-->>Client:{"msg":"hello,client"}
deactivate Server
通常情况下,我们会通过<strong>定时任务</strong>进行重试。例如某次操作失败,则记录下来,当定时任务再次启动,则将数据放到定时任务的方法中,重新跑一遍。最终直至得到想要的结果为止。 无论是基于定时任务的重试机制,还是我们自己写的简单的重试器,缺点都是重试的机制太单一,而且实现起来不优雅。 如何优雅地设计重试实现 一个完备的重试实现,要很好地解决如下问题:
什么条件下重试 什么条件下停止 如何停止重试 停止重试等待多久 如何等待 请求时间限制 如何结束 如何监听整个重试过程
并且,为了更好地封装性,重试的实现一般分为两步:
使用工厂模式构造重试器 执行重试方法并得到结果
一个完整的重试流程可以简单示意为: graph LR A((Start)) -->|build| B(Retryer) B --> C{need call?} C -->|continue| D[call] D --> Z[call count++] Z --> C C -->|finished| E[result] E --> F((success)) E --> G((failed ))
guava-retrying基础用法 guava-retrying是基于谷歌的核心类库guava的重试机制实现,可以说是一个重试利器。 下面就快速看一下它的用法。 1.Maven配置 <!-- https://mvnrepository.com/artifact/com.github.rholder/guava-retrying --> <dependency> <groupId>com.github.rholder</groupId> <artifactId>guava-retrying</artifactId> <version>2.0.0</version> </dependency>
需要注意的是,此版本依赖的是27.0.1版本的guava。如果你项目中的guava低几个版本没问题,但是低太多就不兼容了。这个时候你需要升级你项目的guava版本,或者直接去掉你自己的guava依赖,使用guava-retrying传递过来的guava依赖。 2.实现Callable Callable<Boolean> callable = new Callable<Boolean>() { public Boolean call() throws Exception { return true; // do something useful here } };
Callable的call方法中是你自己实际的业务调用。
通过RetryerBuilder构造Retryer
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder() .retryIfResult(Predicates.<Boolean>isNull()) .retryIfExceptionOfType(IOException.class) .retryIfRuntimeException() .withStopStrategy(StopStrategies.stopAfterAttempt(3)) .build();
使用重试器执行你的业务
retryer.call(callable);
下面是完整的参考实现。 public Boolean test() throws Exception { //定义重试机制 Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder() //retryIf 重试条件 .retryIfException() .retryIfRuntimeException() .retryIfExceptionOfType(Exception.class) .retryIfException(Predicates.equalTo(new Exception())) .retryIfResult(Predicates.equalTo(false))
//等待策略:每次请求间隔1s.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))//停止策略 : 尝试请求6次.withStopStrategy(StopStrategies.stopAfterAttempt(6))//时间限制 : 某次请求不得超过2s , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(2, TimeUnit.SECONDS)).build();//定义请求实现
Callable<Boolean> callable = new Callable<Boolean>() {int times = 1;@Overridepublic Boolean call() throws Exception {log.info("call times={}", times);times++;if (times == 2) {throw new NullPointerException();} else if (times == 3) {throw new Exception();} else if (times == 4) {throw new RuntimeException();} else if (times == 5) {return false;} else {return true;}}
};
//利用重试器调用请求
return retryer.call(callable); }
guava-retrying实现原理 guava-retrying的核心是Attempt类、Retryer类以及一些Strategy(策略)相关的类。
Attempt
Attempt既是一次重试请求(call),也是请求的结果,并记录了当前请求的次数、是否包含异常和请求的返回值。 /**
An attempt of a call, which resulted either in a result returned by the call,
or in a Throwable thrown by the call.
@param <V> The type returned by the wrapped callable.
@author JB */ public interface Attempt<V>
Retryer
Retryer通过RetryerBuilder这个工厂类进行构造。RetryerBuilder负责将定义的重试策略赋值到Retryer对象中。 在Retryer执行call方法的时候,会将这些重试策略一一使用。 下面就看一下Retryer的call方法的具体实现。 /** * Executes the given callable. If the rejection predicate * accepts the attempt, the stop strategy is used to decide if a new attempt * must be made. Then the wait strategy is used to decide how much time to sleep * and a new attempt is made. * * @param callable the callable task to be executed * @return the computed result of the given callable * @throws ExecutionException if the given callable throws an exception, and the * rejection predicate considers the attempt as successful. The original exception * is wrapped into an ExecutionException. * @throws RetryException if all the attempts failed before the stop strategy decided * to abort, or the thread was interrupted. Note that if the thread is interrupted, * this exception is thrown and the thread's interrupt status is set. */ public V call(Callable<V> callable) throws ExecutionException, RetryException { long startTime = System.nanoTime(); //说明: 根据attemptNumber进行循环——也就是重试多少次 for (int attemptNumber = 1; ; attemptNumber++) { //说明:进入方法不等待,立即执行一次 Attempt<V> attempt; try { //说明:执行callable中的具体业务 //attemptTimeLimiter限制了每次尝试等待的时常 V result = attemptTimeLimiter.call(callable); //利用调用结果构造新的attempt attempt = new ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)); } catch (Throwable t) { attempt = new ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)); }
//说明:遍历自定义的监听器for (RetryListener listener : listeners) {listener.onRetry(attempt);}//说明:判断是否满足重试条件,来决定是否继续等待并进行重试if (!rejectionPredicate.apply(attempt)) {return attempt.get();}//说明:此时满足停止策略,因为还没有得到想要的结果,因此抛出异常if (stopStrategy.shouldStop(attempt)) {throw new RetryException(attemptNumber, attempt);} else {//说明:执行默认的停止策略——线程休眠long sleepTime = waitStrategy.computeSleepTime(attempt);try {//说明:也可以执行定义的停止策略blockStrategy.block(sleepTime);} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RetryException(attemptNumber, attempt);}}}
}
Retryer执行过程如下。 graph TB
sq[Retryer] --> ci((call))subgraph Retryingrb>RetryerBuilder]-- build retryer<br/>with strategies --> rodi{Retryer:<br/>using callable whith <br/>strategies execute call...} -.->ro(<br>.retryIf...<br>.withWaitStrategy<br>.withStopStrategy<br>.withAttemptTimeLimiter<br>.withBlockStrategy<br>.withRetryListene)di==>ro2(Attempt: get the result)
endclassDef green fill:#9f6,stroke:#333,stroke-width:2px;classDef orange fill:#f96,stroke:#333,stroke-width:4px;class sq,e greenclass di orange
guava-retrying高级用法 基于guava-retrying的实现原理,我们可以根据实际业务来确定自己的重试策略。 下面以数据同步这种常规系统业务为例,自定义重试策略。 如下实现基于Spring Boot 2.1.2.RELEASE版本。 并使用Lombok简化Bean。 <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
业务描述 当商品创建以后,需要另外设置商品的价格。由于两个操作是有两个人进行的,因此会出现如下问题,即商品没有创建,但是价格数据却已经建好了。遇到这种情况,价格数据需要等待商品正常创建以后,继续完成同步。 我们通过一个http请求进行商品的创建,同时通过一个定时器来修改商品的价格。 当商品不存在,或者商品的数量小于1的时候,商品的价格不能设置。需要等商品成功创建且数量大于0的时候,才能将商品的价格设置成功。 实现过程
自定义重试阻塞策略
默认的阻塞策略是线程休眠,这里使用自旋锁实现,不阻塞线程。 package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.strategy;
import com.github.rholder.retry.BlockStrategy; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j;
import java.time.Duration; import java.time.LocalDateTime;
/**
自旋锁的实现, 不响应线程中断 */ @Slf4j @NoArgsConstructor public class SpinBlockStrategy implements BlockStrategy {
@Override public void block(long sleepTime) throws InterruptedException {
LocalDateTime startTime = LocalDateTime.now();long start = System.currentTimeMillis();long end = start;log.info("[SpinBlockStrategy]...begin wait.");while (end - start <= sleepTime) {end = System.currentTimeMillis();}//使用Java8新增的Duration计算时间间隔Duration duration = Duration.between(startTime, LocalDateTime.now());log.info("[SpinBlockStrategy]...end wait.duration={}", duration.toMillis());
} }
自定义重试监听器
RetryListener可以监控多次重试过程,并可以使用attempt做一些额外的事情。 package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.listener;
import com.github.rholder.retry.Attempt; import com.github.rholder.retry.RetryListener; import lombok.extern.slf4j.Slf4j;
@Slf4j public class RetryLogListener implements RetryListener {
@Override
public <V> void onRetry(Attempt<V> attempt) {// 第几次重试,(注意:第一次重试其实是第一次调用)log.info("retry time : [{}]", attempt.getAttemptNumber());// 距离第一次重试的延迟log.info("retry delay : [{}]", attempt.getDelaySinceFirstAttempt());// 重试结果: 是异常终止, 还是正常返回log.info("hasException={}", attempt.hasException());log.info("hasResult={}", attempt.hasResult());// 是什么原因导致异常if (attempt.hasException()) {log.info("causeBy={}" , attempt.getExceptionCause().toString());} else {// 正常返回时的结果log.info("result={}" , attempt.getResult());}log.info("log listen over.");}
}
自定义Exception
有些异常需要重试,有些不需要。 package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.exception;
/**
当抛出这个异常的时候,表示需要重试 */ public class NeedRetryException extends Exception {
public NeedRetryException(String message) { super("NeedRetryException can retry."+message); }
}
实现具体重试业务与Callable接口
使用call方法调用自己的业务。 package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.model;
import lombok.AllArgsConstructor; import lombok.Data;
import java.math.BigDecimal;
/**
商品model */ @Data @AllArgsConstructor public class Product {
private Long id;
private String name;
private Integer count;
private BigDecimal price;
}
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.repository;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.model.Product; import org.springframework.stereotype.Repository;
import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong;
/**
商品DAO */ @Repository public class ProductRepository {
private static ConcurrentHashMap<Long,Product> products=new ConcurrentHashMap();
private static AtomicLong ids=new AtomicLong(0);
public List<Product> findAll(){ return new ArrayList<>(products.values()); }
public Product findById(Long id){ return products.get(id); }
public Product updatePrice(Long id, BigDecimal price){ Product p=products.get(id); if (null==p){ return p; } p.setPrice(price); return p; }
public Product addProduct(Product product){ Long id=ids.addAndGet(1); product.setId(id); products.put(id,product); return product; }
}
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.service;
import lombok.extern.slf4j.Slf4j; import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.exception.NeedRetryException; import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.model.Product; import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.repository.ProductRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;
import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable;
/**
业务方法实现 */ @Component @Slf4j public class ProductInformationHander implements Callable<Boolean> {
@Autowired private ProductRepository pRepo;
private static Map<Long, BigDecimal> prices = new HashMap<>();
static { prices.put(1L, new BigDecimal(100)); prices.put(2L, new BigDecimal(200)); prices.put(3L, new BigDecimal(300)); prices.put(4L, new BigDecimal(400)); prices.put(8L, new BigDecimal(800)); prices.put(9L, new BigDecimal(900)); }
@Override public Boolean call() throws Exception {
log.info("sync price begin,prices size={}", prices.size());for (Long id : prices.keySet()) {Product product = pRepo.findById(id);if (null == product) {throw new NeedRetryException("can not find product by id=" + id);}if (null == product.getCount() || product.getCount() < 1) {throw new NeedRetryException("product count is less than 1, id=" + id);}Product updatedP = pRepo.updatePrice(id, prices.get(id));if (null == updatedP) {return false;}prices.remove(id);}log.info("sync price over,prices size={}", prices.size());return true;
}
}
构造重试器Retryer
将上面的实现作为参数,构造Retryer。 package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.service;
import com.github.rholder.retry.*; import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.exception.NeedRetryException; import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.listener.RetryLogListener; import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.strategy.SpinBlockStrategy; import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
构造重试器 */ @Component public class ProductRetryerBuilder {
public Retryer build() { //定义重试机制 Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
//retryIf 重试条件//.retryIfException()//.retryIfRuntimeException()//.retryIfExceptionOfType(Exception.class)//.retryIfException(Predicates.equalTo(new Exception()))//.retryIfResult(Predicates.equalTo(false)).retryIfExceptionOfType(NeedRetryException.class)//等待策略:每次请求间隔1s.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))//停止策略 : 尝试请求3次.withStopStrategy(StopStrategies.stopAfterAttempt(3))//时间限制 : 某次请求不得超过2s , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(2, TimeUnit.SECONDS))//默认的阻塞策略:线程睡眠//.withBlockStrategy(BlockStrategies.threadSleepStrategy())//自定义阻塞策略:自旋锁.withBlockStrategy(new SpinBlockStrategy())//自定义重试监听器.withRetryListener(new RetryLogListener()).build();return retryer;
} }
与定时任务结合执行Retryer
定时任务只需要跑一次,但是实际上实现了所有的重试策略。这样大大简化了定时器的设计。 首先使用@EnableScheduling声明项目支持定时器注解。 @SpringBootApplication @EnableScheduling public class DemoRetryerApplication { public static void main(String[] args) { SpringApplication.run(DemoRetryerApplication.class, args); } }
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.task;
import com.github.rholder.retry.Retryer; import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.service.ProductInformationHander; import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.service.ProductRetryerBuilder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component;
/**
商品信息定时器 */ @Component public class ProductScheduledTasks {
@Autowired private ProductRetryerBuilder builder;
@Autowired private ProductInformationHander hander;
/**
- 同步商品价格定时任务
- @Scheduled(fixedDelay = 30000) :上一次执行完毕时间点之后30秒再执行 _/ @Scheduled(fixedDelay = 30_1000) public void syncPrice() throws Exception{ Retryer retryer=builder.build(); retryer.call(hander); }
}
执行结果:由于并没有商品,因此重试以后,抛出异常。 2019-二月-28 14:37:52.667 INFO [scheduling-1] n.i.t.f.s.i.d.r.g.l.RetryLogListener - log listen over. 2019-二月-28 14:37:52.672 ERROR [scheduling-1] o.s.s.s.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task. com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts. at com.github.rholder.retry.Retryer.call(Retryer.java:174)
你也可以增加一些商品数据,看一下重试成功的效果。 完整示例代码在这里。 使用中遇到的问题 Guava版本冲突 由于项目中依赖的guava版本过低,启动项目时出现了如下异常。 java.lang.NoSuchMethodError: com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor()Lcom/google/common/util/concurrent/ListeningExecutorService; at org.apache.curator.framework.listen.ListenerContainer.addListener(ListenerContainer.java:41) at com.bzn.curator.ZkOperator.getZkClient(ZkOperator.java:207) at com.bzn.curator.ZkOperator.checkExists(ZkOperator.java:346) at com.bzn.curator.watcher.AbstractWatcher.initListen(AbstractWatcher.java:87) at com.bzn.web.listener.NebulaSystemInitListener.initZkWatcher(NebulaSystemInitListener.java:84) at com.bzn.web.listener.NebulaSystemInitListener.contextInitialized(NebulaSystemInitListener.java:33) at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4939) at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5434) at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150) at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1559) at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1549) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
因此,要排除项目中低版本的guava依赖。 <exclusion> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </exclusion>
同时,由于Guava在新版本中移除了sameThreadExecutor方法,但目前项目中的ZK需要此方法,因此需要手动设置合适的guava版本。 果然,在19.0版本中MoreExecutors的此方法依然存在,只是标注为过期了。 @Deprecated @GwtIncompatible("TODO") public static ListeningExecutorService sameThreadExecutor() { return new DirectExecutorService(); }
声明依赖的guava版本改为19.0即可。 <!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </dependency>
动态调节重试策略 在实际使用过程中,有时经常需要调整重试的次数、等待的时间等重试策略,因此,将重试策略的配置参数化保存,可以动态调节。 例如在秒杀、双十一购物节等时期增加等待的时间与重试次数,以保证错峰请求。在平时,可以适当减少等待时间和重试次数。 对于系统关键性业务,如果多次重试步成功,可以通过RetryListener进行监控与报警。 关于 『动态调节重试策略 』下面提供一个参考实现。 import com.github.rholder.retry.Attempt; import com.github.rholder.retry.WaitStrategy;
/**
自定义等待策略:根据重试次数动态调节等待时间,第一次请求间隔1s,第二次间隔10s,第三次及以后都是20s。
在创建Retryer的时候通过withWaitStrategy将该等待策略生效即可。
RetryerBuilder.<Boolean>newBuilder()
.withWaitStrategy(new AlipayWaitStrategy())
类似的效果也可以通过自定义 BlockStrategy 来实现,你可以写一下试试。
*/ public class AlipayWaitStrategy implements WaitStrategy {
@Override
public long computeSleepTime(Attempt failedAttempt) { long number = failedAttempt.getAttemptNumber(); if (number==1){ return 1*1000; } if (number==2){ return 10*1000; } return 20*1000;
}
}
转载于:https://my.oschina.net/u/437232/blog/3069727
重试利器Guava Retrying相关推荐
- 重试利器之Guava Retrying (一、介绍及简单实现)
何为Guava Retrying? guava-retrying是基于谷歌的核心类库guava的重试机制实现,可以说是一个重试利器. Guava Retrying的使用场景? 在高并发开发的过程中,调 ...
- foxmail邮件加载失败重试_java retry(重试) spring retry, guava retrying 详解
系列说明 java retry 的一步步实现机制. java-retry 源码地址 情景导入 简单的需求 产品经理:实现一个按条件,查询用户信息的服务. 小明:好的.没问题. 代码 UserServi ...
- java retry(重试) spring retry, guava retrying 详解
转载 自 http://blog.51cto.com/9250070/2156431 系列说明 java retry 的一步步实现机制. java-retry 源码地址 情景导入 简单的需求 产品经理 ...
- 【转载】Java重试机制
重试机制在分布式系统中,或者调用外部接口中,都是十分重要的. 重试机制可以保护系统减少因网络波动.依赖服务短暂性不可用带来的影响,让系统能更稳定的运行的一种保护机制. 为了方便说明,先假设我们想要进行 ...
- 层层递进打造你的重试机制
重试机制在分布式系统中,或者调用外部接口中,都是十分重要的. 重试机制可以保护系统减少因网络波动.依赖服务短暂性不可用带来的影响,让系统能更稳定的运行的一种保护机制. 为了方便说明,先假设我们想要进行 ...
- 使用Guava retryer优雅的实现接口重试机制
转载自: 使用Guava retrying优雅的实现接口重调机制 Guava retrying:基于 guava 的重试组件 实际项目中,为了考虑网络抖动,加锁并发冲突等场景,我们经常需要对异常操作进 ...
- Python重试库Retrying和Tenacity
文章目录 简介 安装 Retrying 初试 最大重试次数 最大重试时间 重试间隔 随机间隔 指数级重试间隔 自定义异常重试 自定义返回重试 参数 Tenacity 初试 最大重试次数 最大重试时间 ...
- Google Guava Retry 优雅的重试方案
Google Guava Retry 优雅的重试方案 前言 使用场景 什么场景不适合重试 了解幂等性 一.Guava Retry是什么? 与Spring retry比较 二.使用步骤 1.引入库 2. ...
- 说说重试的那些实现方式
点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 作者 | 端木轩 来源 | i7q.cn/5acmSt ...
- 这或许是实现重试最优雅的姿势了!
使用场景 在日常开发中,我们经常会遇到需要调用外部服务和接口的场景.外部服务对于调用者来说一般都是不可靠的,尤其是在网络环境比较差的情况下,网络抖动很容易导致请求超时等异常情况,这时候就需要使用失败重 ...
最新文章
- python学会了可以做什么菜_python学习之路(24)
- mac os 命令行下载
- CentOS 7 网络连接优先由与无线问题解决
- python怎么输出图像测试_python pyautogui-不检测图像时的位置打印问题
- 【web安全】第三弹:web攻防平台pentester安装及XSS部分答案解析
- Spring Cloud Gateway-ServerWebExchange核心方法与请求或者响应内容的修改
- Java线程的6种状态
- 不玩虚的!深入B端SaaS产品设计核心理念【干货】
- 45 岁以后的程序员都到哪里去了?
- Java常见面试题收集
- C++中strftime()的详细说明
- SQL Server2019 Express下载安装教程-——孟师傅
- openwrt定时任务 2021-10-08
- zblog火车头采集经验
- Elasticsearch:在华为大数据集群 从c80升级至651版本后,执行es命令修改配置失败
- 强大的 Stream 函数式编程
- Loadrunner执行https报错Action.c(7): Error -27778: SSL protocol error when attempting to connect with hos
- Java微信公众号开发之微信公众平台接入开发者
- XMind 2021 Mac 去水印教程
- Unity实战篇:实现虚拟摇杆控制人物(Easy Touch 5.X插件)