sisyphus 综合了 spring-retry 和 gauva-retrying 的优势,使用起来也非常灵活。

今天,让我们一起看一下西西弗斯背后的故事。

情景导入

简单的需求

产品经理:实现一个按条件,查询用户信息的服务。

小明:好的。没问题。

代码

  • UserService.java
public interface UserService {

    /**
* 根据条件查询用户信息
* @param condition 条件
* @return User 信息
*/
User queryUser(QueryUserCondition condition); }
  • UserServiceImpl.java
public class UserServiceImpl implements UserService {

    private OutService outService;

    public UserServiceImpl(OutService outService) {
this.outService = outService;
} @Override
public User queryUser(QueryUserCondition condition) {
outService.remoteCall();
return new User();
} }

谈话

项目经理:这个服务有时候会失败,你看下。

小明:OutService 在是一个 RPC 的外部服务,但是有时候不稳定。

项目经理:如果调用失败了,你可以调用的时候重试几次。你去看下重试相关的东西

重试

重试作用

对于重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。

远程调用超时、网络突然中断可以重试。在微服务治理框架中,通常都有自己的重试与超时配置,比如dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。

比如外部 RPC 调用,或者数据入库等操作,如果一次操作失败,可以进行多次重试,提高调用成功的可能性

V1.0 支持重试版本

思考

小明:我手头还有其他任务,这个也挺简单的。5 分钟时间搞定他。

实现

  • UserServiceRetryImpl.java
public class UserServiceRetryImpl implements UserService {

