开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

SpringBoot基础之集成接口文档生成

发表于 2021-11-15

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

前言

在平时的多人开发过程中,正确的使用优秀的接口文档,可以减少大量的沟通成本,也可以让前后端更有效率的并行开发,更可以让后来者或者维护者更好更快的上手业务.
使用文档比无文档的项目更容易开发,更容易维护.
使用接口文档生成的接口文档,比第三方维护的文档更方便也更轻松,文档也和代码也更一致.

(一) 文档生成工具swagger

swagger是一个能直接生成接口文档,并能进行接口的是的工具.

(1)引入swagger依赖

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>

<!-- 默认样式 访问地址: /swagger-ui.html -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>

其中springfox-swagger2是swagger的主程序,负责将接口整理成restful形式的数据.
而springfox-swagger-ui是默认的swagger前端界面样式包,默认的访问地址是/swagger-ui.html,
此包可以换成自己喜欢的样式包,比如swagger-ui-layer…

(2)springboot整合swagger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
scss复制代码//记得注解EnableSwagger2
@EnableSwagger2
@Configuration
public class MySwagger2Config {

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("swagger文档标题")
.description("swagger描述")
.version("1.0")
.build();
}

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.zouzdc.controller"))
.paths(PathSelectors.any())
.build();
}

}

(3)使用swagger注解 注释代码

在然后,在方法和参数上使用@ApiParam,@ApiImplicitParams,@ApiOperation注解参数和接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码@ApiOperation(value="多个参数的例子", notes="两个参数的swagger例子,入参一个必填另个非必填")
@ApiImplicitParams({
@ApiImplicitParam(name = "one",value = "姓名" ,required = true),
@ApiImplicitParam(name = "two",value = "年龄")
})
@GetMapping("/moreParams")
public R moreParams(String one,String two){
return R.success("one",one,"two",two);
}


@ApiOperation(value="传入一个对象", notes="传入对象并解释字段含义")
@GetMapping("/dto")
public R tdtoime(StudentSimpleDto dto){
return R.success(dto);
}

StudentSimpleDto.java 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码@Data
public class StudentSimpleDto {

@ApiParam("id")
private Long id;

@ApiParam("姓名")
private String name;

@ApiParam("年龄")
private Integer age;

}

(4)使用

启动项目,然后访问/swagger-ui.html,即可看到swagger界面了

(二) 文档生成工具JApiDocs

JApiDocs是一个无需额外注解、开箱即用的SpringBoot接口文档自动生成工具(也可以有注解)

(1)引入JApiDocs依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>io.github.yedaxia</groupId>
<artifactId>japidocs</artifactId>
<version>1.4.4</version>
</dependency>

(2)正常写javaDoc注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typescript复制代码 /**
* 两个参数的swagger例子,两个参数
* @param one 第一个参数注释
* @param two 第二个参数注释
* @return
*/
@GetMapping("/moreParams")
public R moreParams(String one,String two){
return R.success("one",one,"two",two);
}

/**
* 传入一个对象,传入对象并解释字段含义
* @param StudentSimpleDto 对象参数
* @return
*/
@GetMapping("/dto")
public R tdtoime(StudentSimpleDto dto){
return R.success(dto);
}

StudentSimpleDto.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ruby复制代码@Data
public class StudentSimpleDto {
/**
* id
*/
private Long id;

/**
* 姓名
*/
private String name;

/**
* 年龄
*/
private Integer age;
}

(3)使用

在测试类中运行main方法,生成html离线文档,在本地运行项目时可以直接当方法写在springboot的启动类中

1
2
3
4
5
6
7
8
9
arduino复制代码public static void main(String[] args) {
DocsConfig config = new DocsConfig();
config.setProjectPath("D:\\workSpace\\idea\\sbtest1"); // root project path
config.setProjectName("项目名称"); // project name
config.setApiVersion("V1.0"); // api version
config.setDocsPath("D:\\workSpace\\idea\\sbtest1\\apidoc"); // api docs target path
config.setAutoGenerate(Boolean.TRUE); // auto generate
Docs.buildHtmlDocs(config); // execute to generate
}

其中setProjectPath是项目根目录,setDocsPath是文档的生成目录

(三) swagger和JApiDocs对比

swagger解释基本能用的文档,就必须使用注解,必须侵入正常的业务代码

JApiDocs解释基本能用的文档,使用的是javadoc无需写注解,无需侵入代码,javadoc越规范,文档越清晰,但是使用更多的功能则也需要注解

swagger是直接运行直接生成在线的文档,项目启动即api启动

JApiDocs是基于源码生成的离线文档,需要后端控制生成.

1
2
3
4
arduino复制代码    作者:ZOUZDC
链接:https://juejin.cn/post/7028963866063306760
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

C++-模板基础和函数模板 模板 模板定义

发表于 2021-11-15

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」
[toc]

模板

一个模板就是创建类或者函数的蓝图或者公式,我们提供足够的信息,编译器在编译时将蓝图转换为特定的类或者函数

模板定义

模板定义以关键字template开始,后跟一个模板参数列表,模板参数之间用逗号分隔,模板参数列表用尖括号括起来,如一个函数模板定义如下:

1
2
3
4
5
6
7
8
9
10
cpp复制代码
template <typename T>

bool func(const T& v1,const T& v2)

{

return v1<v2;

}

模板参数表示在类或者函数定义中用到的类型或值,可分为类型参数(type parameter)和非类型参数(nontype parameter)。类型参数前必须使用typename或class关键字。

当使用模板时,我们隐式或显示的指定模板实参,将其绑定到模板参数上。使用函数模板时通常是隐式地指定模板参数,如:

1
2
cpp复制代码
func(1,2);

实参类型是int,所以编译器会推断出模板实参是int类型,并把它绑定到模板参数T。编译器用推断出的模板参数**实例化(instantiate)**模板:

1
2
cpp复制代码
bool func(const int& v1,const int& v2);

非类型参数表示一个值,值必须是常量表达式,绑定到指针或者引用非类型参数的实参必须具有静态的生存期,从而允许编译器在编译时实例化模板,如:

1
2
3
4
5
6
7
8
9
10
cpp复制代码
template<unsigned N,unsigned M>

int compare(const char (&p1)[N],const char (&p2)[M])//数组的引用

{

return strcmp(p1,p2);

}

当我们调用

1
2
cpp复制代码
compare("hi","mom");

编译器会使用字面常量的大小代替N和M,从而实例化模板,因为编译器会在字符串字面量末尾插入一个空字符作为终结符,因此实例化出如下版本:

1
2
cpp复制代码
int compare(const char(&p1)[3],const char (&p2)[4])

inline和constexpr放在模板参数之后,返回类型之前。

模板中的函数参数应使用const引用类型,一方面保证了函数可以用于不可拷贝数据的类型,一方面加快处理速度(不必拷贝),此外模板应该尽可能的减少对类型的要求(如要求支持的运算符)。

由于模板实例化需要模板的定义,因此,模板的头文件通常包括了声明和定义。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文讲懂 Spring事务是怎么通过AOP实现的

发表于 2021-11-15

阅读此文章需要掌握一定的AOP源码基础知识,可以更好的去理解事务,我在另外一篇文章有提过。

spring事务其实就是根据事务注解生成代理类,然后在前置增强方法里获取connection,设置connection到threadlocal,开启事务。再执行原始方法,最后在后置增强方法中判断有无异常来进行事务回滚或提交,再释放连接。

对Spring中的事务功能的代码进行分析,我们先从配置文件开始入手:在配置文件中我们是通过tx:annotation-driven的方式开启的事务配置,所以我们先从这里开始进行分析,根据以往的经验我们在自定义标签中的解析过程中一定是做了一些操作,于是我们先从自定义标签入手进行分析。使用IDEA搜索全局代码,关键字annotation-driven,最终锁定在类TxNamespaceHandler

e1489c1e9152a3c3476015844d580d2d.png

主要查看TxNameSpaceHandler的init方法。

  • 看源码(TxNamespaceHandler.java)
1
2
3
4
5
6
java复制代码@Override
public void init() {
   registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser());
   registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());
   registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser());
}
  • 源码分析

在上述源码中我们看到了annotation-driven这个标识,该行代码也就是说:在遇到诸如tx:annotation-driven为开头的配置后,Spring都会使用AnnotationDrivenBeanDefinitionParser类的parse方法进行解析处理。我们接下来跟踪AnnotationDrivenBeanDefinitionParser类的parse方法

  • 看源码(AnnotationDrivenBeanDefinitionParser.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
   registerTransactionalEventListenerFactory(parserContext);
   String mode = element.getAttribute("mode");
   if ("aspectj".equals(mode)) {
       // mode="aspectj"
       registerTransactionAspect(element, parserContext);
       if (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader())) {
           registerJtaTransactionAspect(element, parserContext);
      }
  } else {
       // mode="proxy"
       AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext);
  }
   return null;
}
  • 源码分析

在上面的源码中我们可以看到有对mode属性进行一个解析判断,根据代码,如果我们需要使用Aspect的方式进行事务的切入(Spring中的事务是以AOP为基础的),根据源码的判断条件我们可以看出我们在开始事务配置的时候也可以像如下这种方式进行开启:

1
2
xml复制代码<!--开启tx注解-->
<tx:annotation-driven transaction-manager="transactionManager" mode="aspectj"/>

事务代理类的创建

1
go复制代码根据上面我们也可以知道,Spring事务会根据配置的`mode`不同,会有不同的实现。我们分开探索:
aspectj模式
  • 看方法registerJtaTransactionAspect
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private void registerTransactionAspect(Element element, ParserContext parserContext) {
   String txAspectBeanName = TransactionManagementConfigUtils.TRANSACTION_ASPECT_BEAN_NAME;
   String txAspectClassName = TransactionManagementConfigUtils.TRANSACTION_ASPECT_CLASS_NAME;
   //如果没有注册过该类,就新注册一个class为 AnnotationTransactionAspect 的bean
   if (!parserContext.getRegistry().containsBeanDefinition(txAspectBeanName)) {
       RootBeanDefinition def = new RootBeanDefinition();
       def.setBeanClassName(txAspectClassName);
       def.setFactoryMethodName("aspectOf");
       //把标签里transaction-manager或transactionManager设置的值配置到bean的成员变量transactionManagerBeanName中
       registerTransactionManager(element, def);
       parserContext.registerBeanComponent(new BeanComponentDefinition(def, txAspectBeanName));
  }
}

