<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
Spring Retry提供了自動重新呼叫失敗的操作的功能。這在錯誤可能是暫時的(例如瞬時網路故障)的情況下很有用。 從2.2.0版本開始,重試功能已從Spring Batch中撤出,成為一個獨立的新庫:Spring Retry
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> <!-- also need to add Spring AOP into our project--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> </dependency>
在啟動類中使用@EnableRetry註解
package org.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.retry.annotation.EnableRetry; @SpringBootApplication @EnableRetry public class RetryApp { public static void main(String[] args) { SpringApplication.run(RetryApp.class, args); } }
需要在重試的程式碼中加入重試註解@Retryable
package org.example; import lombok.extern.slf4j.Slf4j; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Recover; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; import java.time.LocalDateTime; @Service @Slf4j public class RetryService { @Retryable(value = IllegalAccessException.class) public void service1() throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException("manual exception"); } }
預設情況下,會重試3次,間隔1秒
我們可以從註解@Retryable
中看到
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Retryable { /** * Retry interceptor bean name to be applied for retryable method. Is mutually * exclusive with other attributes. * @return the retry interceptor bean name */ String interceptor() default ""; * Exception types that are retryable. Synonym for includes(). Defaults to empty (and * if excludes is also empty all exceptions are retried). * @return exception types to retry Class<? extends Throwable>[] value() default {}; * Exception types that are retryable. Defaults to empty (and if excludes is also * empty all exceptions are retried). Class<? extends Throwable>[] include() default {}; * Exception types that are not retryable. Defaults to empty (and if includes is also * If includes is empty but excludes is not, all not excluded exceptions are retried * @return exception types not to retry Class<? extends Throwable>[] exclude() default {}; * A unique label for statistics reporting. If not provided the caller may choose to * ignore it, or provide a default. * * @return the label for the statistics String label() default ""; * Flag to say that the retry is stateful: i.e. exceptions are re-thrown, but the * retry policy is applied with the same policy to subsequent invocations with the * same arguments. If false then retryable exceptions are not re-thrown. * @return true if retry is stateful, default false boolean stateful() default false; * @return the maximum number of attempts (including the first failure), defaults to 3 int maxAttempts() default 3; //預設重試次數3次 * @return an expression evaluated to the maximum number of attempts (including the first failure), defaults to 3 * Overrides {@link #maxAttempts()}. * @since 1.2 String maxAttemptsExpression() default ""; * Specify the backoff properties for retrying this operation. The default is a * simple {@link Backoff} specification with no properties - see it's documentation * for defaults. * @return a backoff specification Backoff backoff() default @Backoff(); //預設的重試中的退避策略 * Specify an expression to be evaluated after the {@code SimpleRetryPolicy.canRetry()} * returns true - can be used to conditionally suppress the retry. Only invoked after * an exception is thrown. The root object for the evaluation is the last {@code Throwable}. * Other beans in the context can be referenced. * For example: * <pre class=code> * {@code "message.contains('you can retry this')"}. * </pre> * and * {@code "@someBean.shouldRetry(#root)"}. * @return the expression. String exceptionExpression() default ""; * Bean names of retry listeners to use instead of default ones defined in Spring context * @return retry listeners bean names String[] listeners() default {}; }
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Backoff { /** * Synonym for {@link #delay()}. * * @return the delay in milliseconds (default 1000) */ long value() default 1000; //預設的重試間隔1秒 * A canonical backoff period. Used as an initial value in the exponential case, and * as a minimum value in the uniform case. * @return the initial or canonical backoff period in milliseconds (default 1000) long delay() default 0; * The maximimum wait (in milliseconds) between retries. If less than the * {@link #delay()} then the default of * {@value org.springframework.retry.backoff.ExponentialBackOffPolicy#DEFAULT_MAX_INTERVAL} * is applied. * @return the maximum delay between retries (default 0 = ignored) long maxDelay() default 0; * If positive, then used as a multiplier for generating the next delay for backoff. * @return a multiplier to use to calculate the next backoff delay (default 0 = * ignored) double multiplier() default 0; * An expression evaluating to the canonical backoff period. Used as an initial value * in the exponential case, and as a minimum value in the uniform case. Overrides * {@link #delay()}. * @return the initial or canonical backoff period in milliseconds. * @since 1.2 String delayExpression() default ""; * An expression evaluating to the maximimum wait (in milliseconds) between retries. * If less than the {@link #delay()} then the default of * is applied. Overrides {@link #maxDelay()} String maxDelayExpression() default ""; * Evaluates to a vaule used as a multiplier for generating the next delay for * backoff. Overrides {@link #multiplier()}. * @return a multiplier expression to use to calculate the next backoff delay (default * 0 = ignored) String multiplierExpression() default ""; * In the exponential case ({@link #multiplier()} > 0) set this to true to have the * backoff delays randomized, so that the maximum delay is multiplier times the * previous delay and the distribution is uniform between the two values. * @return the flag to signal randomization is required (default false) boolean random() default false; }
我們來執行測試程式碼
package org.example; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class RetryServiceTest { @Autowired private RetryService retryService; @Test void testService1() throws IllegalAccessException { retryService.service1(); } }
執行結果如下:
2021-01-05 19:40:41.221 INFO 3548 --- [ main] org.example.RetryService : do something... 2021-01-05T19:40:41.221763300
2021-01-05 19:40:42.224 INFO 3548 --- [ main] org.example.RetryService : do something... 2021-01-05T19:40:42.224436500
2021-01-05 19:40:43.225 INFO 3548 --- [ main] org.example.RetryService : do something... 2021-01-05T19:40:43.225189300
java.lang.IllegalAccessException: manual exception at org.example.RetryService.service1(RetryService.java:19) at org.example.RetryService$$FastClassBySpringCGLIB$$c0995ddb.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:769) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747) at org.springframework.retry.interceptor.RetryOperationsInterceptor$1.doWithRetry(RetryOperationsInterceptor.java:91) at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:287) at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:164) at org.springframework.retry.interceptor.RetryOperationsInterceptor.invoke(RetryOperationsInterceptor.java:118) at org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor.invoke(AnnotationAwareRetryOperationsInterceptor.java:153) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689) at org.example.RetryService$$EnhancerBySpringCGLIB$$499afa1d.service1(<generated>) at org.example.RetryServiceTest.testService1(RetryServiceTest.java:16) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:675) at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:125) at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:132) at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:124) at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:74) at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115) at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105) at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:104) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:62) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:43) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:35) at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104) at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:202) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:198) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80) at java.base/java.util.ArrayList.forEach(ArrayList.java:1540) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32) at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:229) at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:197) at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:211) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128) at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
可以看到重新執行了3次service1()
方法,然後間隔是1秒,然後最後還是重試失敗,所以丟擲了異常
既然我們看到了註解@Retryable
中有這麼多引數可以設定,那我們就來介紹幾個常用的設定。
@Retryable(include = IllegalAccessException.class, maxAttempts = 5) public void service2() throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException("manual exception"); }
首先是maxAttempts
,用於設定重試次數
2021-01-06 09:30:11.263 INFO 15612 --- [ main] org.example.RetryService : do something... 2021-01-06T09:30:11.263621900
2021-01-06 09:30:12.265 INFO 15612 --- [ main] org.example.RetryService : do something... 2021-01-06T09:30:12.265629100
2021-01-06 09:30:13.265 INFO 15612 --- [ main] org.example.RetryService : do something... 2021-01-06T09:30:13.265701
2021-01-06 09:30:14.266 INFO 15612 --- [ main] org.example.RetryService : do something... 2021-01-06T09:30:14.266705400
2021-01-06 09:30:15.266 INFO 15612 --- [ main] org.example.RetryService : do something... 2021-01-06T09:30:15.266733200java.lang.IllegalAccessException: manual exception
....
從執行結果可以看到,方法執行了5次。
下面來介紹maxAttemptsExpression
的設定
@Retryable(value = IllegalAccessException.class, maxAttemptsExpression = "${maxAttempts}") public void service3() throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException("manual exception"); }
maxAttemptsExpression
則可以使用表示式,比如上述就是通過獲取設定中maxAttempts的值,我們可以在application.yml設定。上述其實省略掉了SpEL表示式#{....}
,執行結果的話可以發現方法執行了4次..
maxAttempts: 4
我們可以使用SpEL表示式
@Retryable(value = IllegalAccessException.class, maxAttemptsExpression = "#{1+1}") public void service3_1() throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException("manual exception"); } @Retryable(value = IllegalAccessException.class, maxAttemptsExpression = "#{${maxAttempts}}")//效果和上面的一樣 public void service3_2() throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException("manual exception"); }
接著我們下面來看看exceptionExpression
, 一樣也是寫SpEL表示式
@Retryable(value = IllegalAccessException.class, exceptionExpression = "message.contains('test')") public void service4(String exceptionMessage) throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException(exceptionMessage); } @Retryable(value = IllegalAccessException.class, exceptionExpression = "#{message.contains('test')}") public void service4_3(String exceptionMessage) throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException(exceptionMessage); }
上面的表示式exceptionExpression = "message.contains('test')"
的作用其實是獲取到丟擲來exception的message(呼叫了getMessage()
方法),然後判斷message的內容裡面是否包含了test
字串,如果包含的話就會執行重試。所以如果呼叫方法的時候傳入的引數exceptionMessage
中包含了test
字串的話就會執行重試。
但這裡值得注意的是, Spring Retry 1.2.5之後exceptionExpression
是可以省略掉#{...}
Since Spring Retry 1.2.5, for exceptionExpression
, templated expressions (#{...}
) are deprecated in favor of simple expression strings (message.contains('this can be retried')
).
使用1.2.5之後的版本執行是沒有問題的
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>1.3.0</version> </dependency>
但是如果使用1.2.5版本之前包括1.2.5版本的話,執行的時候會報錯如下:
2021-01-06 09:52:45.209 INFO 23220 --- [ main] org.example.RetryService : do something... 2021-01-06T09:52:45.209178200
org.springframework.expression.spel.SpelEvaluationException: EL1001E: Type conversion problem, cannot convert from java.lang.String to java.lang.Boolean
at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:75)
at org.springframework.expression.common.ExpressionUtils.convertTypedValue(ExpressionUtils.java:57)
at org.springframework.expression.common.LiteralExpression.getValue(LiteralExpression.java:106)
at org.springframework.retry.policy.ExpressionRetryPolicy.canRetry(ExpressionRetryPolicy.java:113)
at org.springframework.retry.support.RetryTemplate.canRetry(RetryTemplate.java:375)
at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:304)
at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:164)
at org.springframework.retry.interceptor.RetryOperationsInterceptor.invoke(RetryOperationsInterceptor.java:118)
at org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor.invoke(AnnotationAwareRetryOperationsInterceptor.java:153)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
at org.example.RetryService$$EnhancerBySpringCGLIB$$d321a75e.service4(<generated>)
at org.example.RetryServiceTest.testService4_2(RetryServiceTest.java:46)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:686)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:212)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:208)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:137)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:71)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:248)
at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$5(DefaultLauncher.java:211)
at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:226)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:199)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:132)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Boolean] for value 'message.contains('test')'; nested exception is java.lang.IllegalArgumentException: Invalid boolean value 'message.contains('test')'
at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:47)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:191)
at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:70)
... 76 more
Caused by: java.lang.IllegalArgumentException: Invalid boolean value 'message.contains('test')'
at org.springframework.core.convert.support.StringToBooleanConverter.convert(StringToBooleanConverter.java:63)
at org.springframework.core.convert.support.StringToBooleanConverter.convert(StringToBooleanConverter.java:31)
at org.springframework.core.convert.support.GenericConversionService$ConverterAdapter.convert(GenericConversionService.java:385)
at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41)
... 78 more
還可以在表示式中執行一個方法,前提是方法的類在spring容器中註冊了,@retryService
其實就是獲取bean name為retryService
的bean,然後呼叫裡面的checkException
方法,傳入的引數為#root
,它其實就是丟擲來的exception物件。一樣的也是可以省略#{...}
@Retryable(value = IllegalAccessException.class, exceptionExpression = "#{@retryService.checkException(#root)}") public void service5(String exceptionMessage) throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException(exceptionMessage); } @Retryable(value = IllegalAccessException.class, exceptionExpression = "@retryService.checkException(#root)") public void service5_1(String exceptionMessage) throws IllegalAccessException { public boolean checkException(Exception e) { log.error("error message:{}", e.getMessage()); return true; //返回true的話表明會執行重試,如果返回false則不會執行重試
執行結果:
2021-01-06 13:33:52.913 INFO 23052 --- [ main] org.example.RetryService : do something... 2021-01-06T13:33:52.913404
2021-01-06 13:33:52.981 ERROR 23052 --- [ main] org.example.RetryService : error message:test message
2021-01-06 13:33:53.990 ERROR 23052 --- [ main] org.example.RetryService : error message:test message
2021-01-06 13:33:53.990 INFO 23052 --- [ main] org.example.RetryService : do something... 2021-01-06T13:33:53.990947400
2021-01-06 13:33:53.990 ERROR 23052 --- [ main] org.example.RetryService : error message:test message
2021-01-06 13:33:54.992 ERROR 23052 --- [ main] org.example.RetryService : error message:test message
2021-01-06 13:33:54.992 INFO 23052 --- [ main] org.example.RetryService : do something... 2021-01-06T13:33:54.992342900
當然還有更多表示式的用法了...
@Retryable(exceptionExpression = "#{#root instanceof T(java.lang.IllegalAccessException)}") //判斷exception的型別 public void service5_2(String exceptionMessage) { log.info("do something... {}", LocalDateTime.now()); throw new NullPointerException(exceptionMessage); } @Retryable(exceptionExpression = "#root instanceof T(java.lang.IllegalAccessException)") public void service5_3(String exceptionMessage) { log.info("do something... {}", LocalDateTime.now()); throw new NullPointerException(exceptionMessage); }
@Retryable(exceptionExpression = "myMessage.contains('test')") //檢視自定義的MyException中的myMessage的值是否包含test字串 public void service5_4(String exceptionMessage) throws MyException { log.info("do something... {}", LocalDateTime.now()); throw new MyException(exceptionMessage); //自定義的exception } @Retryable(exceptionExpression = "#root.myMessage.contains('test')") //和上面service5_4方法的效果一樣 public void service5_5(String exceptionMessage) throws MyException { log.info("do something... {}", LocalDateTime.now()); throw new MyException(exceptionMessage); }
package org.example; import lombok.Getter; import lombok.Setter; @Getter @Setter public class MyException extends Exception { private String myMessage; public MyException(String myMessage) { this.myMessage = myMessage; } }
下面再來看看另一個設定exclude
@Retryable(exclude = MyException.class) public void service6(String exceptionMessage) throws MyException { log.info("do something... {}", LocalDateTime.now()); throw new MyException(exceptionMessage); }
這個exclude
屬性可以幫我們排除一些我們不想重試的異常
最後我們來看看這個backoff
重試等待策略, 預設使用@Backoff
註解。
我們先來看看這個@Backoff
的value
屬性,用於設定重試間隔
@Retryable(value = IllegalAccessException.class, backoff = @Backoff(value = 2000)) public void service7() throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException(); }
執行結果可以看出來重試的間隔為2秒
2021-01-06 14:47:38.036 INFO 21116 --- [ main] org.example.RetryService : do something... 2021-01-06T14:47:38.036732600
2021-01-06 14:47:40.038 INFO 21116 --- [ main] org.example.RetryService : do something... 2021-01-06T14:47:40.037753600
2021-01-06 14:47:42.046 INFO 21116 --- [ main] org.example.RetryService : do something... 2021-01-06T14:47:42.046642900
java.lang.IllegalAccessException
at org.example.RetryService.service7(RetryService.java:113)
...
接下來介紹@Backoff
的delay
屬性,它與value
屬性不能共存,當delay
不設定的時候會去讀value
屬性設定的值,如果delay
設定的話則會忽略value
屬性
@Retryable(value = IllegalAccessException.class, backoff = @Backoff(value = 2000,delay = 500)) public void service8() throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException(); }
執行結果可以看出,重試的時間間隔為500ms
2021-01-06 15:22:42.271 INFO 13512 --- [ main] org.example.RetryService : do something... 2021-01-06T15:22:42.271504800
2021-01-06 15:22:42.772 INFO 13512 --- [ main] org.example.RetryService : do something... 2021-01-06T15:22:42.772234900
2021-01-06 15:22:43.273 INFO 13512 --- [ main] org.example.RetryService : do something... 2021-01-06T15:22:43.273246700
java.lang.IllegalAccessException
at org.example.RetryService.service8(RetryService.java:121)
接下來我們來看``@Backoff的
multiplier`的屬性, 指定延遲倍數, 預設為0。
@Retryable(value = IllegalAccessException.class,maxAttempts = 4, backoff = @Backoff(delay = 2000, multiplier = 2)) public void service9() throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException(); }
multiplier
設定為2,則表示第一次重試間隔為2s,第二次為4秒,第三次為8s
執行結果如下:
2021-01-06 15:58:07.458 INFO 23640 --- [ main] org.example.RetryService : do something... 2021-01-06T15:58:07.458245500
2021-01-06 15:58:09.478 INFO 23640 --- [ main] org.example.RetryService : do something... 2021-01-06T15:58:09.478681300
2021-01-06 15:58:13.478 INFO 23640 --- [ main] org.example.RetryService : do something... 2021-01-06T15:58:13.478921900
2021-01-06 15:58:21.489 INFO 23640 --- [ main] org.example.RetryService : do something... 2021-01-06T15:58:21.489240600
java.lang.IllegalAccessException
at org.example.RetryService.service9(RetryService.java:128)
...
接下來我們來看看這個@Backoff
的maxDelay
屬性,設定最大的重試間隔,當超過這個最大的重試間隔的時候,重試的間隔就等於maxDelay
的值
@Retryable(value = IllegalAccessException.class,maxAttempts = 4, backoff = @Backoff(delay = 2000, multiplier = 2,maxDelay = 5000)) public void service10() throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException(); }
執行結果:
2021-01-06 16:12:37.377 INFO 5024 --- [ main] org.example.RetryService : do something... 2021-01-06T16:12:37.377616100
2021-01-06 16:12:39.381 INFO 5024 --- [ main] org.example.RetryService : do something... 2021-01-06T16:12:39.381299400
2021-01-06 16:12:43.382 INFO 5024 --- [ main] org.example.RetryService : do something... 2021-01-06T16:12:43.382169500
2021-01-06 16:12:48.396 INFO 5024 --- [ main] org.example.RetryService : do something... 2021-01-06T16:12:48.396327600
java.lang.IllegalAccessException
at org.example.RetryService.service10(RetryService.java:135)
可以最後的最大重試間隔是5秒
當@Retryable
方法重試失敗之後,最後就會呼叫@Recover
方法。用於@Retryable
失敗時的“兜底”處理方法。 @Recover
的方法必須要與@Retryable
註解的方法保持一致,第一入參為要重試的異常,其他引數與@Retryable
保持一致,返回值也要一樣,否則無法執行!
@Retryable(value = IllegalAccessException.class) public void service11() throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException(); } @Recover public void recover11(IllegalAccessException e) { log.info("service retry after Recover => {}", e.getMessage()); //========================= @Retryable(value = ArithmeticException.class) public int service12() throws IllegalAccessException { return 1 / 0; public int recover12(ArithmeticException e) { return 0; public int service13(String message) throws IllegalAccessException { log.info("do something... {},{}", message, LocalDateTime.now()); public int recover13(ArithmeticException e, String message) { log.info("{},service retry after Recover => {}", message, e.getMessage());
熔斷模式:指在具體的重試機制下失敗後開啟斷路器,過了一段時間,斷路器進入半開狀態,允許一個進入重試,若失敗再次進入斷路器,成功則關閉斷路器,註解為@CircuitBreaker
,具體包括熔斷開啟時間、重置過期時間
// openTimeout時間範圍內失敗maxAttempts次數後,熔斷開啟resetTimeout時長 @CircuitBreaker(openTimeout = 1000, resetTimeout = 3000, value = NullPointerException.class) public void circuitBreaker(int num) { log.info(" 進入斷路器方法num={}", num); if (num > 8) return; Integer n = null; System.err.println(1 / n); } @Recover public void recover(NullPointerException e) { log.info("service retry after Recover => {}", e.getMessage()); }
測試方法
@Test public void testCircuitBreaker() throws InterruptedException { System.err.println("嘗試進入斷路器方法,並觸發異常..."); retryService.circuitBreaker(1); retryService.circuitBreaker(1); retryService.circuitBreaker(9); retryService.circuitBreaker(9); System.err.println("在openTimeout 1秒之內重試次數為2次,未達到觸發熔斷, 斷路器依然閉合..."); TimeUnit.SECONDS.sleep(1); System.err.println("超過openTimeout 1秒之後, 因為未觸發熔斷,所以重試次數重置,可以正常存取...,繼續重試3次方法..."); retryService.circuitBreaker(1); retryService.circuitBreaker(1); retryService.circuitBreaker(1); System.err.println("在openTimeout 1秒之內重試次數為3次,達到觸發熔斷,不會執行重試,只會執行恢復方法..."); retryService.circuitBreaker(1); TimeUnit.SECONDS.sleep(2); retryService.circuitBreaker(9); TimeUnit.SECONDS.sleep(3); System.err.println("超過resetTimeout 3秒之後,斷路器重新閉合...,可以正常存取"); retryService.circuitBreaker(9); retryService.circuitBreaker(9); retryService.circuitBreaker(9); retryService.circuitBreaker(9); retryService.circuitBreaker(9); }
執行結果:
嘗試進入斷路器方法,並觸發異常...
2021-01-07 21:44:20.842 INFO 7464 --- [ main] org.example.RetryService : 進入斷路器方法num=1
2021-01-07 21:44:20.844 INFO 7464 --- [ main] org.example.RetryService : service retry after Recover => null
2021-01-07 21:44:20.845 INFO 7464 --- [ main] org.example.RetryService : 進入斷路器方法num=1
2021-01-07 21:44:20.845 INFO 7464 --- [ main] org.example.RetryService : service retry after Recover => null
2021-01-07 21:44:20.845 INFO 7464 --- [ main] org.example.RetryService : 進入斷路器方法num=9
2021-01-07 21:44:20.845 INFO 7464 --- [ main] org.example.RetryService : 進入斷路器方法num=9
在openTimeout 1秒之內重試次數為2次,未達到觸發熔斷, 斷路器依然閉合...
超過openTimeout 1秒之後, 因為未觸發熔斷,所以重試次數重置,可以正常存取...,繼續重試3次方法...
2021-01-07 21:44:21.846 INFO 7464 --- [ main] org.example.RetryService : 進入斷路器方法num=1
2021-01-07 21:44:21.847 INFO 7464 --- [ main] org.example.RetryService : service retry after Recover => null
2021-01-07 21:44:21.847 INFO 7464 --- [ main] org.example.RetryService : 進入斷路器方法num=1
2021-01-07 21:44:21.847 INFO 7464 --- [ main] org.example.RetryService : service retry after Recover => null
2021-01-07 21:44:21.847 INFO 7464 --- [ main] org.example.RetryService : 進入斷路器方法num=1
2021-01-07 21:44:21.848 INFO 7464 --- [ main] org.example.RetryService : service retry after Recover => null
在openTimeout 1秒之內重試次數為3次,達到觸發熔斷,不會執行重試,只會執行恢復方法...
2021-01-07 21:44:21.848 INFO 7464 --- [ main] org.example.RetryService : service retry after Recover => null
2021-01-07 21:44:23.853 INFO 7464 --- [ main] org.example.RetryService : service retry after Recover => null
超過resetTimeout 3秒之後,斷路器重新閉合...,可以正常存取
2021-01-07 21:44:26.853 INFO 7464 --- [ main] org.example.RetryService : 進入斷路器方法num=9
2021-01-07 21:44:26.854 INFO 7464 --- [ main] org.example.RetryService : 進入斷路器方法num=9
2021-01-07 21:44:26.855 INFO 7464 --- [ main] org.example.RetryService : 進入斷路器方法num=9
2021-01-07 21:44:26.855 INFO 7464 --- [ main] org.example.RetryService : 進入斷路器方法num=9
2021-01-07 21:44:26.856 INFO 7464 --- [ main] org.example.RetryService : 進入斷路器方法num=9
package org.example; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.retry.backoff.FixedBackOffPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; @Configuration public class AppConfig { @Bean public RetryTemplate retryTemplate() { RetryTemplate retryTemplate = new RetryTemplate(); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); //設定重試策略 retryPolicy.setMaxAttempts(2); retryTemplate.setRetryPolicy(retryPolicy); FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy(); //設定退避策略 fixedBackOffPolicy.setBackOffPeriod(2000L); retryTemplate.setBackOffPolicy(fixedBackOffPolicy); return retryTemplate; } }
可以看到這些設定跟我們直接寫註解的方式是差不多的,這裡就不過多的介紹了。。
package org.example; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; import org.springframework.retry.support.RetryTemplate; @SpringBootTest public class RetryTemplateTest { @Autowired private RetryTemplate retryTemplate; private RetryTemplateService retryTemplateService; @Test void test1() throws IllegalAccessException { retryTemplate.execute(new RetryCallback<Object, IllegalAccessException>() { @Override public Object doWithRetry(RetryContext context) throws IllegalAccessException { retryTemplateService.service1(); return null; } }); } void test2() throws IllegalAccessException { retryTemplateService.service1(); }, new RecoveryCallback<Object>() { public Object recover(RetryContext context) throws Exception { log.info("RecoveryCallback...."); }
RetryOperations
定義重試的API,RetryTemplate
是API的模板模式實現,實現了重試和熔斷。提供的API如下:
package org.springframework.retry; import org.springframework.retry.support.DefaultRetryState; /** * Defines the basic set of operations implemented by {@link RetryOperations} to execute * operations with configurable retry behaviour. * * @author Rob Harrop * @author Dave Syer */ public interface RetryOperations { /** * Execute the supplied {@link RetryCallback} with the configured retry semantics. See * implementations for configuration details. * @param <T> the return value * @param retryCallback the {@link RetryCallback} * @param <E> the exception to throw * @return the value returned by the {@link RetryCallback} upon successful invocation. * @throws E any {@link Exception} raised by the {@link RetryCallback} upon * unsuccessful retry. * @throws E the exception thrown */ <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E; * Execute the supplied {@link RetryCallback} with a fallback on exhausted retry to * the {@link RecoveryCallback}. See implementations for configuration details. * @param recoveryCallback the {@link RecoveryCallback} * @param retryCallback the {@link RetryCallback} {@link RecoveryCallback} upon * @param <T> the type to return * @param <E> the type of the exception * @return the value returned by the {@link RetryCallback} upon successful invocation, * and that returned by the {@link RecoveryCallback} otherwise. * @throws E any {@link Exception} raised by the unsuccessful retry. <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback) throws E; * A simple stateful retry. Execute the supplied {@link RetryCallback} with a target * object for the attempt identified by the {@link DefaultRetryState}. Exceptions * thrown by the callback are always propagated immediately so the state is required * to be able to identify the previous attempt, if there is one - hence the state is * required. Normal patterns would see this method being used inside a transaction, * where the callback might invalidate the transaction if it fails. * * See implementations for configuration details. * @param retryState the {@link RetryState} * @param <T> the type of the return value * @param <E> the type of the exception to return * @throws E any {@link Exception} raised by the {@link RecoveryCallback}. * @throws ExhaustedRetryException if the last attempt for this state has already been * reached <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RetryState retryState) throws E, ExhaustedRetryException; * A stateful retry with a recovery path. Execute the supplied {@link RetryCallback} * with a fallback on exhausted retry to the {@link RecoveryCallback} and a target * object for the retry attempt identified by the {@link DefaultRetryState}. * @param <T> the return value type * @param <E> the exception type * @see #execute(RetryCallback, RetryState) * @throws E any {@link Exception} raised by the {@link RecoveryCallback} upon <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback, RetryState retryState) throws E; }
下面主要介紹一下RetryTemplate設定的時候,需要設定的重試策略和退避策略
RetryPolicy是一個介面, 然後有很多具體的實現,我們先來看看它的介面中定義了什麼方法
package org.springframework.retry; import java.io.Serializable; /** * A {@link RetryPolicy} is responsible for allocating and managing resources needed by * {@link RetryOperations}. The {@link RetryPolicy} allows retry operations to be aware of * their context. Context can be internal to the retry framework, e.g. to support nested * retries. Context can also be external, and the {@link RetryPolicy} provides a uniform * API for a range of different platforms for the external context. * * @author Dave Syer */ public interface RetryPolicy extends Serializable { /** * @param context the current retry status * @return true if the operation can proceed */ boolean canRetry(RetryContext context); * Acquire resources needed for the retry operation. The callback is passed in so that * marker interfaces can be used and a manager can collaborate with the callback to * set up some state in the status token. * @param parent the parent context if we are in a nested retry. * @return a {@link RetryContext} object specific to this policy. * RetryContext open(RetryContext parent); * @param context a retry status created by the {@link #open(RetryContext)} method of * this policy. void close(RetryContext context); * Called once per retry attempt, after the callback fails. * @param context the current status object. * @param throwable the exception to throw void registerThrowable(RetryContext context, Throwable throwable); }
我們來看看他有什麼具體的實現類
看一下退避策略,退避是指怎麼去做下一次的重試,在這裡其實就是等待多長時間。
listener可以監聽重試,並執行對應的回撥方法
package org.springframework.retry; /** * Interface for listener that can be used to add behaviour to a retry. Implementations of * {@link RetryOperations} can chose to issue callbacks to an interceptor during the retry * lifecycle. * * @author Dave Syer */ public interface RetryListener { /** * Called before the first attempt in a retry. For instance, implementers can set up * state that is needed by the policies in the {@link RetryOperations}. The whole * retry can be vetoed by returning false from this method, in which case a * {@link TerminatedRetryException} will be thrown. * @param <T> the type of object returned by the callback * @param <E> the type of exception it declares may be thrown * @param context the current {@link RetryContext}. * @param callback the current {@link RetryCallback}. * @return true if the retry should proceed. */ <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback); * Called after the final attempt (successful or not). Allow the interceptor to clean * up any resource it is holding before control returns to the retry caller. * @param throwable the last exception that was thrown by the callback. * @param <E> the exception type * @param <T> the return value <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable); * Called after every unsuccessful attempt at a retry. * @param <E> the exception to throw <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable); }
使用如下:
自定義一個Listener
package org.example; import lombok.extern.slf4j.Slf4j; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; import org.springframework.retry.listener.RetryListenerSupport; @Slf4j public class DefaultListenerSupport extends RetryListenerSupport { @Override public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { log.info("onClose"); super.close(context, callback, throwable); } public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { log.info("onError"); super.onError(context, callback, throwable); public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) { log.info("onOpen"); return super.open(context, callback); }
把listener設定到retryTemplate中
package org.example; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.retry.backoff.FixedBackOffPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; @Configuration @Slf4j public class AppConfig { @Bean public RetryTemplate retryTemplate() { RetryTemplate retryTemplate = new RetryTemplate(); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); //設定重試策略 retryPolicy.setMaxAttempts(2); retryTemplate.setRetryPolicy(retryPolicy); FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy(); //設定退避策略 fixedBackOffPolicy.setBackOffPeriod(2000L); retryTemplate.setBackOffPolicy(fixedBackOffPolicy); retryTemplate.registerListener(new DefaultListenerSupport()); //設定retryListener return retryTemplate; } }
測試結果:
2021-01-08 10:48:05.663 INFO 20956 --- [ main] org.example.DefaultListenerSupport : onOpen
2021-01-08 10:48:05.663 INFO 20956 --- [ main] org.example.RetryTemplateService : do something...
2021-01-08 10:48:05.663 INFO 20956 --- [ main] org.example.DefaultListenerSupport : onError
2021-01-08 10:48:07.664 INFO 20956 --- [ main] org.example.RetryTemplateService : do something...
2021-01-08 10:48:07.664 INFO 20956 --- [ main] org.example.DefaultListenerSupport : onError
2021-01-08 10:48:07.664 INFO 20956 --- [ main] org.example.RetryTemplateTest : RecoveryCallback....
2021-01-08 10:48:07.664 INFO 20956 --- [ main] org.example.DefaultListenerSupport : onClose
usage-of-exceptionexpression-in-spring-retry
到此這篇關於Spring Boot中使用Spring-Retry重試框架的文章就介紹到這了,更多相關Spring Boot使用Spring-Retry內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45