    @Override
public User queryUser(QueryUserCondition condition) {
int times = 0;
OutService outService = new AlwaysFailOutServiceImpl(); while (times < RetryConstant.MAX_TIMES) {
try {
outService.remoteCall();
return new User();
} catch (Exception e) {
times++; if(times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
} return null;
} }

V1.1 代理模式版本

易于维护

项目经理:你的代码我看了,功能虽然实现了,但是尽量写的易于维护一点。

小明:好的。(心想,是说要写点注释什么的?)

代理模式

为其他对象提供一种代理以控制对这个对象的访问。

在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介作用。

其特征是代理与委托类有同样的接口。

实现

小明想到以前看过的代理模式,心想用这种方式,原来的代码改动量较少,以后想改起来也方便些

  • UserServiceProxyImpl.java
public class UserServiceProxyImpl implements UserService {

    private UserService userService = new UserServiceImpl();

    @Override
public User queryUser(QueryUserCondition condition) {
int times = 0; while (times < RetryConstant.MAX_TIMES) {
try {
return userService.queryUser(condition);
} catch (Exception e) {
times++; if(times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
}
return null;
} }

V1.2 动态代理模式

方便拓展

项目经理:小明啊,这里还有个方法也是同样的问题。你也给加上重试吧。

小明:好的。

小明心想,我在写一个代理,但是转念冷静了下来,如果还有个服务也要重试怎么办呢?

  • RoleService.java
public interface RoleService {

    /**
* 查询
* @param user 用户信息
* @return 是否拥有权限
*/
boolean hasPrivilege(User user); }

代码实现

  • DynamicProxy.java
public class DynamicProxy implements InvocationHandler {

    private final Object subject;

    public DynamicProxy(Object subject) {
this.subject = subject;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
int times = 0; while (times < RetryConstant.MAX_TIMES) {
try {
// 当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
return method.invoke(subject, args);
} catch (Exception e) {
times++; if (times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
} return null;
} /**
* 获取动态代理
*
* @param realSubject 代理对象
*/
public static Object getProxy(Object realSubject) {
// 我们要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法的
InvocationHandler handler = new DynamicProxy(realSubject);
return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(), handler);
} }
  • 测试代码
@Test
public void failUserServiceTest() {
UserService realService = new UserServiceImpl();
UserService proxyService = (UserService) DynamicProxy.getProxy(realService); User user = proxyService.queryUser(new QueryUserCondition());
LOGGER.info("failUserServiceTest: " + user);
} @Test
public void roleServiceTest() {
RoleService realService = new RoleServiceImpl();
RoleService proxyService = (RoleService) DynamicProxy.getProxy(realService); boolean hasPrivilege = proxyService.hasPrivilege(new User());
LOGGER.info("roleServiceTest: " + hasPrivilege);
}

V1.3 动态代理模式增强

对话

项目经理:小明,你动态代理的方式是挺会偷懒的,可是我们有的类没有接口。这个问题你要解决一下。

小明:好的。(谁?写服务竟然不定义接口)

  • ResourceServiceImpl.java
public class ResourceServiceImpl {

    /**
* 校验资源信息
* @param user 入参
* @return 是否校验通过
*/
public boolean checkResource(User user) {
OutService outService = new AlwaysFailOutServiceImpl();
outService.remoteCall();
return true;
} }

字节码技术

小明看了下网上的资料,解决的办法还是有的。

  • CGLIB

CGLIB 是一个功能强大、高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。

  • javassist

javassist (Java编程助手)使Java字节码操作变得简单。

它是Java中编辑字节码的类库;它允许Java程序在运行时定义新类,并在JVM加载类文件时修改类文件。

与其他类似的字节码编辑器不同,Javassist提供了两个级别的API:源级和字节码级。

如果用户使用源代码级API,他们可以编辑类文件,而不需要了解Java字节码的规范。

整个API只使用Java语言的词汇表进行设计。您甚至可以以源文本的形式指定插入的字节码;Javassist动态编译它。

另一方面,字节码级API允许用户直接编辑类文件作为其他编辑器。

  • ASM

ASM 是一个通用的Java字节码操作和分析框架。

它可以用来修改现有的类或动态地生成类,直接以二进制形式。

ASM提供了一些通用的字节码转换和分析算法,可以从这些算法中构建自定义复杂的转换和代码分析工具。

ASM提供与其他Java字节码框架类似的功能,但主要关注性能。

因为它的设计和实现都尽可能地小和快,所以非常适合在动态系统中使用(当然也可以以静态的方式使用,例如在编译器中)。

实现

小明看了下,就选择使用 CGLIB。

  • CglibProxy.java
public class CglibProxy implements MethodInterceptor {

    @Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
int times = 0; while (times < RetryConstant.MAX_TIMES) {
try {
//通过代理子类调用父类的方法
return methodProxy.invokeSuper(o, objects);
} catch (Exception e) {
times++; if (times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
} return null;
} /**
* 获取代理类
* @param clazz 类信息
* @return 代理类结果
*/
public Object getProxy(Class clazz){
Enhancer enhancer = new Enhancer();
//目标对象类
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
//通过字节码技术创建目标对象类的子类实例作为代理
return enhancer.create();
} }
  • 测试
@Test
public void failUserServiceTest() {
UserService proxyService = (UserService) new CglibProxy().getProxy(UserServiceImpl.class); User user = proxyService.queryUser(new QueryUserCondition());
LOGGER.info("failUserServiceTest: " + user);
} @Test
public void resourceServiceTest() {
ResourceServiceImpl proxyService = (ResourceServiceImpl) new CglibProxy().getProxy(ResourceServiceImpl.class);
boolean result = proxyService.checkResource(new User());
LOGGER.info("resourceServiceTest: " + result);
}

V2.0 AOP 实现

对话

项目经理:小明啊,最近我在想一个问题。不同的服务,重试的时候次数应该是不同的。因为服务对稳定性的要求各不相同啊。

小明:好的。(心想,重试都搞了一周了,今天都周五了。)

下班之前,小明一直在想这个问题。刚好周末,花点时间写个重试小工具吧。

设计思路

  • 技术支持

spring

java 注解

  • 注解定义

注解可在方法上使用,定义需要重试的次数

  • 注解解析

拦截指定需要重试的方法,解析对应的重试次数,然后进行对应次数的重试。

实现

  • Retryable.java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable { /**
* Exception type that are retryable.
* @return exception type to retry
*/
Class<? extends Throwable> value() default RuntimeException.class; /**
* 包含第一次失败
* @return the maximum number of attempts (including the first failure), defaults to 3
*/
int maxAttempts() default 3; }
  • RetryAspect.java
@Aspect
@Component
public class RetryAspect { @Pointcut("execution(public * com.github.houbb.retry.aop..*.*(..)) &&" +
"@annotation(com.github.houbb.retry.aop.annotation.Retryable)")
public void myPointcut() {
} @Around("myPointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Method method = getCurrentMethod(point);
Retryable retryable = method.getAnnotation(Retryable.class); //1. 最大次数判断
int maxAttempts = retryable.maxAttempts();
if (maxAttempts <= 1) {
return point.proceed();
} //2. 异常处理
int times = 0;
final Class<? extends Throwable> exceptionClass = retryable.value();
while (times < maxAttempts) {
try {
return point.proceed();
} catch (Throwable e) {
times++; // 超过最大重试次数 or 不属于当前处理异常
if (times >= maxAttempts ||
!e.getClass().isAssignableFrom(exceptionClass)) {
throw new Throwable(e);
}
}
} return null;
} private Method getCurrentMethod(ProceedingJoinPoint point) {
try {
Signature sig = point.getSignature();
MethodSignature msig = (MethodSignature) sig;
Object target = point.getTarget();
return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
} }

方法的使用

  • fiveTimes()

当前方法一共重试 5 次。

重试条件:服务抛出 AopRuntimeExption

@Override
@Retryable(maxAttempts = 5, value = AopRuntimeExption.class)
public void fiveTimes() {
LOGGER.info("fiveTimes called!");
throw new AopRuntimeExption();
}
  • 测试日志
2018-08-08 15:49:33.814  INFO  [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called! java.lang.reflect.UndeclaredThrowableException
...

V3.0 spring-retry 版本

对话

周一来到公司,项目经理又和小明谈了起来。

项目经理:重试次数是满足了,但是重试其实应该讲究策略。比如调用外部,第一次失败,可以等待 5S 在次调用,如果又失败了,可以等待 10S 再调用。。。

小明:了解。

思考

可是今天周一,还有其他很多事情要做。

小明在想,没时间写这个呀。看看网上有没有现成的。

spring-retry

Spring Retry 为 Spring 应用程序提供了声明性重试支持。 它用于Spring批处理、Spring集成、Apache Hadoop(等等)的Spring。

在分布式系统中,为了保证数据分布式事务的强一致性,大家在调用RPC接口或者发送MQ时,针对可能会出现网络抖动请求超时情况采取一下重试操作。 大家用的最多的重试方式就是MQ了,但是如果你的项目中没有引入MQ,那就不方便了。

还有一种方式,是开发者自己编写重试机制,但是大多不够优雅。

注解式使用

  • RemoteService.java

重试条件:遇到 RuntimeException

重试次数:3

重试策略:重试的时候等待 5S, 后面时间依次变为原来的 2 倍数。

熔断机制:全部重试失败,则调用 recover() 方法。

@Service
public class RemoteService { private static final Logger LOGGER = LoggerFactory.getLogger(RemoteService.class); /**
* 调用方法
*/
@Retryable(value = RuntimeException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 5000L, multiplier = 2))
public void call() {
LOGGER.info("Call something...");
throw new RuntimeException("RPC调用异常");
} /**
* recover 机制
* @param e 异常
*/
@Recover
public void recover(RuntimeException e) {
LOGGER.info("Start do recover things....");
LOGGER.warn("We meet ex: ", e);
} }
  • 测试
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class RemoteServiceTest { @Autowired
private RemoteService remoteService; @Test
public void test() {
remoteService.call();
} }
  • 日志
2018-08-08 16:03:26.409  INFO 1433 --- [           main] c.g.h.r.spring.service.RemoteService     : Call something...
2018-08-08 16:03:31.414 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Call something...
2018-08-08 16:03:41.416 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Call something...
2018-08-08 16:03:41.418 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Start do recover things....
2018-08-08 16:03:41.425 WARN 1433 --- [ main] c.g.h.r.spring.service.RemoteService : We meet ex: java.lang.RuntimeException: RPC调用异常
at com.github.houbb.retry.spring.service.RemoteService.call(RemoteService.java:38) ~[classes/:na]
...

三次调用的时间点:

2018-08-08 16:03:26.409
2018-08-08 16:03:31.414
2018-08-08 16:03:41.416

缺陷

spring-retry 工具虽能优雅实现重试,但是存在两个不友好设计:

一个是重试实体限定为 Throwable 子类,说明重试针对的是可捕捉的功能异常为设计前提的,但是我们希望依赖某个数据对象实体作为重试实体,

但 sping-retry框架必须强制转换为Throwable子类。

另一个就是重试根源的断言对象使用的是 doWithRetry 的 Exception 异常实例,不符合正常内部断言的返回设计。

Spring Retry 提倡以注解的方式对方法进行重试,重试逻辑是同步执行的,重试的“失败”针对的是Throwable,

如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了。

@Recover 注解在使用时无法指定方法,如果一个类中多个重试方法,就会很麻烦。

guava-retrying

谈话

小华:我们系统也要用到重试

项目经理:小明前段时间用了 spring-retry,分享下应该还不错

小明:spring-retry 基本功能都有,但是必须是基于异常来进行控制。如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了。

小华:我们项目中想根据对象的属性来进行重试。你可以看下 guava-retry,我很久以前用过,感觉还不错。

小明:好的。

guava-retrying

guava-retrying 模块提供了一种通用方法, 可以使用Guava谓词匹配增强的特定停止、重试和异常处理功能来重试任意Java代码。

  • 优势

guava retryer工具与spring-retry类似,都是通过定义重试者角色来包装正常逻辑重试,但是Guava retryer有更优的策略定义,在支持重试次数和重试频度控制基础上,能够兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。

Guava Retryer也是线程安全的,入口调用逻辑采用的是 java.util.concurrent.Callablecall() 方法

代码例子

入门案例

遇到异常之后,重试 3 次停止

  • HelloDemo.java
public static void main(String[] args) {
Callable<Boolean> callable = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
// do something useful here
LOGGER.info("call...");
throw new RuntimeException();
}
}; Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.isNull())
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
retryer.call(callable);
} catch (RetryException | ExecutionException e) {
e.printStackTrace();
} }
  • 日志
2018-08-08 17:21:12.442  INFO  [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts.
2018-08-08 17:21:12.443 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
2018-08-08 17:21:12.444 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
at com.github.rholder.retry.Retryer.call(Retryer.java:174)
at com.github.houbb.retry.guava.HelloDemo.main(HelloDemo.java:53)
Caused by: java.lang.RuntimeException
at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:42)
at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:37)
at com.github.rholder.retry.AttemptTimeLimiters$NoAttemptTimeLimit.call(AttemptTimeLimiters.java:78)
at com.github.rholder.retry.Retryer.call(Retryer.java:160)
... 1 more

总结

优雅重试共性和原理

正常和重试优雅解耦,重试断言条件实例或逻辑异常实例是两者沟通的媒介。

约定重试间隔,差异性重试策略,设置重试超时时间,进一步保证重试有效性以及重试流程稳定性。

都使用了命令设计模式,通过委托重试对象完成相应的逻辑操作,同时内部封装实现重试逻辑。

spring-retry 和 guava-retry 工具都是线程安全的重试,能够支持并发业务场景的重试逻辑正确性。

优雅重试适用场景

功能逻辑中存在不稳定依赖场景,需要使用重试获取预期结果或者尝试重新执行逻辑不立即结束。比如远程接口访问,数据加载访问,数据上传校验等等。

对于异常场景存在需要重试场景,同时希望把正常逻辑和重试逻辑解耦。

对于需要基于数据媒介交互,希望通过重试轮询检测执行逻辑场景也可以考虑重试方案。

谈话

项目经理:我觉得 guava-retry 挺好的,就是不够方便。小明啊,你给封装个基于注解的吧。

小明:……

更好的实现

于是小明含泪写下了 sisyphus.

java 重试框架——sisyphus

希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。

我是老马,期待与你的下次重逢。

更好的 java 重试框架 sisyphus 背后的故事的更多相关文章

  1. 更好的 java 重试框架 sisyphus 配置的 2 种方式介绍

    回顾 我们前面学习了 更好的 java 重试框架 sisyphus 入门简介 更好的 java 重试框架 sisyphus 背后的故事 这一节让我们一起学习下 sisyphus 基于函数式的配置和注解 ...

  2. 更好的 java 重试框架 sisyphus 的 3 种使用方式

    回顾 我们前面学习了 更好的 java 重试框架 sisyphus 入门简介 更好的 java 重试框架 sisyphus 配置的 2 种方式介绍 更好的 java 重试框架 sisyphus 背后的 ...

  3. 更好的 java 重试框架 sisyphus 入门简介

    What is Sisyphus sisyphus 综合了 spring-retry 和 gauva-retrying 的优势,使用起来也非常灵活. 为什么选择这个名字 我觉得重试做的事情和西西弗斯很 ...

  4. 更好用 更简单的Java缓存框架 jscache

    比Spring Cache 更好用 更简单的缓存工具 jscache 取名意义为 java simple cache,基于AOP实现,支持注解到接口 自定义单个缓存过期时间配置 ttl,轻松扩展缓存实 ...

  5. 【万字长文】Dubbo 入门总结 ,一款高性能的 Java RPC 框架

    这篇文章是我学习整理 Dubbo 的一篇文章,首先大部分内容参考了官网 + 某硅谷的视频,内容讲解进行了重新编排,40多张图片,也都是我修改重制的,虽然一万多字,但是其实也可以看出来,更多的内容集中在 ...

  6. Java自动化测试框架-08 - TestNG之并行性和超时篇 (详细教程)

    一.并行性和超时 您可以指示TestNG以各种方式在单独的线程中运行测试. 可以通过在suite标签中使用 parallel 属性来让测试方法运行在不同的线程中.这个属性可以带有如下这样的值: 二.并 ...

  7. Java 分布式框架面试题合集

    Java 分布式框架面试题合集 1.什么是 ZooKeeper? 答:ZooKeeper 是一个开源的分布式应用程序协调服务,是一个典型的分布式数据一致性解决方案.设计目的是将那些复杂且容易出错的分布 ...

  8. 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念

    深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.AQS框架简介 AQS诞生于Jdk1.5,在当时低效且功能单一的synchroni ...

  9. 深入理解Java并发框架AQS系列(四):共享锁(Shared Lock)

    深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 深入理解Java并发框架AQS系列(三):独占锁(Exclusive Lock) 深入 ...

随机推荐

  1. Java 学习:对象和类

    对象和类 从认识的角度考虑是先有对象后有类.对象,是具体的事物.类,是抽象的,是对对象的抽象. 从代码运行角度考虑是先有类后又对象.类是对象的模板. 对象:对象是类的一个实例,有状态和行为. 类:类是 ...

  2. Lambda@edge 实现负载均衡器功能

    一般的业务实现流程为CDN->ELB->EC2,但OTT业务往往会产生很高的流量费用,如果使用常规的架构,流量费用会成倍增加,为了降低费用,我们对架构做了一些优化. AWS Cloudfr ...

  3. 20210803 noip29

    考场 第一次在 hz 考试.害怕会困,但其实还好 看完题感觉不太难,估计有人 AK. T3 比较套路,没办法枚举黑点就从 LCA 处考虑,在一个点变成黑点时计算其他点和它的 LCA 的贡献,暴力跳父亲 ...

  4. harbor高可用集群搭建

    高可用harbor集群搭建 一.安装部署 1.节点角色 角色 数量 名称 备注 harbor主节点 2 harbor-1 harbor-2 双主模式 haproxy 2 HA-1 HA-2 需要通过k ...

  5. CSS002. 字体穿透蒙层(用img设置字体的color)

    之前在逛Apple Store时看到了下面的UI: 交互图标非常圆滑上手也很舒服,虽然背景底色本就是白底,但是只依赖css能不能使  "+" 穿透背景看到底色 ? 大致思路如下: ...

  6. 菜狗、《灵笼》、《时光代理人》,重新审视Z世代的电商逻辑

    来源:懂懂笔记 B站还有多少潜力可以挖掘? 虽然B站的最新财报依然还是亏损,但同时也让人看到更多的可能性. 从财报数据的亮点来看,一是营收增长,B站二季度营收为44.95亿元,同比增长72%.营收上B ...

  7. 深入理解Linux文件系统与日志文件

    目录: 一.inode与block 二.inode内容 三.inode的号码 四.inode的大小 五.链接文件 六.inode节点耗尽故障处理 七.恢复EXT类型的文件编译安装extundelete ...

  8. 从零开始学习SQL SERVER(1)--- 了解SQL

    SQL是什么 SQL (发音为 sequal ['  sikwəl ' ]) SQL指 Structured Query Language 结构化查询语言,是用于访问和处理数据库的标准的计算机语言. ...

  9. 第七章:网络优化与正则化(Part1)

    任何数学技巧都不能弥补信息的缺失. --科尼利厄斯·兰佐斯(Cornelius Lanczos) 匈牙利数学家.物理学家 文章相关 1 第七章:网络优化与正则化(Part1) 2 第七章:网络优化与正 ...

  10. 对Java的annotation(注解)的认识

    什么是java的annotation(注解) ? 注解的定义(annootation): public @interface TestAnnotation { } 上面的这种形式,便定义了注解是如何定 ...