看一下AnnotationTransactionAspect 类图

3563fc05ee15e382be13213a5371db71.png

我们发现AnnotationTransactionAspect和父类AbstractTransactionAspect都不是正常class,是aspect;之前设置的transactionManagerBeanName属性在TransactionAspectSupport中

接下来继续追踪AnnotationTransactionAspect类

其实这里这种用法和@Aspect切面类里定义@Pointcut,@Around的用法一个效果

  • 看源码(AnnotationTransactionAspect.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码public aspect AnnotationTransactionAspect extends AbstractTransactionAspect {
   public AnnotationTransactionAspect() {
       super(new AnnotationTransactionAttributeSource(false));
  }
   /**
    * Matches the execution of any public method in a type with the Transactional
    * annotation, or any subtype of a type with the Transactional annotation.
    */
   /**
    *     作用在有Transactional注解或者包含Transactional的注解的public方法
    */
   private pointcut executionOfAnyPublicMethodInAtTransactionalType() :
           execution(public * ((@Transactional *)+).*(..)) && within(@Transactional *);
   /**
    * Matches the execution of any method with the Transactional annotation.
    */
   /**
    * 作用在任何有Transactional注解的方法
    */
   private pointcut executionOfTransactionalMethod() :
           execution(@Transactional * *(..));
   /**
    * Definition of pointcut from super aspect - matched join points
    * will have Spring transaction management applied.
    */
   /**
    * 父类抽象方法,满足上面任一方法条件且满足父类条件的
    */
   protected pointcut transactionalMethodExecution(Object txObject) :
          (executionOfAnyPublicMethodInAtTransactionalType() || executionOfTransactionalMethod() ) && this(txObject);
}
  • 看源码(AbstractTransactionAspect.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
java复制代码public abstract aspect AbstractTransactionAspect extends TransactionAspectSupport implements DisposableBean {
   /**
    * Construct the aspect using the given transaction metadata retrieval strategy.
    * @param tas TransactionAttributeSource implementation, retrieving Spring
    * transaction metadata for each joinpoint. Implement the subclass to pass in
    * {@code null} if it is intended to be configured through Setter Injection.
    */
   protected AbstractTransactionAspect(TransactionAttributeSource tas) {
       setTransactionAttributeSource(tas);
  }
   @Override
       public void destroy() {
       // An aspect is basically a singleton -> cleanup on destruction
       clearTransactionManagerCache();
  }
   //!!!增强逻辑在这,事务也在这实现
   @SuppressAjWarnings("adviceDidNotMatch")
       Object around(final Object txObject): transactionalMethodExecution(txObject) {
       MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature();
       // Adapt to TransactionAspectSupport's invokeWithinTransaction...
       try {
           //父类方法
           return invokeWithinTransaction(methodSignature.getMethod(), txObject.getClass(), new InvocationCallback() {
               public Object proceedWithInvocation() throws Throwable {
                   //就是使用@Around注解时执行ProceedingJoinPoint.proceed()方法
                   return proceed(txObject);
              }
          }
          );
      }
       catch (RuntimeException | Error ex) {
           throw ex;
      }
       catch (Throwable thr) {
           Rethrower.rethrow(thr);
           throw new IllegalStateException("Should never get here", thr);
      }
  }
   /**
    * Concrete subaspects must implement this pointcut, to identify
    * transactional methods. For each selected joinpoint, TransactionMetadata
    * will be retrieved using Spring's TransactionAttributeSource interface.
    */
   /**
    * 由子类实现具体的pointcut
    */
   protected abstract pointcut transactionalMethodExecution(Object txObject);
   /**
    * Ugly but safe workaround: We need to be able to propagate checked exceptions,
    * despite AspectJ around advice supporting specifically declared exceptions only.
    */
   private static class Rethrower {
       public static void rethrow(final Throwable exception) {
           class CheckedExceptionRethrower<T extends Throwable> {
               @SuppressWarnings("unchecked")
                               private void rethrow(Throwable exception) throws T {
                   throw (T) exception;
              }
          }
           new CheckedExceptionRethrower<RuntimeException>().rethrow(exception);
      }
  }
}

总结

aspectj模式其实就是定义了一个Aspect,里面定义了切点,针对所有@Transaction注解的方法,并对切点进行@Around增强,会调用父类TransactionAspectSupport的invokeWithinTransaction方法。

proxy模式

我们从默认的配置方式进行分析(也就是不加mode="aspect"的方式)。对AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext);进行分析。首先我们先看一下AopAutoProxyConfigurer类的configureAutoProxyCreator方法:

AopAutoProxyConfigurer类属于AnnotationDrivenBeanDefinitionParser的内部类

  • 源码分析(AnnotationDrivenBeanDefinitionParser.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码private static class AopAutoProxyConfigurer {
   public static void configureAutoProxyCreator(Element element, ParserContext parserContext) {
       // 向IOC注册 registerAutoProxyCreatorIfNecessary 这个类型的Bean
       // 具体是在 AopConfigUtils 的 registerAutoProxyCreatorIfNecessary 方法中定义的 registerAutoProxyCreatorIfNecessary
       AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element);
       String txAdvisorBeanName = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME;
       if (!parserContext.getRegistry().containsBeanDefinition(txAdvisorBeanName)) {
           Object eleSource = parserContext.extractSource(element);
           // Create the TransactionAttributeSource definition.
           // 创建 AnnotationTransactionAttributeSource 类型的Bean
           RootBeanDefinition sourceDef = new RootBeanDefinition(
                                   "org.springframework.transaction.annotation.AnnotationTransactionAttributeSource");
           sourceDef.setSource(eleSource);
           sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
           String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef);
           // Create the TransactionInterceptor definition.
           // 创建 TransactionInterceptor 类型的Bean
           RootBeanDefinition interceptorDef = new RootBeanDefinition(TransactionInterceptor.class);
           interceptorDef.setSource(eleSource);
           interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
           registerTransactionManager(element, interceptorDef);
           interceptorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName));
           String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef);
           // Create the TransactionAttributeSourceAdvisor definition.
           // 创建 BeanFactoryTransactionAttributeSourceAdvisor 类型的Bean
           RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class);
           advisorDef.setSource(eleSource);
           advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
           // 将上面 AnnotationTransactionAttributeSource 类型Bean注入进上面的Advisor
           advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName));
           // 将上面 TransactionInterceptor 类型Bean注入进上面的Advisor
           advisorDef.getPropertyValues().add("adviceBeanName", interceptorName);
           if (element.hasAttribute("order")) {
               advisorDef.getPropertyValues().add("order", element.getAttribute("order"));
          }
           parserContext.getRegistry().registerBeanDefinition(txAdvisorBeanName, advisorDef);
           // 将上面三个Bean注册进IOC中
           CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource);
           compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName));
           compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName));
           compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, txAdvisorBeanName));
           parserContext.registerComponent(compositeDef);
      }
  }
}
  • 源码分析

看上述源码简单总结一下:这里主要是注册了三个Bean,分别是AnnotationTransactionAttributeSource、TransactionInterceptor、BeanFactoryTransactionAttributeSourceAdvisor,还注册了一个InfrastructureAdvisorAutoProxyCreator的bean;其中前三个Bean支撑了整个事务的功能。在这里我们简单回顾一下AOP原理:

AOP原理:

AOP中有一个Advisor存放在代理类中,而Advisor中有advise与pointcut信息,每次执行被代理类的方法时都会执行代理类的invoke(如果是JDK代理)方法,而invoke方法会根据advisor中的pointcut动态匹配这个方法需要执行的advise链,遍历执行advise链,从而达到AOP切面编程的目的。

需要注意的地方BeanFactoryTransactionAttributeSourceAdvisor.class,首先看到这个类的继承结构,可以看的出来它其实是一个Advisor,这个类中有几个关键的地方需要注意一下,在前面提到的一共注册了三个Bean,将AnnotationTransactionAttributeSource、TransactionInterceptor这两个属性注入到了这个bean中:代码如下:

1
2
3
4
csharp复制代码// 将上面 AnnotationTransactionAttributeSource 类型Bean注入进上面的Advisor
advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName));
// 将上面 TransactionInterceptor 类型Bean注入进上面的Advisor
advisorDef.getPropertyValues().add("adviceBeanName", interceptorName);

问题来了:它们被注入成了什么呢?进入到BeanFactoryTransactionAttributeSourceAdvisor一看便知

1
2
java复制代码@Nullable
private TransactionAttributeSource transactionAttributeSource;

在BeanFactoryTransactionAttributeSourceAdvisor父类中AbstractBeanFactoryPointcutAdvisor有这样的一个属性

1
2
java复制代码@Nullable
private String adviceBeanName;

从上面可以大概知道,先将TransactionInterceptor的BeanName传入到注入到Advisor,然后将AnnotationTransactionAttributeSource这个Bean注入到Advisor。那么这个Source Bean有什么用呢?我们继续追踪BeanFactoryTransactionAttributeSourceAdvisor.java的源码:

  • 看源码(BeanFactoryTransactionAttributeSourceAdvisor.java)
1
2
3
4
5
6
7
java复制代码private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() {
   @Override
   @Nullable
   protected TransactionAttributeSource getTransactionAttributeSource() {
       return transactionAttributeSource;
  }
};
  • 源码解析

看到这里应该明白了,这里的Source提供了Pointcut信息,作为存放事务属性的一个类注入到Advisor中。进行到这里我们也知道了这三个BeanAnnotationTransactionAttributeSource、TransactionInterceptor、BeanFactoryTransactionAttributeSourceAdvisor,的作用了。简单来说就是先注册pointcut、advice、advisor,然后将pointcut和advice注入到advisor中,在之后动态代理的时候会使用这个Advisor去寻找每个Bean是否需要动态代理(取决与是否有开启事务),因为Advisor中有pointcut信息。

74798311c894b6ca6b9b49f332b41a2d.png

  • InfrastructureAdvisorAutoProxyCreator:在方法的开头,首先调用了AopConfigUtils去注册了这个Bean,那么这个Bean是做什么的?首先还是看一下这个类的结构。

5740dac193f061a99f51d3548b981eaa.png
这个类继承了AbstractAutoProxyCreator,看到这个名字,结合之前说过的AOP的应该知道它是做什么的了。其次这个类还实现了BeanPostProcessor接口,凡是实现了这个BeanPostProcessor接口的类,我们首先关注的就是它的postProcessAfterInitialization方法,这个在其父类也就是刚刚提到的AbstractAutoProxyCreator去实现的(这里需要知道Spring容器初始化Bean的过程,关于BeanPostProcessor的使用后续讲解,如果不知道只需要了解如果一个Bean实现了BeanPostProcessor接口,当所有Bean实例化且依赖注入之后初始化方法之后会执行这个实现Bean的postProcessAfterInitialization方法)。

接下来进入这个函数:

  • 看源码(AopNamespaceUtils.java)
1
2
3
4
5
6
7
java复制代码public static void registerAutoProxyCreatorIfNecessary(
           ParserContext parserContext, Element sourceElement) {
   BeanDefinition beanDefinition = AopConfigUtils.registerAutoProxyCreatorIfNecessary(
                   parserContext.getRegistry(), parserContext.extractSource(sourceElement));
   useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement);
   registerComponentIfNecessary(beanDefinition, parserContext);
}

继续追踪上面的源码中的registerComponentIfNecessary方法

  • 看源码(AopNamespaceUtils.java)
1
2
3
4
5
6
java复制代码private static void registerComponentIfNecessary(@Nullable BeanDefinition beanDefinition, ParserContext parserContext) {
   if (beanDefinition != null) {
       parserContext.registerComponent(
                           new BeanComponentDefinition(beanDefinition, AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME));
  }
}
  • 源码分析

对于上面解析来的代码流程已经在AOP中有所分析,可以自行翻看AOP的文章,上面的两个函数主要的目的是注册了InfrastructureAdvisorAutoProxyCreator类型的Bean,那么注册这个类的目的是什么呢?再次查回顾这个类的层次结构:

5740dac193f061a99f51d3548b981eaa.png
分析这个类的层次结构:InfrastructureAdvisorAutoProxyCreator这个类间接实现了SmartInstantiationAwareBeanPostProcessor接口,而SmartInstantiationAwareBeanPostProcessor这个接口有继承了InstantiationAwareBeanPostProcessor接口。也就是说在Spring中,所有Bean实例化时Spring都会保证调用其postProcessAfterInstantiation方法。其实现是在其父类AbstractAutoProxyCreator中。

接下来一之前AccountByXMLServiceImpl为例,当实例化AccountByXmlServiceImpl的Bean时便会调用下面这个方法,方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
   if (bean != null) {
       // 根据bean的class 和 name构建出一个key 格式:beanClassName_beanName
       Object cacheKey = getCacheKey(bean.getClass(), beanName);
       if (this.earlyProxyReferences.remove(cacheKey) != bean) {
           // 如果它适合被代理,则需要指定封装bean
           return wrapIfNecessary(bean, beanName, cacheKey);
      }
  }
   return bean;
}

这里实现的主要目的就是对指定的Bean进行封装,当然首先要确定是否需要封装,检测与封装的工作都委托给了wrapIfNecessary函数进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
   // 如果已经处理过
   if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
       return bean;
  }
   // 无需增强
   if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
       return bean;
  }
   // 给定的bean类是否是一个基础设施类,基础设施类不应该被代理,或者配置了指定的bean不需要代理
   if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
       this.advisedBeans.put(cacheKey, Boolean.FALSE);
       return bean;
  }
   // 获取能够应用到当前 Bean 的所有 Advisor(已根据 @Order 排序)
   // Create proxy if we have advice.
   Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
   // 如果有 Advisor,则进行下面的动态代理创建过程
   if (specificInterceptors != DO_NOT_PROXY) {
       // 如果获取到了增强则需要针对增强进行代理
       this.advisedBeans.put(cacheKey, Boolean.TRUE);
       // 创建代理 JDK 动态代理或者 CGLIB 动态代理
       Object proxy = createProxy(
                           bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
       // 将代理对象的 Class 对象(目标类的子类)保存
       this.proxyTypes.put(cacheKey, proxy.getClass());
       // 返回这个 Bean 对象
       return proxy;
  }
   this.advisedBeans.put(cacheKey, Boolean.FALSE);
   return bean;
}

wrapIfNecessary函数看上去比较复杂,但是逻辑相对还是比较简单的,在wrapIfNecessary函数中主要的工作如下:

  • 找出指定bean对应的增强器
  • 根据找出的增强器创建代理

听起来挺简单的逻辑,但是Spring又做了哪些复杂的工作呢?对于创建代理的工作,通过之前AOP的文章分析相信大家已经有所熟悉了。但是对于增强器的获取,Spring又是如何操作的呢?

获取对应class/method的增强器

寻找候选的增强

获取指定Bean对应的增强器,其中包含了两个关键字:增强器与对应。也就是说在getAdvicesAndAdvisorsForBean函数(上面wrapIfNecessary函数里面的方法)中,不仅要找出增强器,而且要判断增强器是否满足要求。接下来看一下源码:

  • 看源码(AbstractAdvisorAutoProxyCreator.java)
1
2
3
4
5
6
7
8
9
10
java复制代码@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
           Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
   List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
   if (advisors.isEmpty()) {
       return DO_NOT_PROXY;
  }
   return advisors.toArray();
}

继续查看一下上面方法中的findEligibleAdvisors函数:

  • 看源码(AbstractAdvisorAutoProxyCreator.java)
1
2
3
4
5
6
7
8
9
java复制代码protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
   List<Advisor> candidateAdvisors = findCandidateAdvisors();
   List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
   extendAdvisors(eligibleAdvisors);
   if (!eligibleAdvisors.isEmpty()) {
       eligibleAdvisors = sortAdvisors(eligibleAdvisors);
  }
   return eligibleAdvisors;
}

在findEligibleAdvisors函数中我们发现了findCandidateAdvisors和findAdvisorsThatCanApply这两个函数;其中findCandidateAdvisors这个函数是寻找候选的增强,我们简单扫一下这个源码:

  • 看源码(AbstractAdvisorAutoProxyCreator.java)
1
2
3
4
java复制代码protected List<Advisor> findCandidateAdvisors() {
   Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available");
   return this.advisorRetrievalHelper.findAdvisorBeans();
}

接着追踪里面的findAdvisorBeans函数

  • 看源码(BeanFactoryAdvisorRetrievalHelper.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
java复制代码public List<Advisor> findAdvisorBeans() {
   // Determine list of advisor bean names, if not cached already.
   String[] advisorNames = this.cachedAdvisorBeanNames;
   if (advisorNames == null) {
       // Do not initialize FactoryBeans here: We need to leave all regular beans
       // uninitialized to let the auto-proxy creator apply to them!
       // 获取BeanFactory中所有对应Advisor.class的类名
       // 这里和AspectJ的方式有点不同,AspectJ是获取所有的Object.class,然后通过反射过滤有注解AspectJ的类
       advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
                           this.beanFactory, Advisor.class, true, false);
       this.cachedAdvisorBeanNames = advisorNames;
  }
   if (advisorNames.length == 0) {
       return new ArrayList<>();
  }
   List<Advisor> advisors = new ArrayList<>();
   for (String name : advisorNames) {
       if (isEligibleBean(name)) {
           if (this.beanFactory.isCurrentlyInCreation(name)) {
               if (logger.isTraceEnabled()) {
                   logger.trace("Skipping currently created advisor '" + name + "'");
              }
          } else {
               try {
                   //直接获取advisorNames的实例,封装进advisors数组
                   advisors.add(this.beanFactory.getBean(name, Advisor.class));
              }
               catch (BeanCreationException ex) {
                   Throwable rootCause = ex.getMostSpecificCause();
                   if (rootCause instanceof BeanCurrentlyInCreationException) {
                       BeanCreationException bce = (BeanCreationException) rootCause;
                       String bceBeanName = bce.getBeanName();
                       if (bceBeanName != null && this.beanFactory.isCurrentlyInCreation(bceBeanName)) {
                           if (logger.isTraceEnabled()) {
                               logger.trace("Skipping advisor '" + name +
                                                                           "' with dependency on currently created bean: " + ex.getMessage());
                          }
                           // Ignore: indicates a reference back to the bean we're trying to advise.
                           // We want to find advisors other than the currently created bean itself.
                           continue;
                      }
                  }
                   throw ex;
              }
          }
      }
  }
   return advisors;
}
  • 源码分析

首先这个findAdvisorBeans函数先通过BeanFactoryUtils类提供的工具方法获取对应的Advisor.class,获取的办法就是使用ListableBeanFactory中提供的方法:

1
java复制代码String[] getBeanNamesForType(@Nullable Class<?> type, boolean includeNonSingletons, boolean allowEagerInit);

在我们讲解自定义标签时曾经注册了一个类型为BeanFactoryTransactionAttributeSourceAdvisor的Bean,而在此bean中我们又注入了另外两个Bean,那么此时这个Bean就会开始被使用,因为BeanFactoryTransactionAttributeSourceAdvisor同样也实现了Advisor接口。那么在获取所有增强器时自然也会将此Bean提取出来,并随着其他增强器一起在后续的步骤中被植入代理。

候选增强器中寻找匹配项

当找出对应的增强器后,接下来的任务就是看这些增强器是否与对应的class匹配了,当然不只是class,class内部的方法如果匹配也可以通过验证。

接下来看在findEligibleAdvisors函数中我的findAdvisorsThatCanApply这个函数:

  • 看源码(AbstractAdvisorAutoProxyCreator.java)
1
2
3
4
5
6
7
8
9
10
11
java复制代码protected List<Advisor> findAdvisorsThatCanApply(
           List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {
   ProxyCreationContext.setCurrentProxiedBeanName(beanName);
   try {
       // 过滤已经得到的advisors
       return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);
  }
   finally {
       ProxyCreationContext.setCurrentProxiedBeanName(null);
  }
}

继续追踪里面的findAdvisorsThatCanApply方法

  • 看源码(AopUtils)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
   if (candidateAdvisors.isEmpty()) {
       return candidateAdvisors;
  }
   List<Advisor> eligibleAdvisors = new ArrayList<>();
   // 首先处理引介增强
   /*
        * 引介增强是一种特殊的增强,其它的增强是方法级别的增强,即只能在方法前或方法后添加增强。
        * 而引介增强则不是添加到方法上的增强, 而是添加到类方法级别的增强,即可以为目标类动态实现某个接口,
        * 或者动态添加某些方法。我们通过下面的事例演示引介增强的使用
        */
   for (Advisor candidate : candidateAdvisors) {
       if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
           eligibleAdvisors.add(candidate);
      }
  }
   Boolean hasIntroductions = !eligibleAdvisors.isEmpty();
   for (Advisor candidate : candidateAdvisors) {
       // 引介增强已经处理
       if (candidate instanceof IntroductionAdvisor) {
           // already processed
           continue;
      }
       // 对于普通bean的 进行处理
       if (canApply(candidate, clazz, hasIntroductions)) {
           eligibleAdvisors.add(candidate);
      }
  }
   return eligibleAdvisors;
}

继续追踪该方法中的canApply方法:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public static Boolean canApply(Advisor advisor, Class<?> targetClass, Boolean hasIntroductions) {
   if (advisor instanceof IntroductionAdvisor) {
       return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);
  } else if (advisor instanceof PointcutAdvisor) {
       PointcutAdvisor pca = (PointcutAdvisor) advisor;
       return canApply(pca.getPointcut(), targetClass, hasIntroductions);
  } else {
       // It doesn't have a pointcut so we assume it applies.
       return true;
  }
}
  • 源码分析

回想之前,因为BeanFactoryTransactionAttributeSourceAdvisor间接实现了PointcutAdvisor。

09b27a046a2002c1fecf557072e78d05.png

所以在canApply函数中的第二个if判断是就会通过判断。会将BeanFactoryTransactionAttributeSourceAdvisor中的getPointcut() 方法返回值作为参数继续调用canApply方法,而getPoint() 方法返回的是TransactionAttributeSourcePointcut实例,对于transactionAttributeSource这个属性相信大家还有印象,就是在解析自定义标签时注入进去的,方法如下

1
2
3
4
5
6
7
8
java复制代码private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() {
   @Override
           @Nullable
           protected TransactionAttributeSource getTransactionAttributeSource() {
       return transactionAttributeSource;
  }
}
;

那么,使用TransactionAttributeSourcePointcut类型的实例作为函数继续追踪canApply

  • 源码(AopUtils.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码public static Boolean canApply(Pointcut pc, Class<?> targetClass, Boolean hasIntroductions) {
   Assert.notNull(pc, "Pointcut must not be null");
   // 通过Pointcut的条件判断此类是否匹配
   if (!pc.getClassFilter().matches(targetClass)) {
       return false;
  }
   // 此时的pc表示TransactionAttributeSourcePointcut
   // pc.getMethodMatcher()返回的正是自身(this)
   MethodMatcher methodMatcher = pc.getMethodMatcher();
   if (methodMatcher == MethodMatcher.TRUE) {
       // No need to iterate the methods if we're matching any method anyway...
       return true;
  }
   IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
   if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
       introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
  }
   Set<Class<?>> classes = new LinkedHashSet<>();
   if (!Proxy.isProxyClass(targetClass)) {
       classes.add(ClassUtils.getUserClass(targetClass));
  }
   //获取对应类的所有接口
   classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
   //对类进行遍历
   for (Class<?> clazz : classes) {
       // 反射获取类中所有的方法
       Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
       for (Method method : methods) {
           // 根据匹配原则判断该方法是否能匹配Pointcut中的规则,如果有一个方法匹配则返回true
           if (introductionAwareMethodMatcher != null ?
                                   introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :
                                   methodMatcher.matches(method, targetClass)) {
               return true;
          }
      }
  }
   return false;
}

通过上面的函数大致可以理清大致的脉络:

首先获取对应类的所有接口并连通本类一起遍历,遍历过程中又对类中的方法再次遍历,一旦匹配成功便认为这个类适用于当前增强器。

到这里我们就会有疑问?**对于事务的配置不仅仅局限在函数配置上,我们都知道,在类或接口的配置上可以延续到类中的每个函数上。那么,如果针对每个函数进行检测,在本类身上配置的事务属性岂不是检测不到了吗?**接下来我们带着这个疑问继续探索canApply函数中的matcher的方法。

做匹配的时候methodMatcher.matches(method, targetClass)会使用TransactionAttributeSourcePointcut类的matches方法。

  • 看源码(TransactionAttributeSourcePointcut.java)
1
2
3
4
5
6
java复制代码@Override
public Boolean matches(Method method, Class<?> targetClass) {
   // 自定义标签解析时注入
   TransactionAttributeSource tas = getTransactionAttributeSource();
   return (tas == null || tas.getTransactionAttribute(method, targetClass) != null);
}
  • 源码分析

此时上述源码中的tas标识AnnotationTransactionAttributeSource类型,这里会判断tas.getTransactionAttribute(method, targetClass) ,而AnnotationTransactionAttributeSource 类型的getTransactionAttribute方法如下:

  • 看源码(AbstractFallbackTransactionAttributeSource.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
java复制代码@Override
@Nullable
public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
   if (method.getDeclaringClass() == Object.class) {
       return null;
  }
   // First, see if we have a cached value.
   Object cacheKey = getCacheKey(method, targetClass);
   TransactionAttribute cached = this.attributeCache.get(cacheKey);
   //先从缓存中获取TransactionAttribute
   if (cached != null) {
       // Value will either be canonical value indicating there is no transaction attribute,
       // or an actual transaction attribute.
       if (cached == NULL_TRANSACTION_ATTRIBUTE) {
           return null;
      } else {
           return cached;
      }
  }
   // 如果缓存中没有,工作又委托给了computeTransactionAttribute函数 else {
       // We need to work it out.
       TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass);
       // Put it in the cache.
       if (txAttr == null) {
           // 设置为空
           this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE);
      } else {
           String methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass);
           if (txAttr instanceof DefaultTransactionAttribute) {
               DefaultTransactionAttribute dta = (DefaultTransactionAttribute) txAttr;
               dta.setDescriptor(methodIdentification);
               dta.resolveAttributeStrings(this.embeddedValueResolver);
          }
           if (logger.isTraceEnabled()) {
               logger.trace("Adding transactional method '" + methodIdentification + "' with attribute: " + txAttr);
          }
           //加入缓存中
           this.attributeCache.put(cacheKey, txAttr);
      }
       return txAttr;
  }
}
  • 源码分析

尝试从缓存加载,如果对应信息没有缓存的话,工作有委托给了computeTransactionAttribute函数,在computeTransactionAttribute函数中我们终于看到了事务标签的提取过程。

  • 看源码(AbstractFallbackTransactionAttributeSource.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
   // Don't allow no-public methods as required.
   if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
       return null;
  }
   // The method may be on an interface, but we need attributes from the target class.
   // If the target class is null, the method will be unchanged.
   // method代表接口中的方法,specificMethod代表实现类中的方法
   Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
   // First try is the method in the target class.
   // 查看方法中是否存在事务声明
   TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
   if (txAttr != null) {
       return txAttr;
  }
   // Second try is the transaction attribute on the target class.
   // 查看方法所在类中是否存在事务声明
   txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
   if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
       return txAttr;
  }
   // 如果存在接口,则到接口中去寻找
   if (specificMethod != method) {
       // Fallback is to look at the original method.
       // 查找接口方法
       txAttr = findTransactionAttribute(method);
       if (txAttr != null) {
           return txAttr;
      }
       // Last fallback is the class of the original method.
       // 到接口中的类中去寻找
       txAttr = findTransactionAttribute(method.getDeclaringClass());
       if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
           return txAttr;
      }
  }
   return null;
}
  • 源码分析

对于事务属性的获取规则相信大家都已经熟悉很清楚,如果方法中存在事务属性,则使用方法上的属性,否则使用方法所在类上的属性,如果方法所在类的属性还是没有搜寻到对应的事务属性,那么搜寻接口中的方法,再没有的话,最后尝试搜寻接口类上面的声明。对于函数computeTransactionAttribute中的逻辑与我们所认识的规则并无差别,但是上面函数中并没有真正的去做搜寻事务属性的逻辑,而是搭建了个执行框架,将搜寻事务属性的任务委托给了findTransactionAttribute方法去执行。继续进行分析:

  • 看源码(AnnotationTransactionAttributeSource.java)
1
2
3
4
5
java复制代码@Override
@Nullable
protected TransactionAttribute findTransactionAttribute(Class<?> clazz) {
   return determineTransactionAttribute(clazz);
}

继续查看上面的determineTransactionAttribute函数:

  • 看源码(AnnotationTransactionAttributeSource.java)
1
2
3
4
5
6
7
8
9
10
java复制代码@Nullable
protected TransactionAttribute determineTransactionAttribute(AnnotatedElement element) {
   for (TransactionAnnotationParser parser : this.annotationParsers) {
       TransactionAttribute attr = parser.parseTransactionAnnotation(element);
       if (attr != null) {
           return attr;
      }
  }
   return null;
}
  • 源码分析

this.annotationParsers是在当前类AnnotationTransactionAttributeSource 初始化的时候初始化的,其中的值被加入了SpringTransactionAnnotationParser,也就是当进行属性获取的时候虎其实是使用可SpringTransactionAnnotationParser类的parseTransactionAnnotation方法进行解析的。继续分析源码

  • 看源码(SpringTransactionAnnotationParser.java)
1
2
3
4
5
6
7
8
9
10
11
java复制代码@Override
@Nullable
public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) {
   AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(
                   element, Transactional.class, false, false);
   if (attributes != null) {
       return parseTransactionAnnotation(attributes);
  } else {
       return null;
  }
}
  • 源码分析

至此,我们终于看到了想看到的获取注解标记的代码,首先会判断当前的类是否包含又Transactional注解,这是事务属性的基础,当然如果有的话会继续调用parseTransactionAnnotation方法解析详细的属性。接着看源码:

  • 看源码()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) {
   RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute();
   // 解析propagation
   Propagation propagation = attributes.getEnum("propagation");
   rbta.setPropagationBehavior(propagation.value());
   Isolation isolation = attributes.getEnum("isolation");
   // 解析isolation
   rbta.setIsolationLevel(isolation.value());
   // 解析timeout
   rbta.setTimeout(attributes.getNumber("timeout").intValue());
   String timeoutString = attributes.getString("timeoutString");
   Assert.isTrue(!StringUtils.hasText(timeoutString) || rbta.getTimeout() < 0,
                   "Specify 'timeout' or 'timeoutString', not both");
   rbta.setTimeoutString(timeoutString);
   // 解析readOnly
   rbta.setReadOnly(attributes.getBoolean("readOnly"));
   // 解析value
   rbta.setQualifier(attributes.getString("value"));
   rbta.setLabels(Arrays.asList(attributes.getStringArray("label")));
   List<RollbackRuleAttribute> rollbackRules = new ArrayList<>();
   // 解析rollbackFor
   for (Class<?> rbRule : attributes.getClassArray("rollbackFor")) {
       rollbackRules.add(new RollbackRuleAttribute(rbRule));
  }
   // 解析rollbackForClassName
   for (String rbRule : attributes.getStringArray("rollbackForClassName")) {
       rollbackRules.add(new RollbackRuleAttribute(rbRule));
  }
   // 解析noRollbackFor
   for (Class<?> rbRule : attributes.getClassArray("noRollbackFor")) {
       rollbackRules.add(new NoRollbackRuleAttribute(rbRule));
  }
   // 解析noRollbackForClassName
   for (String rbRule : attributes.getStringArray("noRollbackForClassName")) {
       rollbackRules.add(new NoRollbackRuleAttribute(rbRule));
  }
   rbta.setRollbackRules(rollbackRules);
   return rbta;
}
  • 源码解析

至此,我们终于完成了事务标签的解析,回顾一下,我们现在的任务是找出某个增强其是否适合于对应的类,而是否匹配的关键则在于是否从指定的类或类中的方法中找到对应的事务属性。现在我们之前的AccountByXMLServiceImpl为例,已经在它的接口AccountByXMLServiceImpl中找到了事务属性,所以,它是与事务增强器匹配的,也就是它会被事务功能修饰。

1
markdown复制代码至此,事务功能的初始化工作便结束了,当判断某个bean适用于事务增强时,也就i是适用于增强器`BeanFactoryTransactionAttributeSourceAdvisor`,**BeanFactoryTransactionAttributeSourceAdvisor**作为Advisor的实现类,自然要遵从Advisor的处理方式,当代理被调用时会调用这个类的增强方法,也就是bean的Advice,又因为在解析事务定义标签时我们把`Transactionlnterceptor`类的Bean注入到了`BeanFactoryTransactionAttributeSourceAdvisor`中,所以,在调用事务增强器增强的代理类时会首先执行**Transactionlnterceptor**进行增强,同时,也就是在Transactionlnterceptor类中的invoke方法中完成了整个事务的逻辑。

总结:

这一篇文章主要将了事务的Advisor是如何注册进Spring容器的,也讲解了Spring是如何将有配置事务的类配置上事务的,实际上就是使用了AOP那一套,也讲解了Advisor和Pointcut验证流程。至此事务的初始化工作已经完成,在之后的调用过程,如果代理类的方法被调用,都会调用BeanFactoryTransactionAttributeSourceAdvisor这个Advisor的增强方法。目前就是我们还没有提到的那个Advisor里面的Advice;还记得吗我们在自定义标签的时候我们将TransactionInterceptor这个Advice作为Bean注入到IOC容器中,并且将其注入到Advisor中,这个Advice在代理类的invoke方法中会被封装到拦截器链中,最终事务的功能都能在Advice中体现。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

冒泡排序、选择排序之间的比较与代码实现!

发表于 2021-11-15

file

【阅读全文】

冒泡排序

算法特点:越小的元素会慢慢的经过冒泡的方式到数据列的最前面

算法思想:主要是通过对相邻的两个数据元素之间进行比较,直到最后一组相邻元素比较完成。
如此循环往复的比较每组元素,最后自然得到正确的排序结果。

过程演示:

file

代码函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码def bubble_sequence(num_arr):
'''
冒泡排序
:param num_arr:
:return:
'''
num_arr_len = len(num_arr) # 获取数组长度
for i in range(1, num_arr_len): # 外层遍历
for j in range(0, num_arr_len-i): # 内层遍历
if num_arr[j] > num_arr[j+1]: # 相邻元素两两比较
num_arr[j], num_arr[j + 1] = num_arr[j + 1], num_arr[j] # 完成数据元素交换
# 返回最终排序结果
return num_arr

选择排序

算法特点:通过挨个选择的方式选择出最小的放在第一位,次小一些的排在第二位,以此类推实现排序。

算法思想:通过挨个选择的方式选择出最小的放在第一位,次小一些的排在第二位,通过一直搜索从而实现最终排序。

过程演示:

file

代码函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码def selection_sequence(num_arr):
'''
选择排序
:param num_arr:
:return:
'''
num_arr_len = len(num_arr) # 获取数组长度
for i in range(num_arr_len - 1): # 外层遍历
minIndex = i # 记录最小位置的索引
for j in range(i + 1, num_arr_len): # 内层遍历
if num_arr[j] < num_arr[minIndex]: # 比较是否比最小数还要小
minIndex = j
if i != minIndex:
num_arr[i], num_arr[minIndex] = num_arr[minIndex], num_arr[i] # 实现值的交换
return num_arr # 返回最终排序结果

file

【往期精彩】

如何通过pynput与日志记录实现键盘、鼠标的监听行为?

如果你是一名java程序员,面对已经写好的python脚本该如何调用,其实很简单!

如何使用PyQt5一步步实现用户登录GUI界面、登录后跳转?

办公自动化:几行代码将PDF文档转换为WORD文档(代码实战)!

办公自动化:轻松提取PDF页面数据,并生成Excel文件(代码实战)!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go常见坑:Go语言里被defer的函数一定会执行么?

发表于 2021-11-15

前言

大家都知道Go编程中,假设在函数F里,执行了defer A(),那在函数F正常return之前或者因为panic要结束运行之前,被defer关键字修饰的函数调用A()都会被执行到。

比如下面的2个例子:

test1()会在main结束之前执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码// defer1.go
package main

import (
"fmt"
)

func test1() {
fmt.Println("test")
}

func main() {
fmt.Println("main start")
defer test1()
fmt.Println("main end")
}

这个例子输出的结果是:

1
2
3
go复制代码main start
main end
test

test1()会在panic之前执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码// defer2.go
package main

import (
"fmt"
)

func test1() {
fmt.Println("test")
}

func test2() {
panic(1)
}
func main() {
fmt.Println("main start")
defer test1()
test2()
fmt.Println("main end")
}

这个例子输出的结果是:

1
2
3
4
5
6
7
8
9
10
go复制代码main start
test
panic: 1

goroutine 1 [running]:
main.test2(...)
/path/to/defer2.go:13
main.main()
/path/to/defer2.go:18 +0xb8
exit status 2

问题

如果在函数F里,defer A()这个语句执行了,是否意味着A()这个函数调用一定会执行?

这里大家可以先脑补一会。

请看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码// defer3.go
package main

import (
"fmt"
"os"
)

func test1() {
fmt.Println("test")
}

func main() {
fmt.Println("main start")
defer test1()
fmt.Println("main end")
os.Exit(0)
}

上面的代码运行结果会是怎么样?

结论

上面defer3.go执行的结果是:

1
2
go复制代码main start
main end

被defer的test1()并没有在main结束之前执行。这是为什么呢?

查看os.Exit的说明如下:

1
2
3
go复制代码Exit causes the current program to exit with the given status code. Conventionally, code zero indicates success, non-zero an error. The program terminates immediately; deferred functions are not run.

For portability, the status code should be in the range [0, 125].

如果在函数里是因为执行了os.Exit而退出,而不是正常return退出或者panic退出,那程序会立即停止,被defer的函数调用不会执行。

defer 4原则回顾

  1. defer后面跟的必须是函数或者方法调用,defer后面的表达式不能加括号。
1
go复制代码defer (fmt.Println(1)) // 编译报错,因为defer后面跟的表达式不能加括号
  1. 被defer的函数的参数在执行到defer语句的时候就被确定下来了。
1
2
3
4
5
6
go复制代码func a() {
i := 0
defer fmt.Println(i) // 最终打印0
i++
return
}

上例中,被defer的函数fmt.Println的参数i在执行到defer这一行的时候,i的值是0,fmt.Println的参数就被确定下来是0了,因此最终打印的结果是0,而不是1。
3. 被defer的函数执行顺序满足LIFO原则,后defer的先执行。

1
2
3
4
5
go复制代码func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}

上例中,输出的结果是3210,后defer的先执行。
4. 被defer的函数可以对defer语句所在的函数的命名返回值做读取和修改操作。

1
2
3
4
5
6
7
8
go复制代码// f returns 42
func f() (result int) {
defer func() {
// result is accessed after it was set to 6 by the return statement
result *= 7
}()
return 6
}

上例中,被defer的函数func对defer语句所在的函数f的命名返回值result做了修改操作。

调用函数f,返回的结果是42。

执行顺序是函数f先把要返回的值6赋值给result,然后执行被defer的函数func,result被修改为42,然后函数f返回result,也就是返回了42。

官方说明如下:

1
2
3
4
5
6
7
8
9
go复制代码Each time a "defer" statement executes, the function value and parameters to
the call are evaluated as usual and saved anew but the actual function is not
invoked. Instead, deferred functions are invoked immediately before the
surrounding function returns, in the reverse order they were deferred. That
is, if the surrounding function returns through an explicit return statement,
deferred functions are executed after any result parameters are set by that
return statement but before the function returns to its caller. If a deferred
function value evaluates to nil, execution panics when the function is
invoked, not when the "defer" statement is executed.

代码

相关代码和说明开源在GitHub:github.com/jincheng9/g…

也可以搜索公众号:coding进阶,查看更多Go知识。
file

一起进步!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Mysql 温故知新系列「聚合函数」

发表于 2021-11-15

「这是我参与11月更文挑战的第 15 天,活动详情查看:2021最后一次更文挑战」

聚合函数

一般情况下,我们存储在 DB 表中的,都是单条的数据记录,但有时候,又需要聚合这些数据来做分析处理(别杠可以在后台做,那样性能会很差)

根据定义,聚合函数对一组值执行计算并返回单个值

mysql 提供了许多的聚合函数,比如 avg, count, sum, max, min,除 count 之外,其他的聚合函数都会忽略 null

如果有需要,我们可以使用 where 条件先对数据进行过滤,然后再使用聚合函数进行操作

avg

avg 用于计算一组数据的平均值,以手上现有的表数据为例:

image.png

我们统计 amount 的平均数量

image.png

由图可知,很多记录的 amount = 1 导致平均值被拉下去,那么我们忽略掉 amount = 1 的记录,看其余数据的平均值又是多少

image.png

由这里可以看出,avg 在做求平均值的时候,是在 where 条件过滤之后的结果上进行的操作

count

count 返回表中的函数,常规的用法由 count(*) 和 count(1)

image.png

image.png

同样的的测试,count 也是在 where 过滤后的结果上做总计

这里查找了一部分资料:

基于InnoDB引擎

count(*)、count(主键id)和count(1) 都表示返回满足条件的结果集的总行数

count(字段),则表示返回满足条件的数据行里面,参数“字段”不为NULL的总个数。

按照效率排序的话,count(字段)<count(主键id)<count(1)≈count(*)

sum

表示对某个字段的数据进行求和,我们常规用于对数值类型的数据求和

image.png

image.png

max

选取一组记录中,字段值最大的那个

image.png

在常规的表设计中,我们都是使用的自增主键,我们可以通过如下的方式定位到当前表最新的一条数据记录

1
sql复制代码select max(id) from table

min

选取一组记录中,字段值最小的那个

image.png


在聚合函数的单独使用中,都是返回单个的数值类型的结果

但我们可以配合分组查询以及联表查询发挥出更加强大的威力,比如, count + group by 直接定位出重复的记录,这些知识点会在后续的文章中补上

原创文章,未经允许,禁止转载

– by 安逸的咸鱼

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

里式替换原则到底是一个什么原则?

发表于 2021-11-15

这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

  1. 里式替换原则(Liskov Substitution Principle LSP)

假如我们对接了不同的商城,每个商城,都有每个商城自己的下单逻辑,但是我们对外提供的下单逻辑只能有一套,所以需要封装一下,这里就遵守出了 里式替换原则。

在一般的 MVC 架构中,我们一般有 Controller、Service层。而 Service 层一般是接口和对应的实现类,类图如下

2_3_order类图

我们在下单的时候可以使用接口中的 submitOrder,具体的实现逻辑放在不同的子类里,比如我们可以有 JDSubmitOrderServiceImpl、 TMSubmitOrderServiceImpl 等。

里式替换原则:

1
2
3
4
5
6
7
python复制代码If for each object o1 of type S there is an object o2 of type T such that for
all programs P defined in terms of T,the behavior of P is unchanged when o1
is substituted for o2 then S is a subtype of T.

如果对每一个类型为S的对象o1,都有类型为T的对象o2,
使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,
那么类型S是类型T的子类型。

这是一种标准定义。还有一种通俗的定义:

1
2
3
4
css复制代码Functions that use pointers or references to base classes must be able to 
useobjects of derived classes without knowing it.

所有引用基类的地方必须能透明地使用其子类的对象。

再换句话说,就是 任何基类可以出现的地方,子类一定可以出现。

里式替换原则是在告诉我们,继承关系应该遵循哪些原则? 或者说,如果你要使用继承,那么你就应该遵守里式替换原则。

里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原则是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。

里式替换原则有四层含义,或者说,当你实现继承的时候,应该遵守这四个规则。

  • 子类必须完全实现父类的方法

如果子类不能够完全实现父类的方法,那么建议断绝继承关系,使用组合、依赖等代替继承关系。

比如我们的例子中,如果有一个类 XXXOrderServiceImpl 继承了 SubmitOrderService,但是没有实现 Service 中的接口,那么就应该断绝继承关系。

PS:其实这个原则应该没有说完,还有下句。子类必须完全实现父类的方法。但不得重写(覆盖)父类的非抽象(已实现)方法

就是说子类不能改变父类已经实现好的方法,不能改变父类方法的逻辑。

  • 子类可以有自己的个性

或者说,子类可以有自己独有的方法、属性。但是这里需要注意:如果你在子类中定义了方法,那么你在使用这个类的时候就不能使用多肽了。

子类不仅可以继承父类中的方法和属性,还可以定义自己的方法和属性。但是这样做之后,就不能使用多肽了,比如,当我们在使用下单逻辑的时候,一般会这么定义:

1
2
3
ini复制代码SubmitOrderService orderService = new JDSubmitOrderServiceImpl();

orderService.submitOrder();

但是,如果我们在 JDSubmitOrderServiceImpl 中定义了一个自己的方法 xxxOrder(),如果想要使用该方法,就不能使用上面这种定义方式。

1
2
3
ini复制代码JDSubmitOrderServiceImpl orderService = new JDSubmitOrderServiceImpl();

orderService.xxxOrder();

只能使用 JDSubmitOrderServiceImpl 作为对象的类型。

  • 覆盖或实现父类的方法时输入参数(方法的参数)可以被放大

比如,我们在实现 SubmitOrderService 的时候,重写 submitOrder 方法的时候,可以将方法的参数定义成 Order 或者是 Order 的父类。

比如我们 SubmitOrderService 有个方法xxxOrder(ArrayList list) 参数是一个 ArrayList,在实现这个方法的时候可以传入 ArrayList 的父类,可以这么定义xxxOrder(List list)

PS: 这里说的是参数可以被放大,是当你有不得不放大参数的时候,可以放大,而不是只要实现就去放大参数,一般在实现的时候尽量不要放大,而且,一般父类的方法参数都是某个类的父类,很少使用子类的,所以很少遇到放大参数的情况。

注意: 这里说的是方法定义的时候可以被放大。在实际调用的时候可以传入一个参数的子类。比如:

1
2
3
ini复制代码SubmitOrderService orderService = new JDSubmitOrderServiceImpl();

orderService.submitOrder(new JDOrder());

这里我们传入的参数是 Order 的子类。而不是父类。

  • 覆写或实现父类的方法时输出结果(方法的返回值)可以被缩小

跟第 3 条对应,这里说的是方法的返回值可以被缩小,比如父类方法的返回值是 List ,那么子类的返回值可以是 List 或者是 List 的子类。比如 ArrayList。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

withCredentails true为什么没带上coo

发表于 2021-11-15

跨域请求携带cookie需同时满足以下三个条件:

  1. 跨域:CORS
  2. 权限:set-cookie时的same-site
  3. 声明:withCredentails: true

跨域

cors方式可以解决跨域问题,关键点:

  1. options预检请求的发送条件与处理
  2. response header的设置

cookie权限:

(涉及cookies时,是否携带主要取决于是否跨站)

该cookie在set-cookie时same-site的配置:

  1. Strict ,所有跨站请求都不发送cookies
  2. Lax(default) ,大部分跨站请求不发送cookies.
请求类型 示例 正常情况 Lax
链接 <a href="..."></a> 发送 Cookie 发送 Cookie
预加载 <link rel="prerender" href="..."/> 发送 Cookie 发送 Cookie
GET 表单 <form method="GET" action="..."> 发送 Cookie 发送 Cookie
POST 表单 <form method="POST" action="..."> 发送 Cookie 不发送
iframe <iframe src="..."></iframe> 发送 Cookie 不发送
AJAX $.get("...") 发送 Cookie 不发送
Image <img src="..."> 发送 Cookie 不发送

(摘自www.ruanyifeng.com/blog/2019/0…)

  1. None , 允许跨站发送。(必须同时设置Secure)也就是说必须在https协议下才允许跨站发送cookie

💡 跨站一定跨域、跨域不一定跨站


相关细节

预检请求

判断条件:

💡 以下两个条件全部满足会则**不会**发送预检请求,否则发送

  • 请求方法是以下三种方法之一:
+ HEAD
+ GET
+ POST
  • HTTP的头信息不超出以下几种字段:
+ Accept
+ Accept-Language
+ Content-Language
+ Last-Event-ID
+ Content-Type:只限于三个值`application/x-www-form-urlencoded`、`multipart/form-data`、`text/plain`

预检请求的Response header:

  1. Access-Control-Allow-Origin
  2. Access-Control-Allow-Methods
  3. Access-Control-Allow-Headers
  4. Access-Control-Allow-Credentials
  5. Access-Control-Max-Age

正常跨域请求

request header

浏览器自动添加origin字段

response header

  1. Access-Control-Allow-Origin:允许的域
  2. Access-Control-Allow-Credentials:是否允许携带cookies
1. 为true时Access-Control-Allow-Origin不能设置为\*
  1. Access-Control-Expose-Headers:允许客户端拿到response中的哪些header

背后的思考:

跨域主要会带来一些安全问题,如果用户在某个网站登陆后,访问别的网站就可以携带已登陆的网站的cookies进行一些危险操作。

  1. 获取用户信息,
  2. 危险操作
  3. 获取用户浏览痕迹(在A站放入B站的图片,B站可以根据请求cookie得知某个用户浏览了哪个网页)

防御措施:

  1. HttpOnly:禁止script读取cookie
1. 防止xss窃取cookie
  1. Secure:必须通过https发送
  2. SameSite:跨站禁止发送cookie

所以最好不要跨域

  • 生产环境应当使用代理避免跨域

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Kubernetes监控体系总结

发表于 2021-11-15

基本概念

cAdvisor

图片

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux/Windows/Mac 机器上。容器镜像正成为一个新的标准化软件交付方式。为了能够获取到 Docker 容器的运行状态,用户可以通过 Docker 的 stats 命令获取到当前主机上运行容器的统计信息,可以查看容器的 CPU 利用率、内存使用量、网络 IO 总量以及磁盘 IO 总量等信息。

显然如果我们想对监控数据做存储以及可视化的展示,那么 docker 的 stats 是不能满足的。

为了解决 docker stats 的问题(存储、展示),谷歌开源的 cadvisor 诞生了,cadvisor 不仅可以搜集一台机器上所有运行的容器信息,还提供基础查询界面和 http 接口,方便其他组件如 Prometheus 进行数据抓取,或者 cAdvisor + influxDB + grafana 搭配使用。cAdvisor 可以对节点机器上的资源及容器进行实时监控和性能数据采集,包括 CPU 使用情况、内存使用情况、网络吞吐量及文件系统使用情况

监控原理

cAdvisor 使用 Go 语言开发,利用 Linux 的 cgroups 获取容器的资源使用信息,在 K8S 中集成在 Kubelet 里作为默认启动项,官方标配。

Docker 是基于 Namespace、Cgroups 和联合文件系统实现的

Cgroups 不仅可以用于容器资源的限制,还可以提供容器的资源使用率。不管用什么监控方案,底层数据都来源于 Cgroups

Cgroups 的工作目录 /sys/fs/cgroup 下包含了 Cgroups 的所有内容。Cgroups 包含了很多子系统,可以对 CPU,内存,PID,磁盘 IO 等资源进行限制和监控。

cAdvisor 运行原理,如下图

图片

Prometheus

图片

Prometheus 是一套开源的监控报警系统。主要特点包括多维数据模型、灵活查询语句 PromQL 以及数据可视化展示等

架构图图片

基本原理

Prometheus 的基本原理是通过 HTTP 协议周期性抓取被监控组件的状态,任意组件只要提供对应的 HTTP 接口就可以接入监控。不需要任何 SDK 或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如 VM、Docker、Kubernetes 等。输出被监控组件信息的 HTTP 接口被叫做 exporter 。目前互联网公司常用的组件大部分都有 exporter 可以直接使用,比如 Varnish、Haproxy、Nginx、MySQL、Linux 系统信息(包括磁盘、内存、CPU、网络等等)。

服务过程

  • Prometheus Daemon 负责定时去目标上抓取 metrics(指标)数据,每个抓取目标需要暴露一个 http 服务的接口给它定时抓取。Prometheus 支持通过配置文件、文本文件、Zookeeper、Consul、DNS SRV Lookup 等方式指定抓取目标。Prometheus 采用 PULL 的方式进行监控,即服务器可以直接通过目标 PULL 数据或者间接地通过中间网关来 Push 数据。
  • Prometheus 在本地存储抓取的所有数据,并通过一定规则进行清理和整理数据,并把得到的结果存储到新的时间序列中。
  • Prometheus 通过 PromQL 和其他 API 可视化地展示收集的数据。Prometheus 支持很多方式的图表可视化,例如 Grafana、自带的 Promdash 以及自身提供的模版引擎等等。Prometheus 还提供 HTTP API 的查询方式,自定义所需要的输出。
  • PushGateway 支持 Client 主动推送 metrics 到 PushGateway,而 Prometheus 只是定时去 Gateway 上抓取数据。
  • Alertmanager 是独立于 Prometheus 的一个组件,可以支持 Prometheus 的查询语句,提供十分灵活的报警方式。

Operator

Operator 是 CoreOS 推出的旨在简化复杂有状态应用管理的框架,它是一个感知应用状态的控制器,通过扩展 Kubernetes API 来自动创建、管理和配置应用实例。

Operator 基于 CustomResourceDefinition(CRD) 扩展了新的应用资源,并通过控制器来保证应用处于预期状态。比如 etcd operator 通过下面的三个步骤模拟了管理 etcd 集群的行为:

  1. 通过 Kubernetes API 观察集群的当前状态;
  2. 分析当前状态与期望状态的差别;
  3. 调用 etcd 集群管理 API 或 Kubernetes API 消除这些差别。

图片

Prometheus Operator

为了在 Kubernetes 能够方便的管理和部署 Prometheus,我们使用 ConfigMap 了管理 Prometheus 配置文件。每次对 Prometheus 配置文件进行升级时,我们需要手动移除已经运行的 Pod 实例,从而让 Kubernetes 可以使用最新的配置文件创建 Prometheus。而如果当应用实例的数量更多时,通过手动的方式部署和升级 Prometheus 过程繁琐并且效率低下。

从本质上来讲 Prometheus 属于是典型的有状态应用,而其又包含了一些自身特有的运维管理和配置管理方式。而这些都无法通过 Kubernetes 原生提供的应用管理概念实现自动化。为了简化这类应用程序的管理复杂度,CoreOS 率先引入了 Operator 的概念,并且首先推出了针对在 Kubernetes 下运行和管理 Etcd 的 Etcd Operator。并随后推出了 Prometheus Operator。

从概念上来讲 Operator 就是针对管理特定应用程序的,在 Kubernetes 基本的 Resource 和 Controller 的概念上,以扩展 Kubernetes api 的形式。帮助用户创建,配置和管理复杂的有状态应用程序。从而实现特定应用程序的常见操作以及运维自动化。

在 Kubernetes 中我们使用 Deployment、DamenSet,StatefulSet 来管理应用 Workload,使用 Service,Ingress 来管理应用的访问方式,使用 ConfigMap 和 Secret 来管理应用配置。我们在集群中对这些资源的创建,更新,删除的动作都会被转换为事件 (Event),Kubernetes 的 Controller Manager 负责监听这些事件并触发相应的任务来满足用户的期望。这种方式我们成为声明式,用户只需要关心应用程序的最终状态,其它的都通过 Kubernetes 来帮助我们完成,通过这种方式可以大大简化应用的配置管理复杂度。

而除了这些原生的 Resource 资源以外,Kubernetes 还允许用户添加自己的自定义资源 (Custom Resource)。并且通过实现自定义 Controller 来实现对 Kubernetes 的扩展。

如下所示,是 Prometheus Operator 的架构示意图:

图片

Prometheus 的本职就是一组用户自定义的 CRD 资源以及 Controller 的实现,Prometheus Operator 负责监听这些自定义资源的变化,并且根据这些资源的定义自动化的完成如 Prometheus Server 自身以及配置的自动化管理工作。

简言之,Prometheus Operator 能够帮助用户自动化的创建以及管理 Prometheus Server 以及其相应的配置。

HPA

Horizontal Pod Autoscaler ,K8S 中的一个概念,可以自动调整 Pod 的数量,以达到指定的目标值。

Pod 水平自动扩缩(Horizontal Pod Autoscaler) 可以基于 CPU 利用率自动扩缩 ReplicationController、Deployment、ReplicaSet 和 StatefulSet 中的 Pod 数量。除了 CPU 利用率,也可以基于其他应程序提供的 自定义度量指标来执行自动扩缩。Pod 自动扩缩不适用于无法扩缩的对象,比如 DaemonSet。

图片

Heapster

Heapster 是容器集群监控和性能分析工具,天然的支持 Kubernetes 和 CoreOS。

Heapster 首先从 K8S Master 获取集群中所有 Node 的信息,然后通过这些 Node 上的 kubelet 获取有用数据,而 kubelet 本身的数据则是从 cAdvisor 得到。所有获取到的数据都被推到 Heapster 配置的后端存储中,并还支持数据的可视化。现在后端存储 + 可视化的方法,如 InfluxDB + grafana。

Heapster 可以收集 Node 节点上的 cAdvisor 数据,还可以按照 kubernetes 的资源类型来集合资源,比如 Pod、Namespace 域,可以分别获取它们的 CPU、内存、网络和磁盘的 metric。默认的 metric 数据聚合时间间隔是 1 分钟。

注意 :Kubernetes 1.11 不建议使用 Heapster,就 SIG Instrumentation 而言,这是为了转向新的 Kubernetes 监控模型的持续努力的一部分。仍使用 Heapster 进行自动扩展的集群应迁移到 metrics-server 和自定义指标 API。

图片

Metrics Server

图片

kubernetes 集群资源监控之前可以通过 heapster 来获取数据,在 1.11 开始开始逐渐废弃 heapster 了,采用 metrics-server 来代替,metrics-server 是集群的核心监控数据的聚合器,它从 kubelet 公开的 Summary API 中采集指标信息,metrics-server 是扩展的 APIServer,依赖于 kube-aggregator,因为我们需要在 APIServer 中开启相关参数。

Metrics Server 并不是 kube-apiserver 的一部分,而是通过 Aggregator 这种插件机制,在独立部署的情况下同 kube-apiserver 一起统一对外服务的。

Aggregator

“

通过聚合层扩展 Kubernetes API使用聚合层(Aggregation Layer),用户可以通过额外的 API 扩展 Kubernetes, 而不局限于 Kubernetes 核心 API 提供的功能。这里的附加 API 可以是现成的解决方案比如 metrics server, 或者你自己开发的 API。聚合层不同于 定制资源(Custom Resources)。后者的目的是让 kube-apiserver 能够认识新的对象类别(Kind)。

”

“

聚合层聚合层在 kube-apiserver 进程内运行。在扩展资源注册之前,聚合层不做任何事情。要注册 API,用户必须添加一个 APIService 对象,用它来“申领” Kubernetes API 中的 URL 路径。自此以后,聚合层将会把发给该 API 路径的所有内容(例如 /apis/myextension.mycompany.io/v1/…) 转发到已注册的 APIService。

”

“

APIService 的最常见实现方式是在集群中某 Pod 内运行 扩展 API 服务器。如果你在使用扩展 API 服务器来管理集群中的资源,该扩展 API 服务器(也被写成“extension-apiserver”) 一般需要和一个或多个控制器一起使用。apiserver-builder 库同时提供构造扩展 API 服务器和控制器框架代码。

”

这里,Aggregator APIServer 的工作原理,可以用如下所示的一幅示意图来表示清楚 :

图片

因为 k8s 的 api-server 将所有的数据持久化到了 etcd 中,显然 k8s 本身不能处理这种频率的采集,而且这种监控数据变化快且都是临时数据,因此需要有一个组件单独处理他们,于是 metric-server 的概念诞生了。

Metrics server 出现后,新的 Kubernetes 监控架构将变成下图的样子

  • 核心流程(黑色部分):这是 Kubernetes 正常工作所需要的核心度量,从 Kubelet、cAdvisor 等获取度量数据,再由 metrics-server 提供给 Dashboard、HPA 控制器等使用。
  • 监控流程(蓝色部分):基于核心度量构建的监控流程,比如 Prometheus 可以从 metrics-server 获取核心度量,从其他数据源(如 Node Exporter 等)获取非核心度量,再基于它们构建监控告警系统。

图片

注意:

  • metrics-sevrer 的数据存在内存中。
  • metrics-server 主要针对 node、pod 等的 cpu、网络、内存等系统指标的监控

kube-state-metrics

已经有了 cadvisor、heapster、metric-server,几乎容器运行的所有指标都能拿到,但是下面这种情况却无能为力:

  • 我调度了多少个 replicas?现在可用的有几个?
  • 多少个 Pod 是 running/stopped/terminated 状态?
  • Pod 重启了多少次?
  • 我有多少 job 在运行中

而这些则是 kube-state-metrics 提供的内容,它基于 client-go 开发,轮询 Kubernetes API,并将 Kubernetes 的结构化信息转换为 metrics。

kube-state-metrics 与 metrics-server 对比

我们服务在运行过程中,我们想了解服务运行状态,pod 有没有重启,伸缩有没有成功,pod 的状态是怎么样的等,这时就需要 kube-state-metrics,它主要关注 deployment,、node 、 pod 等内部对象的状态。而 metrics-server 主要用于监测 node,pod 等的 CPU,内存,网络等系统指标。

  • metric-server(或 heapster)是从 api-server 中获取 cpu、内存使用率这种监控指标,并把他们发送给存储后端,如 influxdb 或云厂商,他当前的核心作用是:为 HPA 等组件提供决策指标支持。
  • kube-state-metrics 关注于获取 k8s 各种资源的最新状态,如 deployment 或者 daemonset,之所以没有把 kube-state-metrics 纳入到 metric-server 的能力中,是因为他们的关注点本质上是不一样的。metric-server 仅仅是获取、格式化现有数据,写入特定的存储,实质上是一个监控系统。而 kube-state-metrics 是将 k8s 的运行状况在内存中做了个快照,并且获取新的指标,但他没有能力导出这些指标
  • 换个角度讲,kube-state-metrics 本身是 metric-server 的一种数据来源,虽然现在没有这么做。
  • 另外,像 Prometheus 这种监控系统,并不会去用 metric-server 中的数据,他都是自己做指标收集、集成的(Prometheus 包含了 metric-server 的能力),但 Prometheus 可以监控 metric-server 本身组件的监控状态并适时报警,这里的监控就可以通过 kube-state-metrics 来实现,如 metric-serverpod 的运行状态。

图片

custom-metrics-apiserver

kubernetes 的监控指标分为两种

  • Core metrics(核心指标):从 Kubelet、cAdvisor 等获取度量数据,再由 metrics-server 提供给 Dashboard、HPA 控制器等使用。
  • Custom Metrics(自定义指标):由 Prometheus Adapter 提供 API custom.metrics.k8s.io,由此可支持任意 Prometheus 采集到的指标。

以下是官方 metrics 的项目介绍:

Resource Metrics API(核心 api)

  • Heapster
  • Metrics Server

Custom Metrics API:

  • Prometheus Adapter
  • Microsoft Azure Adapter
  • Google Stackdriver
  • Datadog Cluster Agent

核心指标只包含 node 和 pod 的 cpu、内存等,一般来说,核心指标作 HPA 已经足够,但如果想根据自定义指标:如请求 qps/5xx 错误数来实现 HPA,就需要使用自定义指标了,目前 Kubernetes 中自定义指标一般由 Prometheus 来提供,再利用 k8s-prometheus-adpater 聚合到 apiserver,实现和核心指标(metric-server) 同样的效果。

HPA 请求 metrics 时,kube-aggregator(apiservice 的 controller) 会将请求转发到 adapter,adapter 作为 kubernentes 集群的 pod,实现了 Kubernetes resource metrics API 和 custom metrics API,它会根据配置的 rules 从 Prometheus 抓取并处理 metrics,在处理(如重命名 metrics 等)完后将 metric 通过 custom metrics API 返回给 HPA。最后 HPA 通过获取的 metrics 的 value 对 Deployment/ReplicaSet 进行扩缩容。

adapter 作为 extension-apiserver(即自己实现的 pod),充当了代理 kube-apiserver 请求 Prometheus 的功能。

图片

其实 k8s-prometheus-adapter 既包含自定义指标,又包含核心指标,即如果安装了 prometheus,且指标都采集完整,k8s-prometheus-adapter 可以替代 metrics server。

Prometheus 部署方案

prometheus operator

  • github.com/prometheus-…

kube-prometheus

  • github.com/prometheus-…

在集群外部署

  • www.qikqiak.com/post/monito…

kube-prometheus 既包含了 Operator,又包含了 Prometheus 相关组件的部署及常用的 Prometheus 自定义监控,具体包含下面的组件

  • The Prometheus Operator:创建 CRD 自定义的资源对象
  • Highly available Prometheus:创建高可用的 Prometheus
  • Highly available Alertmanager:创建高可用的告警组件
  • Prometheus node-exporter:创建主机的监控组件
  • Prometheus Adapter for Kubernetes Metrics APIs:创建自定义监控的指标工具(例如可以通过 nginx 的 request 来进行应用的自动伸缩)
  • kube-state-metrics:监控 k8s 相关资源对象的状态指标
  • Grafana:进行图像展示

我们的做法

我们的做法,其实跟 kube-prometheus 的思路差不多,只不过我们没有用 Operator ,是自己将以下这些组件的 yaml 文件用 helm 组织了起来而已:

  • kube-state-metrics
  • prometheus
  • alertmanager
  • grafana
  • k8s-prometheus-adapter
  • node-exporter

当然 kube-prometheus 也有 helm charts 由 prometheus 社区提供:github.com/prometheus-…

这么干的原因是:这样的灵活度是最高的,虽然在第一次初始化创建这些脚本的时候麻烦了些。不过还有一个原因是我们当时部署整个基于 prometheus 的监控体系时,kube-prometheus 这个项目还在早期,没有引起我们的关注。如果在 2021 年年初或 2020 年年底的时候创建的话,可能就会直接上了。

参考

  • blog.opskumu.com/cadvisor.ht…
  • prometheus.io/
  • kubernetes.io/zh/docs/tas…
  • www.cnblogs.com/chenqionghe…
  • www.qikqiak.com/post/k8s-op…
  • kubernetes.io/zh/docs/con…
  • segmentfault.com/a/119000001…
  • segmentfault.com/a/119000003…
  • yasongxu.gitbook.io/
  • mp.weixin.qq.com/s/p4FAFKHi8…
  • kubernetes.feisky.xyz/apps/index/…
  • yunlzheng.gitbook.io/prometheus-…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

浅析分布式ID生成方案

发表于 2021-11-15

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

高并发情况下的数据库自增ID会出现重复情况,导致新增失败。
解决办法就是使用分布式ID

分布式ID的生成方式有以下几种

  • UUID
  • 数据库不同步长自增
  • 雪花算法
  • redis
  • 滴滴出品(TinyID)
  • 百度 (Uidgenerator)
  • 美团(Leaf)

常用.png

UUID

生成唯一ID,最简单的就是UUID,具有唯一性,但是结果太长了,保存在数据库中,会导致索引太大,消耗太大。

数据库不同步长自增ID

设置起始值和自增步长,就可以保证唯一ID是自增有序,而且还不会重复,缺点就是不利于扩容,增加维护成本。

  • mysql 1 配置:
1
2
sql复制代码set @@auto_increment_offset = 1;     -- 起始值
set @@auto_increment_increment = 2; -- 步长
  • mysql 2 配置:
1
2
sql复制代码set @@auto_increment_offset = 2;     -- 起始值
set @@auto_increment_increment = 2; -- 步长

雪花算法(Snowflake)

雪花算法(Snowflake)是twitter公司内部分布式项目采用的ID生成算法,理论上单机每秒400W+,最多每秒可以生成41亿+的ID

snow.png

分段 作用 说明
1bit 保留 —
41bit 时间戳,精确到毫秒 可以支持69年的跨度
5bit DatacenterId 可以最多支持32个节点
5bit WorkerId 可以最多支持32个节点
12bit 毫秒内的计数 支持每个节点每毫秒产生4096个ID
  • 优点
    • ID趋势递增
    • 生成效率高,单机每秒400W+
    • 支持线性扩充
    • 稳定性高,不依赖DB等服务
  • 缺点
    • 依赖服务器时间,如果服务器时间发生回拨,可能导致生成重复ID
    • 在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,也许有时候也会出现不是全局递增的情况

Redis生成ID

Redis可以作为集中式ID生成器,数据库的表记录最后分配的ID也是集中式ID生成器。
目前使用的ID生成器结合了这两种,实现如下:

  • redis累加器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public int getId() {
// 获取redis累加器,累加获得id
int id = 1;
// 判断key是否存在
if (stringRedisLettuceTemplate.hasKey("message:id")) {
// 如果存在key则直接获取后进行累加
id = Integer.valueOf(stringRedisLettuceTemplate.boundValueOps("message:id").get());
stringRedisLettuceTemplate.opsForValue().increment("message:id", 1);
} else {
// 如果不存在,查询数据库最后一条,获取后+1
id = userDao.getLastId() + 1;
// 再次+1后存入redis
stringRedisLettuceTemplate.opsForValue().set("message:id", String.valueOf(id + 1));
}
return id;
}
  • 数据库
1
2
3
4
5
6
xml复制代码<select id="getLastId" resultType="java.lang.Integer">
SELECT IFNULL(id, 0) AS id
FROM user
ORDER BY id desc
limit 1
</select>

参考

  • 分布式唯一ID生成器
  • 分布式id生成器

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…334335336…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%