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

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


  • 首页

  • 归档

  • 搜索

【Java实用技术】字符串的拆分用什么方法好?有一半程序员都

发表于 2021-10-31
  • 小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
  • 本文同时参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

必备字符串操作

我们将字符串操作分为下面6种:

  1. 基本操作方法
  2. 字符串判空、比较
  3. 字符串截取和拆分
  4. 字符串查找和替换
  5. 字符串和其他类型数据的转换
  6. 字符串拼接和格式化

今天我们来讲解第3.2节。

字符串拆分原生方法

对于字符串拆分的原生操作

1
2
java复制代码//字符串拆分原生方法:
public String[] String.split(String regex)

问题: 对字符串中含有“|”分割符部分进行拆分,怎么写?

直接str.split("|")可以吗?当然可以,但是结果是不是你想要的呢?

1
2
3
java复制代码String str = "a.|b.";
*** 原生 String.split 方法结果 *****
[a, ., |, b, .]

答案: 如果只是单纯想按照“|”分割,正确的写法是ss.split("\\|"),因为这里碰到烦人的正则表达式。

1
2
3
4
5
6
java复制代码String str = "a.|b."; // 使用“|”拆分
System.out.println("*** 原生 String.split 方法 *****");
if (StringUtils.isNotEmpty(str)) {
    String[] s1 = str.split("\\|");
    System.out.println(Arrays.toString(s1));
}

split源码分析

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
java复制代码// 选取源码中关键的分析

/**
围绕给定正则表达式的匹配拆分此字符串。
此方法的工作方式就像通过使用给定表达式和零限制参数调用双参数split方法一样。 因此,结果数组中不包含尾随空字符串。
参数:regex – 定界正则表达式
返回:通过围绕给定正则表达式的匹配拆分此字符串计算出的字符串数组
抛出:PatternSyntaxException – 如果正则表达式的语法无效
*/
public String[] split(String regex) {
    return split(regex, 0);
}

/**
围绕给定正则表达式的匹配拆分此字符串。
以str形式调用此方法。 split(regex , n)产生与表达式相同的结果:
Pattern.compile(regex).split(str , n)
*/
public String[] split(String regex, int limit) {
        /* 下面情况使用快速模式:
         (1)单个字符但是不能是正则中的元字符,比如".$|()[{^?*+\"
         (2)两个字符,第一个字符是\,第二个字符不能是数字或者字母[0-9a-zA-Z]
         */
        char ch = 0;
        if (((regex.value.length == 1 && ".$|()[{^?*+\".indexOf(ch = regex.charAt(0)) == -1) 
             || (regex.length() == 2 && regex.charAt(0) == '\' && (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 
             && ((ch-'a')|('z'-ch)) < 0 &&  ((ch-'A')|('Z'-ch)) < 0)) 
             &&  (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE))
        {
            //省略 使用substring拆分  过程
            String[] result = new String[resultSize];
            return list.subList(0, resultSize).toArray(result);
        }
       // 其他场景使用正则表达式拆分
        return Pattern.compile(regex).split(this, limit);
    }

源码不复杂,核心内容是:

(1)不是正则表达式的元字符".$|()[{^?*+\"或``开头但后面不是[0-9a-zA-Z]的使用快速拆分模式

(2)其他情况使用正则表达式匹配模式拆分

这里涉及到另外一个知识点,正则表达式,如果对正则表达式感兴趣的可以参考其他博文。

当然我认为你是没有兴趣的,而且很多程序员也很讨厌正则表达的规则,需要用的时候再翻阅。

对于(1),如果你十分精通正则表达式,可以很快知道哪些是正则元字符,你可以尝试使用原生的方法,毕竟使用方便且拆分速度确实快。

对于(2),因为正则表达式需要编译再匹配,比较耗时,所以不建议使用正则表达式。至于为什么正则表达式会慢,你就想你只见过一个姑娘一面就想找到她,和你知道她的名字地址具体信息比,哪个找到她更快?

综上所述,原生方法存在以下问题:

  • 使用前要判空
  • 小心正则表达式里面特殊字符
  • 性能不佳

为什么不推荐StringTokenizer

当然有同学可能会说jdk还有一个用于拆分字符串的类:StringTokenizer,它不是基于正则表达式进行拆分,性能应该高。但是笔者并不推荐使用此方法进行字符串拆分。我们先看源码:

1
2
3
4
5
6
7
8
9
10
java复制代码/**
StringTokenizer is a legacy class that is retained for compatibility reasons although its use is 
discouraged
in new code. It is recommended that anyone seeking this functionality 
use the split method of String or the java.util.regex package instead.
Since: JDK1.0
*/
public class StringTokenizer implements Enumeration<Object> {
    //...
}

只看关键单词discouraged=不推荐。知道它是一个为了兼容而遗留下的类,官方不推荐使用就足够的。

另外一个不推荐的原因是:本来很简单的一个拆分,使用它需要搭配while循环,索性就不要用它了。

推荐字符串拆分操作

善用StringUtils.splitXX()

推荐使用Apache工具类StringUtils.splitXX()

优势: 不用担心字符串为null(空),方法名直白。

推荐常用方法:

1
2
3
4
5
6
7
8
9
10
11
java复制代码// 按照指定分割符拆分字符串
public static String[] split(String str, String separator)
 // 按照完整分割符拆分字符串
public static String[] splitByWholeSeparator(String str, String separator)
 // 拆分字符串,保留所有分割符位置 --较少使用
public static String[] splitPreserveAllTokens(String str, String separatorChars)

// 获取分割符的前部分
 public static String[] substringBefore(String str, String separatorChars)
 //获取分割符的后面部分
 public static String[] substringAfterLast(String str, String separatorChars)

对于上面问题按照“|”拆分,可以一行代码搞定。

1
2
3
java复制代码String str = "a.|b.";
String[] s2 = StringUtils.split(str, "|");
/* 输出:[a., b.] */

开发中比较常见的需求是对", . | * : "等进行拆分。如果你对正则元字符不熟悉,很容易写出错误的拆分。

所以还是建议直接使用StringUtils工具类进行拆分,减少写代码时的思考成本。

StringUtils.split也有坑

img

还是上述例子,如果对字符串"a.|b.c"按照".|"进行拆分,使用StringUtils.split可以吗?

1
2
3
4
5
java复制代码// 问题:请使用“.|”拆分
 String str = "a.|b.c";
String[] s2 = StringUtils.split(str, ".|");
/* 运行结果:[a, b, c]
并不是期望的[a, b.c] */

答案是不行,拆分的结果是[a, b, c],并不是期望的[a, b.c], 这是因为StringUtils.split方法是将多字符分割符拆成单个分割符进行拆分,再将非分割符部分返回。

如果希望对多字符分割符拆分,请使用StringUtils.splitByWholeSeparator()方法。

这个小细节,希望大家能够避坑。

demo用例

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
java复制代码import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;
import java.util.StringTokenizer;

/**
 * Java实用技术课程 By Pandas.
 * 公众号:Java实用技术手册
 * JDK版本:jdk1.8.0_66
 *
 * @author Pandas
 * @date 2021/10/30
 */
public class StringSplitDemo {

    /**
     * 字符串拆分方法用哪个方法好?
     */
    public static void main(String[] args) {
        // 问题:请使用“|”拆分
        String str = "a.|b.c";
        System.out.println("*** 原生 String.split 方法 *****");
        if (StringUtils.isNotEmpty(str)) {
            String[] s1 = str.split("\|");
            System.out.println(Arrays.toString(s1));
        }
        String[] s11 = StringUtils.split(str, "|");
        System.out.println(Arrays.toString(s11));

        System.out.println("*** 原生 StringTokenizer 方法 *****");
        StringTokenizer tokenizer = new StringTokenizer(str, "|");
        while (tokenizer.hasMoreTokens()) {
            System.out.println(tokenizer.nextToken());
        }

        // "a.|b.c"
        str = "a.|b.c";
        System.out.println("*** StringUtils 方法 *****");
        String[] s2 = StringUtils.split(str, ".|");
        System.out.println(Arrays.toString(s2));

        String[] s3 = StringUtils.splitByWholeSeparator(str, ".|");
        System.out.println(Arrays.toString(s3));

        str = "a.|b...";
        String[] s4 = StringUtils.splitPreserveAllTokens(str, ".");
        System.out.println(Arrays.toString(s4));
    }
}

/** 运行结果===>
*** 原生 String.split 方法 *****
[a., b.c]
[a., b.c]
*** 原生 StringTokenizer 方法 *****
a.
b.c
*** StringUtils 方法 *****
[a, b, c]
[a, b.c]
[a, |b, , , ]
*/

总结

  • 直接使用StringUtils最省事(注意上面写的避坑)。
  • 如果是对非特殊字符拆分,比如对字母数字拆分,可以使用原生的split方法。
  • 不要使用StringTokenizer。

感谢阅读本期内容,希望对新入行的你有帮助。

如果你意犹未尽,后面有一个字符串拆分姊妹篇,欢迎继续查看。

往期内容:

  • 我决定写一本Java实用技术,特点实用!实用!还是实用!
  • 【Java实用技术】必备字符串操作之判空
  • 【Java实用技术】java中关于整数的几个冷知识,总有一个你不知道
  • 【Java实用技术】字符串的截取用什么方法好?

我是Pandas,专注Java编程实用技术分享,公众号Java实用技术手册和B站均有视频解说,欢迎来玩。

如果你觉得这篇文章有用,别忘了点赞+关注,一起进步!

本文转载自: 掘金

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

SpringBoot源码分析 自动配置 自定义Starter

发表于 2021-10-31

自动配置

使用 Spring Boot 开发较之以前的基于 xml 配置式的开发,要简捷方便快速的多。而这完全得益于 Spring Boot 的自动配置。下面就通过源码阅读方式来分析自动配置的运行原理。

打开启动类的@SpringBootApplication 注解源码。

1
2
3
4
5
6
7
java复制代码@SpringBootApplication
public class Springboot01DemoApplication {

public static void main(String[] args) {
SpringApplication.run(Springboot01DemoApplication.class, args);
}
}

查看@SpringBootApplication注解的源码,我们发现@SpringBootApplication 注解其实就是一个组合注解,如下:

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复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {


@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};

@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};


@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};


@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};


@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;

}

元注解

  • @Target(ElementType.TYPE):表示当前注解仅能注解在类上。
  • @Retention(RetentionPolicy.RUNTIME):表示当前注解的生存时间为源码、字节码,及 JVM 运行期间。
  • @Documented:表示当前注解的注释信息将显示在 javaAPI 文档中
  • @Inherited:表示当前注解所标注的类的子类将会继承该类上的注解

@SpringBootConfiguration

该注解与@Configuration 注解功能完全相同。即标注该类是Spring配置类

1
2
3
4
5
6
7
8
9
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {

@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;
}

@ComponentScan

用于指定当前应用所要扫描的包。

image.png

@EnableAutoConfiguration

该注解用于完成自动配置,是 Spring Boot 的核心注解。后面要详细解析。这里主要是要对@EnableXxx 注解进行说明。@EnableXxx 注解一般用于开启某一项功能,是为了避免简化配置代码的引入。其是组合注解,一般情况下@EnableXxx 注解中都会组合一个@Import 注解,而该@Import 注解用于导入指定的类,而该被导入的类一般为配置类。其导入配置类的方式常见的有三种:

  • A、直接引入配置类,@Import 中指定的类一般为 Configuration 结尾,且该类上会注解@Configuration,表示当前类为 JavaConfig 类。例如用于开启定时任务的@EnableScheduling 注解的@Import。
  • B、 根据条件选择配置类,@Import 中指定的类一般以 ConfigurationSelector 结尾,且该类实现了 ImportSelector接口,表示当前类会根据条件选择不同的配置类导入。例如,用于开启缓存功能的@EnableCaching 的@Import。
  • C、 动态注册 Bean,@Import 中指定的类一般以 Registrar 结尾,且该类实现了 ImportBeanDefinitionRegistrar接口,用于表示在代码运行时若使用了到该配置类,则系统会自动将其导入。例如,用于开启 AspectJ 自动代理功能的@EnableAspectJAutoProxy 注解的@Import。

查看@EnableAutoConfiguration注解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

Class<?>[] exclude() default {};

String[] excludeName() default {};
}

该注解用于完成自动配置,是 Spring Boot 的核心注解,是一个组合注解。所谓自动配置是指,将用户自定义的类及框架本身用到的类进行装配。其中最重要的注解有两个:

  • @AutoConfigurationPackage:用于导入并装配用户自定义类,即自动扫描包中的类
  • @Import:用于导入并装配框架本身的类

@Import

image.png
由于AutoConfigurationImportSelector类间接实现了ImportSelector,该接口的方法selectImports能够将返回的类的全限定类名集合中的所有类都注册到Spring容器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {


@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);

//重点getAutoConfigurationEntry方法
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}

接着查看getAutoConfigurationEntry方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//重点getCandidateConfigurations方法
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}

接着getCandidateConfigurations方法,如下:

1
2
3
4
5
6
7
8
java复制代码protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//重点
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}

接着查看SpringFactoriesLoader的loadFactoryNames方法

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
java复制代码public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}


private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

/**
* 加载spring.factories文件,封装成Map返回
*/
try {
//FACTORIES_RESOURCE_LOCATION的值为:META-INF/spring.factories
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

image.png

image.png

spring.factories文件的内容如下:

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
properties复制代码# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration

# Failure analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\
org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\
org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer

# Template availability providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider

综上AutoConfigurationImportSelector类的作用就是将META-INF/spring.factories中默认的全限定类名对应的类注册到Spring容器中。

@AutoConfigurationPackage

该注解的定义如下:

1
2
3
4
5
6
7
8
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

}

接着重点查看@Import(AutoConfigurationPackages.Registrar.class)中的AutoConfigurationPackages.Registrar类

1
2
3
4
5
6
7
8
9
10
11
java复制代码static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

/**
* 该方法能够手动往bdMap中添加BeanDefinition
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
//metadata封装了我们的@SpringBootApplication信息
register(registry, new PackageImport(metadata).getPackageName());
}
}

Registrar实现了ImportBeanDefinitionRegistrar接口,该接口的registerBeanDefinitions方法能够往Spring的容器BeanDefinitionMap中注册自定义的BeanDefinition对象。又由于metadata封装了我们的@SpringBootApplication信息,调用getPackageName()返回带有@SpringBootApplication注解类的包名,所以@AutoConfigurationPackage注解的作用就是扫描启动类(即带有@SpringBootApplication注解的类)所在的包及其子包下的类,封装成BeanDefinition对象,注册到Spring容器BeanDefinitionMap中。

image.png

image.png

自定义Starter

自定义starter

1.首先创建工程zdy-spring-boot-starter,引入依赖如下:

1
2
3
4
5
6
7
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
</dependencies>

2.编写javaBean,如下:

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
java复制代码/**
* @EnableConfigurationProperties注解就是开启@ConfigurationProperties注解
*/
@EnableConfigurationProperties(SimpleBean.class)//或者直接使用@Component注解
@ConfigurationProperties(prefix = "simplebean")
public class SimpleBean {

private int id;
private String name;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "SimpleBean{" +
"id=" + id +
", name='" + name + ''' +
'}';
}
}

3.编写配置类MyAutoConfifiguration,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Configuration
@ConditionalOnClass(SimpleBean.class)//当类路径下即classpath下有指定的类(即SimpleBean)的情况就会自动配置
public class MyAutoConfiguration {

static {
System.out.println("MyAutoConfiguration init....");
}

@Bean
public SimpleBean simpleBean() {
return new SimpleBean();
}
}

4.在resources下创建/META-INF/spring.factories

image.png

1
2
properties复制代码org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.lagou.config.MyAutoConfiguration

使用starter

1.引入上面自定义starter工程的依赖,如下:

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.lagou</groupId>
<artifactId>zdy-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

2.在application.properties中配置属性值

1
2
properties复制代码simplebean.id=1
simplebean.name=自定义starter

3.编写测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@RunWith(SpringRunner.class)  //测试启动器,并加载spring boot测试注解
@SpringBootTest
//标记该类为spring boot单元测试类,并加载项目的applicationContext上下文环境
class Springboot01DemoApplicationTests {
//测试自定义starter

@Autowired
private SimpleBean simpleBean;

@Test
public void zdyStarterTest(){
System.out.println(simpleBean);
}
}

启动源码

大家都知道SpringBoot项目启动主要是依靠启动类中的run方法,例如下面的Springboot01DemoApplication类

1
2
3
4
5
6
7
java复制代码@SpringBootApplication
public class Springboot01DemoApplication {

public static void main(String[] args) {
SpringApplication.run(Springboot01DemoApplication.class, args);
}
}

接着查看run方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* 调用静态类,参数primarySource对应的就是SpringbootDemoApplication.class以及main方法中的args
*/
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
//SpringApplication的启动由两部分组成:
//1. 实例化SpringApplication对象
//2. run(args):调用run方法
return new SpringApplication(primarySources).run(args);
}

从以上代码可知,run方法主要分为两个步骤,一个是SpringApplication实例化,另外一个是调用SpringApplication的run方法。

SpringApplication实例化

首先来查看上面代码中的new SpringApplication(primarySources),primarySources参数就是run方法传进来的项目启动类Springboot01DemoApplication。

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
java复制代码public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}

@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {

this.sources = new LinkedHashSet();
this.bannerMode = Mode.CONSOLE;
this.logStartupInfo = true;
this.addCommandLineProperties = true;
this.addConversionService = true;
this.headless = true;
this.registerShutdownHook = true;
this.additionalProfiles = new HashSet();
this.isCustomEnvironment = false;
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");

//项目启动类 SpringbootDemoApplication.class设置为属性存储起来
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

//设置应用类型是SERVLET应用(Spring 5之前的传统MVC应用)还是REACTIVE应用(Spring 5开始出现的WebFlux交互式应用)
this.webApplicationType = WebApplicationType.deduceFromClasspath();

// 设置初始化器(Initializer),最后会调用这些初始化器
//所谓的初始化器就是org.springframework.context.ApplicationContextInitializer的实现类,在Spring上下文被刷新之前进行初始化的操作
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

// 设置监听器(Listener)
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

// 初始化 mainApplicationClass 属性:用于推断并设置项目main()方法启动的主程序启动类
this.mainApplicationClass = deduceMainApplicationClass();
}

从上述源码可以看出,SpringApplication的初始化过程主要包括4部分,具体说明如下。

  • (1)this.webApplicationType = WebApplicationType.deduceFromClasspath()用于判断当前webApplicationType应用的类型。deduceFromClasspath()方法用于查看Classpath类路径下是否存在某个特征类,从而判断当前webApplicationType类型是SERVLET应用(Spring 5之前的传统MVC应用)还是REACTIVE应用(Spring 5开始出现的WebFlux交互式应用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* @return 从 classpath 上,判断 Web 应用类型。
*/
static WebApplicationType deduceFromClasspath() {
// WebApplicationType.REACTIVE 类型 通过类加载器判断REACTIVE相关的Class是否存在
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) // 判断REACTIVE相关的Class是否存在
&& !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
// WebApplicationType.NONE 类型
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) { // 不存在 Servlet 的类
return WebApplicationType.NONE;
}
}
/**
* 最终会调用改行代码
*/
// WebApplicationType.SERVLET 类型。可以这样的判断的原因是,引入 Spring MVC 时,是内嵌的 Web 应用,会引入 Servlet 类,即DispatcherServlet
return WebApplicationType.SERVLET;
}
  • (2)this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class))用于SpringApplication应用的初始化器设置。在初始化器设置过程中,会使用Spring类加载器SpringFactoriesLoader从META-INF/spring.factories类路径下的META-INF下的spring.factores文件中获取所有可用的应用初始化器类ApplicationContextInitializer。
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
java复制代码/**
* 获得指定类类对应的对象们。
*
* @param type 指定类
* @param <T> 泛型
* @return 对象们
* // // 这里的入参type是:org.springframework.context.ApplicationContextInitializer.class
r
* // ApplicationListener
*/
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return getSpringFactoriesInstances(type, new Class<?>[] {});
}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
// 加载指定类型对应的,在 `META-INF/spring.factories` 里的类名的数组
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
//org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
//org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
// 根据names来进行实例化
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
// 对实例进行排序
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

接着查看SpringFactoriesLoader.loadFactoryNames(type, classLoader),如下:

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
java复制代码public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());

}

/**
* 从META-INF/spring.factories中加载类型为org.springframework.context.ApplicationContextInitializer的bean
*/
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

try {
//FACTORIES_RESOURCE_LOCATION为META-INF/spring.factories
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

image.png

从META-INF/spring.factories类路径下的META-INF下的spring.factores文件中获取所有可用的应用初始化器类ApplicationContextInitializer,一共有两个,如下:

1
2
复制代码org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
  • (3)this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class))用于SpringApplication应用的监听器设置。监听器设置的过程与上一步初始化器设置的过程基本一样,也是使用SpringFactoriesLoader从META-INF/spring.factories类路径下的META-INF下的spring.factores文件中获取所有可用的监听器类ApplicationListener。

代码跟第二步的类似,从META-INF/spring.factories类路径下的META-INF下的spring.factores文件中获取所有可用的监听器类ApplicationListener。,如下图所示

image.png

  • (4)this.mainApplicationClass = this.deduceMainApplicationClass()用于推断并设置项目main()方法启动的主程序启动类

run方法

接着查看run方法,如下:

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
java复制代码public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
//SpringApplication的启动由两部分组成:
//1. 实例化SpringApplication对象
//2. run(args):调用run方法
return new SpringApplication(primarySources).run(args);
}

/**
* 一共9个步骤
*/
public ConfigurableApplicationContext run(String... args) {
// 创建 StopWatch 对象,并启动。StopWatch 主要用于简单统计 run 启动过程的时长。
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 初始化应用上下文和异常报告集合
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 配置 headless 属性
configureHeadlessProperty();

/**
* 重点在下面额九个步骤
*/

// (1)获取并启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
// 创建 ApplicationArguments 对象 初始化默认应用参数类
// args是启动Spring应用的命令行参数,该参数可以在Spring应用中被访问。如:--server.port=9000
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

//(2)项目运行环境Environment的预配置
// 创建并配置当前SpringBoot应用将要使用的Environment
// 并遍历调用所有的SpringApplicationRunListener的environmentPrepared()方法
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);

configureIgnoreBeanInfo(environment);
// 准备Banner打印器 - 就是启动Spring Boot的时候打印在console上的ASCII艺术字体
Banner printedBanner = printBanner(environment);

// (3)创建Spring容器
context = createApplicationContext();
// 获得异常报告器 SpringBootExceptionReporter 数组
//这一步的逻辑和实例化初始化器和监听器的一样,
// 都是通过调用 getSpringFactoriesInstances 方法来获取配置的异常类名称并实例化所有的异常处理类。
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);


// (4)Spring容器前置处理
//这一步主要是在容器刷新之前的准备动作。包含一个非常关键的操作:将启动类注入容器,为后续开启自动化配置奠定基础。
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);

// (5):刷新容器
refreshContext(context);

// (6):Spring容器后置处理
//扩展接口,设计模式中的模板方法,默认为空实现。
// 如果有自定义需求,可以重写该方法。比如打印一些启动结束log,或者一些其它后置处理
afterRefresh(context, applicationArguments);
// 停止 StopWatch 统计时长
stopWatch.stop();
// 打印 Spring Boot 启动的时长日志。
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// (7)发出结束执行的事件通知
listeners.started(context);

// (8):执行Runners
//用于调用项目中自定义的执行器XxxRunner类,使得在项目启动完成后立即执行一些特定程序
//Runner 运行器用于在服务启动时进行一些业务初始化操作,这些操作只在服务启动后执行一次。
//Spring Boot提供了ApplicationRunner和CommandLineRunner两种服务接口
callRunners(context, applicationArguments);
} catch (Throwable ex) {
// 如果发生异常,则进行处理,并抛出 IllegalStateException 异常
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

// (9)发布应用上下文就绪事件
//表示在前面一切初始化启动都没有问题的情况下,使用运行监听器SpringApplicationRunListener持续运行配置好的应用上下文ApplicationContext,
// 这样整个Spring Boot项目就正式启动完成了。
try {
listeners.running(context);
} catch (Throwable ex) {
// 如果发生异常,则进行处理,并抛出 IllegalStateException 异常
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
//返回容器
return context;
}

从上述源码可以看出,项目初始化启动过程大致包括以下部分:

  • 第一步:获取并启动监听器

this.getRunListeners(args)和listeners.starting()方法主要用于获取SpringApplication实例初始化过程中初始化的SpringApplicationRunListener监听器并运行。

  • 第二步:根据SpringApplicationRunListeners以及参数来准备环境

this.prepareEnvironment(listeners, applicationArguments)方法主要用于对项目运行环境进行预设置,同时通过this.configureIgnoreBeanInfo(environment)方法排除一些不需要的运行环境

  • 第三步:创建Spring容器根据webApplicationType进行判断, 确定容器类型,如果该类型为SERVLET类型,会通过反射装载

对应的字节码,也就是AnnotationConfigServletWebServerApplicationContext,接着使用之前初始化设置的context(应用上下文环境)、environment(项目运行环境)、listeners(运行监听器)、applicationArguments(项目参数)和printedBanner(项目图标信息)进行应用上下文的组装配置,并刷新配置

  • 第四步:Spring容器前置处理

这一步主要是在容器刷新之前的准备动作。设置容器环境,包括各种变量等等,其中包含一个非常关键的操作:将启动类注入容器,为后续开启自动化配置奠定基础

  • 第五步:刷新容器

开启刷新spring容器,通过refresh方法对整个IOC容器的初始化(包括bean资源的定位,解析,注册等等),同时向JVM运行时注册一个关机钩子,在JVM关机时会关闭这个上下文,除非当时它已经关闭

  • 第六步:Spring容器后置处理

扩展接口,设计模式中的模板方法,默认为空实现。如果有自定义需求,可以重写该方法。比如打印一些启动结束log,或者一些其它后置处理。

  • 第七步:发出结束执行的事件

获取EventPublishingRunListener监听器,并执行其started方法,并且将创建的Spring容器传进去了,创建一个ApplicationStartedEvent事件,并执行ConfigurableApplicationContext 的publishEvent方法,也就是说这里是在Spring容器中发布事件,并不是在SpringApplication中发布事件,和前面的starting是不同的,前面的starting是直接向SpringApplication中的监听器发布启动事件。

  • 第八步:执行Runners

用于调用项目中自定义的执行器XxxRunner类,使得在项目启动完成后立即执行一些特定程序。其中,Spring Boot提供的执行器接口有ApplicationRunner 和CommandLineRunner两种,在使用时只需要自定义一个执行器类实现其中一个接口并重写对应的run()方法接口,然后Spring Boot项目启动后会立即执行这些特定程序

下面,通过一个Spring Boot执行流程图,让大家更清晰的知道Spring Boot的整体执行流程和主要启动阶段:

image.png

本文转载自: 掘金

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

看完这篇G1垃圾收集器的总结就足以吊打面试官了~ 基础概念

发表于 2021-10-31

基础概念

定义: Garbage First,垃圾优先,主要面向服务端应用的垃圾收集器。
开启命令: -XX:+UseG1GC
目标: “停顿时间模型”的收集器:能够支持指定所在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。
适用场景: 适用于大内存、多CPU的机器。

设计理念

跳出之前要不收集新生代,要不收集老年代的樊笼,G1面向所有的堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块区域中存放的垃圾数量最多,回收效益最大。

区域划分

Region区域

定义: 将Java堆划分为多个大小相等的Region,每个Region都可以是新生代、老年代。G1收集器根据角色的不同采用不同的策略去处理
大小: 2 的N次幂,1MB~32MB
配置参数: -XX:G1HeapRegionSize,不指定G1会根据堆大小自行指定

Humongous区域(Region中的一部分)

定义: 专门用来存储大对象(超过Region容量一半的对象即为大对象),超过整个Region区域的会放在多个连续的Humongous区。
回收身份: G1把Humongous当做老年代的一部分
在这里插入图片描述

垃圾收集

借助R大的回答,从最高层看,G1的Collector一侧就是两个大部分,并且这两个部分可以相对独立执行。

  • 全局并发标记(global concurrent marking)
  • 拷贝存活对象(evacuation)

全局并发标记(global concurrent marking)

基于SATB(Snapshot At The Begining)形式的并发标记。

1、初始标记(STW)

G1对根进行标记。扫描根集合,标记所有从根集合可直接到达的对象(CMS的初始标记也是类似)并将它们的字段压入扫描栈(marking stack)中等到后续扫描。G1使用外部的bitmap来记录mark信息,而不是用对象头的mark word里的mark bit。在分代式G1模式中,初始标记阶段借用 yong GC的暂停,因而没有额外的、单独的暂停阶段。

为什么初始标记阶段是借用Yong GC的暂停做的?

从逻辑上将“全局并发标记”和“拷贝存活对象”是相对独立的,但是“全局并发标记”阶段的“初始标记”阶段又和Yong GC要做的事情有重叠—遍历根集合,所以在实现上把他们安排在一起做,Yong GC期间可以顺带做,也可以不做。

2、并发标记

G1在整个堆中查找可以访问的(存活的)对象,递归扫描整个堆里的对象图。每扫描到一个对象就对其进行标记,并压入扫描栈中。重复扫描过程直到扫描栈清空。过程中还会扫描SATB 写屏障(write barrier)所记录下的引用,SATB相关下文会介绍。

3、最终/重新标记(STW)

处理在并发标记阶段剩余未处理的SATB写屏障的记录。同时此阶段也进行弱引用处理(reference proccessing),**这个暂停与CMS的remark有一个本质的区别,这个暂停只需要扫描SATB buffer(将这些旧引用作为根重新扫描一遍,避免漏标),而CMS的remark需要重新扫描mod-union table里的dirty card外加整个根集合,**而此时整个Yong 区不管对象死活都会被当做根集合的一部分,因而CMS remark有可能会非常慢。

4、清理(cleanup)(STW)

清点和重置标记状态,与mark-sweep中的sweep阶段类似,但不是在堆上sweep实际对象,而是在marking bitmap里统计每个Region被标记为活的对象有多少。这个阶段如果发现完全没有活对象的Region,就会将其整体回收到可分配Region列表中(空闲列表)。

拷贝存活对象 (Evacuiation)(STW)

也叫筛选回收/清理(STW),负责把一部分Region里的活对象拷贝到空Region里去,然后回收原本的Region的空间。
此阶段可以自由选择任意多个Region来独立收集构成收集集合(Collection Set,CSet),靠每个Region的RSet实现。这是Regional garbage collector的特点。
确定完CSet后肯定就要复制了,其实就和ParallelScavenge的Young GC算法类似,采用并行复制算法把CSet里每个Region里的活对象拷贝到新的Region里,整个过程完全暂停。
“Garbage-First Garbage Collection”论文中讲到,CSet的选定完全靠统计模型找出收益最高、开销不超过用户指定的上限的若干个Region,由于每个Region都有RSet覆盖,要单独evacuate任意一个或者多个Region都没问题。

分代式G1模式下有两种选定CSet的子模式:

  • Yong GC:选定所有Yong区里的Region。通过控制Yong区的Region个数来控制GC的开销。
  • Mixex GC:选定所有Yong Gen里的Region,外加根据global concurrent marking统计得出收益最高的若干old区的Region。在用户指定的开销目标范围内尽可能选择收益高的old区Region

可以看到Yong区Region总是在CSet内,因此分代式G1不维护从Yong区Region出发的引用设计的RSet更新。

工作流程总结

分代式G1(只有分代式G1,其它的目前还没有)的正常工作流程就是在YongGC与Mixed GC之间视情况进行切换,背后定期做全局并发标记。初始化标记默认搭在YongGC上执行;
当全局并发标记正在工作时,G1不会选择做Mixed GC,反之MixedGC正在进行中G1也不会启动初始化标记。
在正常的工作流程中没有Full GC的概念,Old区的收集完全靠MixedGC来完成。

问题

如何保证收集线程与用户线程互不干扰的运行?

在算法实现细节中说过了“三色标记”算法,这个算法阐明了对象在垃圾收集过程中所有的状态,白、黑、灰。垃圾收集过程中,对象的状态可能会出现黑色对象引用了白色对象或者灰色对象与白色对象之间的引用断开了(当两种条件同时满足时就会出现漏标/错标的情况),其实也就是垃圾收集过程中原有的对象结构被打破了,解决这种情况的方案有两种:增量更新、原始快照,G1 GC使用的是原始快照(SATB),CMS使用的是增量更新(incremental update)

SATB( Snapshot At The Begining)

SATB是维持并发GC正确性的一个手段,抽象的说就是

  • 在一次GC开始的时候活的对象就被认为是活的,此时的对象图形成一个逻辑“快照”;
  • 在GC过程中新分配的对象都当做是活的,其他不可到达的对象就是死的。

G1如何知道哪些对象是GC开始后新分配的呢?

每个Region记录着两个TAMS(Top At Mark Start)指针,分别为prevTAMS和nextTAMS,G1在并发标记期间会让新分配的对象在TAMS上分配。在TAMS以上的对象就是新分配的,因而被视为隐式标记。

G1的并发标记用了两个bitmap:

  • prevBitmap记录第n-1轮并发标记所得的对象存活状态,由于第n-1轮并发标记已经完成,这个bitmap的信息可以直接使用
  • nextBitmap记录第n轮concurrent marking的结果,这个bitmap是当前将要或者正在进行并发标记的结果,还不能使用

对应的每个Region都有这么几个指针:
在这里插入图片描述

top是该Region的当前分配指针,[bottom,top)是当前Region已用的部分,[top,end)是尚未使用的可分配空间。

  1. [bottom,preTAMS)这里的对象存活信息可以通过prevBitmap来得知
  2. [prevTAMS,nextTAMS)这部分里的对象在第n-1轮并发标记中隐式存活的
  3. [nextTAMS,top)这部分的对象是在第n轮并发标记中隐式存活的

G1如何处理在并发标记阶段用户线程对对象引用的修改呢?

SATB write barrier,是对“对引用类型字段赋值”这个动作的环切,也就是说赋值前后都在barrier覆盖的范畴内。在赋值前的部分叫做pre-write barrier,在赋值后的叫作post-write barrier。在JVM记忆集文章中我们也讲过,在G1 GC之前其他的垃圾收集器都只是使用了post-write barrier。

SATB要维持“在GC开始时活的对象”的状态这个逻辑snapshot。除了从root出发把整个对象图mark下来之外,其实只需要用pre-write barrier把每次引用关系变化时旧的引用值记下来就好了。这样等并发标记到某个对象时,这个对象的所有引用类型字段的变化全都有记录,就不会漏掉任何在snapshot里活着的对象。当然,很有可能有对象在snapshot中是活的,但是随着并发GC的进行,它已经死了但SATB还是会让它活过这次GC,这时候就会产生floal garbage.

因此在G1 GC中,整个write barrier+oop_field_store是这样的:

1
2
3
4
5
cpp复制代码void oop_field_store(oop* field, oop new_value) {  
pre_write_barrier(field); // pre-write barrier: for maintaining SATB invariant
*field = new_value; // the actual store
post_write_barrier(field, new_value); // post-write barrier: for tracking cross-region reference
}

pre-write barrier的过程如下:

1
2
3
4
5
6
7
8
cpp复制代码void pre_write_barrier(oop* field) {  
oop old_value = *field;
if (old_value != null) {
if ($gc_phase == GC_CONCURRENT_MARK) { // SATB invariant only maintained during concurrent marking
$current_thread->satb_mark_queue->enqueue(old_value);
}
}
}

enqueue动作的实际代码是在G1SATBCardTableModRefBS::enqueue(oop pre_val),它判断当前是否在并发标记阶段用的是JavaThread::satb_mark_queue_set().is_active() ,SATBMarkQueueSet只有在并发标记阶段才会被置为active。
CMS的增量更新设计使得它在Remark阶段必须重新扫描所有线程栈和整个Yong区作为Root;而G1的SATB设计在Remark阶段则只需要扫描剩下的satb_mark_queue

为何在pre-write barrier中只是把旧的引用放入了SATBMarkQueue,为何没有压入标记栈中?

为了减少write barrier对用户线程性能的影响,G1将一部分原本要在barrier里做的事情挪到了别的线程中并发执行,实现这种分离的方式就是通过logging形式的 write barrier实现的,用户线程只在barrier里把要做的事情信息记录到一个队列中,由另外的线程从队列中取出信息批量完成剩余的动作。

以SATB write barrier为例,每个Java线程由一个独立的、定长的SATBMarkQueue,用户线程在barrier里只把old_value压入该队列中。一个队列满了之后,这个队列就会被加到全局的SATB队列集合SATBMarkQueueSet里等待处理,然后给对应的Java线程换一个新的、干净的队列继续执行下去。
并发标记过程中会定期检查全局SATB队列集合的大小。当SATBMarkQueueSet****中队列数量超过一定阈值后,并发标记线程就会处理集合里所有的队列,****把队列里记录的每个对象都标记上,并将其引用字段压到标记栈上等后边做进一步标记。
在这里插入图片描述

跨Region引用如何解决?

JVM使用记忆集(Remember Set)来避免全堆最为GC Roots扫描,关于记忆集的内容前边的文章中已经讲过,如果你已经忘记了可以返回去看看,JVM记忆集相关。

Remember Set

G1的堆与其它GC一样有一个覆盖整个堆的cart table,从逻辑上说,G1的RSet是应该每个Region都有一份,这个RSet记录的是从别的Region指向该Region的card。所以说这是一种“ponits-into”的RSet。
用card table实现的RSet通常是points-out的,也是就是说card table要记录的是从它覆盖的范围出发指向别的范围的指针,以分代式GC的card table为例,要记录old区指向yong区的跨代指针,被标记的card是old区范围内的。

G1 GC在ponits-out的card table之上再加了一层结构来构成points-into RSet:每个Region会记录下到底哪些别的Region有指向自己的指针,而这些指针分别在哪些card的范围内。这个RSet其实就是一个Hash Table,key是别的Region的起始地址,value是一个集合,里面的元素是card table的index,card table又对应一个card page。下图形象的秒数了points-into RSet的关系,原文地址为Tips for Tuning the Garbage First Garbage Collector
在这里插入图片描述

举例:
如果Region A的RSet中有一个key是Region B,value里有index为1234的card,那么它的意思就是RegionB的一个card里有引用指向Region A。所以对A来说,该RSet记录的是points-into的关系,而card table仍然记录了points-out的关系(不要太纠结points-into和points-out)。

缺点

G1的Region区域过多会导致G1收集器比其他收集器占有的内存多,据经验,G1收集器要耗费大约相当于Java堆内存10%到20%的额外内存来维持工作。

RSet是如何被更新的

前边也已经讲过了,通过write-barrier来实现,G1通过post-write barrier来更新RSet。
理论上post-write barrier的逻辑

  1. 首先会先获取指向该字段卡卡页
  2. 判断卡页是否已经被标记为脏页,如果已经为脏页,则不处理
  3. 将当前卡页变“脏”,以防多个字段同属于一个卡页重复执行此逻辑
  4. 判断是否为Yong 区,如果为Yong区则不处理,前边已经讲过了分代式G1下,Yong GC和Mixed GC都会的处理Yong GC,因此过滤到从Yong区出发的引用涉及的RSet的维护(既然GC都会扫描,干嘛还浪费时间区更新呢)
  5. 找到卡页所属的Region
  6. 找到堆中引用Region的Region
  7. 更新cart table及Region的RSet

实际上post-write barrier也利用了前边讲SATB中的logging barrier,与SATBMarkQueue类似,每个Java线程由一个DirtyCardQueue,有一个全局的DirtyCardQueueSet,更新RSet的动作交由多个ConcurrentG1RefineThread并发完成,当全局集合中的队列个数超过指定阈值后,ConcurrentG1RefineThread就会取出若干个队列,遍历每个队列记录的card并将card加到对应的Region的RSet里去。

如何建立起可靠的停顿预测模型?

可以通过-XX:MaxGCPauseMillis参数指定预期的停顿时间,G1 GC的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。也就是说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。

优点

1、G1收集器并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。设计理念与之前的垃圾收集器也不相同,由以前追求一次把整个Java堆全部清理干净到追求能够应付应用的内存分配速率。
2、可指定期望停顿时间
3、不会产生空间碎片,在程序为大对象分配内存时不容易出现找不到连续内存而提前触发下一次GC或者Full GC
4、处理跨代引用时使用原始快照,避免了在重标记阶段像CMS一样扫描所有的“脏”页,只需要扫描SATB buffer中的引用即可。

缺点

  1. 如果Mixed GC无法跟上程度分配内存的速度,导致Old区域填满无法分配内存时,就会切换到G1之外的Serial Old GC来收集整个Java堆(包括Yong、OId、Permgen),也就是Full GC,这种状态的G1就和-XX:+UseSerialGC的Full GC一样(背后的核心代码是两者公用的)

G1 GC的System.gc()默认是Full GC,也就是Serial Old GC,只有加上-XX:+ExplicitGCInvokesConcurrent时才会用自身的并发GC来执行System.gc(),此时System.gc()的作用是强行启动一次global concurrent marking;一般情况下暂停中只会做初始标记然后就返回了,接下来的并发标记还是照常并发执行。

注意事项

不要把 -XX:MaxGCPauseMillis设置的太低,设置的太低会导致G1垃圾收集跟不上对象的分配,可能会导致垃圾堆积,最后引发Full GC。

相关参数

参数 含义
-XX:G1HeapRegionSize=n 设置Region的大小,并非最终值
-XX:MaxGCPauseMills 设置G1收集过程的目标时间,默认值200,不是硬性条件
-XX:G1NewSizePercent 新生代最小值,默认值5%
-XX:G1MaxNewSizePercent 新生代最大值,默认值60%
-XX:ParallelGCThreads STW期间,并行GC线程数
-XX:ConcGCThreads=n 并发标记阶段,并行执行的线程数
-XX:InitiatingHeapOccupancyPercent 设置触发标记周期的 Java 堆占用率阈值。默认值是45%。这里的java堆占比指的是non_young_capacity_bytes,包括old+humongous

本文转载自: 掘金

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

分布式事务如何保证接口请求顺序性?

发表于 2021-10-31

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

先抛出一个问题大家思考一下:在分布式系统中,我们如何保证多个请求的顺序性问题,比如有A/B两个系统,系统A在一次订单业务处理中,向B系统发送三次请求,先进行插入订单操作,然后对订单状态进行修改,最后增加用户积分。

但是这三次请求分别落在了不同的机器上,并且插入订单的操作由于一些意外导致延迟,修改订单操作先执行了,但是此时并没有订单信息,也就会出现我们期望之外的结果了。

image.png

那面对这种情况我们应该如何避免呢,这就需要了解花哥今天说的:分布式服务中,如何保证请求的顺序性。

增加接入服务

我们可以在A、B系统之间增加一个接入服务,比如上述场景中,一次完整的业务中包含了三个请求,在每一次请求时,都携带一个唯一id作为标识传入到接入服务中,接入服务根据id,将对应的请求全部分发到某一台机器上,这样这三次请求就能全部由同一台服务来完成操作。

image.png

增加队列

增加【接入服务后】,能够让相同id的请求分发到同一台机器,只要系统A顺序发送,那系统B也会顺序接收,但是还会出现一个问题,就是如果系统B中是以多线程的方式处理接收到的请求时,这样还是会出现消费顺序的问题。

其实解决这个问题的思路就是增加一个内存队列,将相同id的请求,全部丢到一个内存队列中,让这几个请求进行排队消费,这样就可以保证多线程下的顺序性。

image.png

分布式锁保证顺序性

在一般的情况下,上述两步基本满足了我们的业务需求,但其实还是会存在极端情况导致顺序性问题,比如接入服务分发过程中,将顺序异常分发到系统B中。对于一些需要100%保证接口顺序性的系统,我们可以通过接入分布式锁来实现。

image.png

如上图,现在三条请求都已到达系统B中,每个请求中除了携带唯一id外,还带上本次请求的顺序编号ord,并且在每个请求执行业务逻辑之前,都会去请求zookeeper锁,只有获取到锁的线程才能执行:

  • 如果删除请求率先获取锁,它在执行业务逻辑前,会去数据库中判断新增请求、修改请求是否已经完成,如果没有完成,就将锁释放;
  • 接下来新增请求获取到锁,它判断当前请求的执行顺序ord=1,可以执行,因此就会去数据库中插入数据,并释放锁;
  • 接下来如果删除请求获取到锁,它依旧会去数据库中查询,发现修改请求还没有完成操作,因此继续释放当前锁资源;
  • 修改请求获取到锁,发现数据库中已经存在记录,因此对该订单状态修改,并释放锁;
  • 删除请求获取锁,并完成删除操作,最后释放锁。

缺陷

通过以上这三个步骤,可以实现100%的消费顺序性,但是这一通操作下来,系统的吞吐量严重下降,特别是分布式锁的引入,成为系统并发的瓶颈。

除此之外,我们还需要保证引入的中间件可用性,衍生出一连串的解决方案,提升了系统的复杂性,对日后的维护都是一种挑战。因此,最好的方案就是能不能在业务逻辑上解决,比如将几个步骤合并为一步或者用其他方案代替,从而在根上避免。

本文转载自: 掘金

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

老婆都能懂的云原生之istio的概念 作者:肥嘟嘟左卫门熊

发表于 2021-10-31

作者:肥嘟嘟左卫门熊

网上已经有很多很好的istio相关的文章,写这篇文章是因为他们都太长了,讲了很多概念,虽然很细但是难以先形成一个完整的认识。

故写一篇小白文解释一下istio是什么,怎么用?

不过至少需要具备一点k8s的知识,知道k8s是什么

pic

1.首先说一下,istio是什么

按照isito文档中给出的定义:

Istio提供一种简单的方式来建立已部署的服务的网络,具备负载均衡,服务到服务认证,监控等等功能,而不需要改动任何服务代码。

??

Istio服务网格逻辑上分为数据面板和控制面板。

  • 数据面板由一组智能代理(Envoy)组成,代理部署为边车,调解和控制微服务之间所有的网络通信。
  • 控制面板负责管理和配置代理来路由流量,以及在运行时执行策略。

那么问题来了

  1. 这里已部署的服务指的是什么?
  2. 简单的方式指的是什么?
  3. 怎么实现的具备负载均衡,服务到服务认证,监控等等功能?

我们一一解答~

1.已部署的服务指的是什么?

已部署的服务即任意k8s服务,比如

k8s

这里涉及到一些k8s的概念

  • Service是k8s中的一种资源,通常用来为pod提供稳定公开的网络服务,将流量导入对应的pod。
  • Pod是一组运行的容器,容器里面是用户提供的服务。

当用户访问 x.x.x.x:32010时,流量进入32010端口的Service1,通过Service1转入Pod1 ;

如果用户访问的是x.x.x.x:32010/backend,则后端流量会继续通过Pod1内的nginx配置,转入后端的Service2;

比如

proxy_pass http://Service2.namespace.svc.cluster.local.:80;

其中http://Service2.namespace.svc.cluster.local.:80是k8s资源Service的域名访问方式

(在 Kubernetes 中,Service 和 Pod 都会被分配对应的 DNS A 记录(从域名解析 IP 的记录),因此可以通过上述域名访问service资源。)

2.简单的方式指的是什么?

即通过对所有pod注入sidecar,从而调解和控制服务之间所有的网络通信。

这里再稍微说一下sidecar是什么?

  • Sidecar即边车模式

所谓的边车模式,对应于我们生活中熟知的边三轮摩托车。也就是说,我们可以通过给一个摩托车加上一个边车的方式来扩展现有的服务和功能。这样可以很容易地做到 “ 控制 “ 和 “ 逻辑 “ 的分离。

具体来说,你可以理解为,边车就有点像一个服务的 Agent,这个服务所有对外的进出通讯都通过这个 Agent 来完成

。

由“边车”来实现监控,日志等与业务逻辑没有关系的控制功能。

motor

具体到本服务中,即给pod里都加上了一个sidecar容器

istio

实现方式很简单,这里举其中一个方式为例,命名空间添加标签,给命名空间添加标签,指示 Istio 在部署应用的时候,自动注入 Envoy 边车代理

kubectl label namespace default istio-injection=enabled

之后,当我们在default命名空间下创建pod,则会自动添加一个sidecar代理,pod内容器数量会从x个变为x+1个

1
2
3
4
5
6
7
8
csharp复制代码# Before
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
pod-558b8b4b76-2llld 1/1 Running 0 2m41s
# After
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
pod-558b8b4b76-v3das 2/2 Running 0 2m41s

这便是istio的数据面板(下图中的Proxy即为sidecar)

data

proxy由Envoy(一种网络代理sidecar)实现,Envoy相当于打工人,目前只是把打工人安插在了每个工作单位(pod)里。

man

3.怎么实现的具备负载均衡,服务到服务认证,监控等等功能?

打工人sidecar通常根据VirtualService和DestinationRule这两个crd,描述istio的控制面板,实现对流量的控制

  • VirtualService 用于对流量进行路由

下面的例子是将带有【header】【id:exampleid】的http请求转到版本v2,其他http请求找发到版本v1的VirtualService。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yaml复制代码apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: service1
namespace: testns
spec:
hosts: #接收流量的目标主机
- service1
http:
- match:
- headers:
id:
exact: exampleid
route:
- destination:
host: service1
subset: v2
- route:
- destination:
host: service1
subset: v1
  • DestinationRule 用于定义流量路由后的策略,比如负载均衡的方式等

下面的例子将service1按labels拆分为两个版本,从而将VirtualService的流量路由转发到对应版本的pod中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: service1 #DestinationRule的名称
namespace: testns #DestinationRule的命名空间
spec:
host: service1 #注册中心的服务名,如k8s中的Service2.namespace.svc.cluster.local,如果使用短名称Service2,则会自动根据命名空间补全
subsets: #一组Service Endpoint的子集
- name: v1 #子集名,可以用于VirtualService的路由规则
labels: #用于筛选子集的标签集
version: v1
- name: v2
labels: #用于筛选子集的标签集
version: v2

通过上述配置,即完成了一个将带指定头部的请求导入v2版本服务,其他请求导入v1版本服务的灰度发布istio配置!

总结

下图是宏观视图,可以更形象的展示Istio两个面板的功能和合作:

whole

通过数据面板sidecar负责对数据进行控制,控制面板来配置路由策略,完成服务网格的搭建。

本文只介绍了最基础的istio使用方式和大概的istio架构~ 如果对istio有兴趣可以直接从官方文档入手进行深入了解~

注释

  • endpoints的概念: 即每个pod的ip和port,比如10.222.76.153:8081,交给service管理,访问service实际访问的就是背后endpoint集合之一

kubernetes.io/docs/refere…

stackoverflow.com/questions/5…

  • crd的概念:CustomResourceDefinition ,对k8s资源的一种自定义,达到对 Kubernetes API 进行扩展满足需求的目的。本文中的VirtualService和DestinationRule都属于crd
1
2
csharp复制代码# 通过下面的命令可以看到默认命名空间下的crd资源有哪些
kubectl get crd

本文使用 文章同步助手 同步

本文转载自: 掘金

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

性能分析之一个简单 Java 线程 dump 分析示例 一、

发表于 2021-10-31

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。


一、背景

Java 应用怎么通过方法定位到代码的具体步骤,下面通过一个具体的例子来说明。

二、分析步骤

  1. 使用 TOP 命令找到谁在消耗 CPU 比较高的进程,例如:pid = 1232
  2. 使用 top -p 1232 单独监控该进程
  3. 输入大写的 H 列出当前进程下的所有线程
  4. 查看消耗 CPU 比较高的线程,并看线程编号,例如:2399
  5. 使用 jstack 1232>pagainfo.dump 获取当前进程下的 dump 线程信息
  6. 将第四步获取的线程编号 12399 转换成 16 进制 306f (printf “%x\n” 12399)
  7. 根据 306f 在第 5 步获取的栈信息中查找 tid=0x306 的线程
  8. 定位代码位置(根据打印出来的堆栈信息查看代码所在位置)

注意:

从操作系统打印出的虚拟机的本地线程看,本地线程数量和 Java 线程堆栈中的线程数量相同, 说明二者是一一对应的。只不过 java 线程中的 nid 中用 16 进制来表示, 而本地线程中的 id 用十进制表示。

三、一个例子

1、编写demo

一个 Springboot 例子:

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
java复制代码/**
* @author 7d
* @Title: PageIndex
* @Description: 使用命令top-->top -p PID -- H jstack pid
* @date 2019/11/9 / 10:23
*/

@Log4j2
@Controller
public class PageIndexController {

/**
* 测试demo
* @return
*/
@GetMapping("/7d")
@ResponseBody
public Object topDemo() {
StudentInfo stInfo = new StudentInfo();
stInfo.setName("topJava");
stInfo.setAge(30);
stInfo.setDes("冠礼之年测试java通过top命令查看jvm");
stInfo.setGrade("7DGroup");

for (int i = 0; i < 100000; i++) {
i++;
try {
log.info("my is print" + i);
Thread.sleep(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return stInfo;
}

/**
* 调试
* @return
*/
@GetMapping("/7ddemo")
@ResponseBody
public Object topDemo1() {
StudentInfo stInfo = new StudentInfo();
stInfo.setName("topJava");
stInfo.setAge(30);
stInfo.setDes("冠礼之年测试java通过top命令查看jvm");
stInfo.setGrade("7DGroup");

return stInfo;
}
}

2、部署demo

上传到 linux 服务器运行:

1
bash复制代码java -jar jdktop-0.0.1-SNAPSHOT.jar

在这里插入图片描述

打开浏览器访问:

在这里插入图片描述

3、问题现象

可以使用 Jmeter 压测,方便咱们进行性能定位,也可以使用 idea 简单发起请求:
在这里插入图片描述

点击请求:
在这里插入图片描述

访问写好的请求 http://ip:port/7d 后,通过查看日志显示系统后台运行,但前端一直处于等待状态。
在这里插入图片描述

打开 Linux 服务器,敲 top 命令查看消耗 CPU 中的 Java 进程,通过观察该进程在操作系统中消耗 CPU 不是很高,这里演示下上面操作步骤:
在这里插入图片描述

使用命令 top -p PID 即可看到该进程下的线程信息:
在这里插入图片描述

接下来使用 jstack 命令进行,打出 dump 信息

1
bash复制代码[root@localhost bin]# ./jstack 93114 >/home/7d/7djava.dump

在这里插入图片描述

切换到 dump 生成的目录下:
在这里插入图片描述

我们可以下载到本地进行查看或者使用 Jdk 自带的 jvisualvm 工具查看。这里演示方便直接在 Linux 服务器上打开:
​

1
bash复制代码[root@localhost 7d]# vim  7djava.dump

在这里插入图片描述

这里演示需要,就找到下图中的 java 线程:
​
在这里插入图片描述

PID 十进制转换 16 进制:
在这里插入图片描述

在打开 dump 文件中通过 vim 搜索命令查找:
在这里插入图片描述

从上面可以看出目前线程正处于 TIMED_WAITING 状态,表示当前被挂起一段时间,时长为参数中指定的时长,如Thread.sleep(1000)。因此该线程当前不消耗CPU。

这里简单介绍下线程怎么看:
在这里插入图片描述

在这里插入图片描述

1
bash复制代码http-nio-8080-exec-11" #30 daemon prio=5 os_prio=0 tid=0x00007fadf8001000 nid=0x16bf8 waiting on condition [0x00007fae6c7d0000]
  • 线程名称:”http-nio-8080-exec-11”
  • 线程优先级:prio=5
  • 线程对应的本地线程id号:nid=0x16bf8
  • 线程的状态:waiting
  • 线程占用内存地址:[0x00007fae6c7d0000]

接下来我们找到代码的包名与代码行:
在这里插入图片描述

从下面信息得出:
在这里插入图片描述

再次查看源码:
在这里插入图片描述

四、总结

快速通过线程 dump 分析知道程序目前处于什么状态,就知道怎么下一步如何下手分析。

本文转载自: 掘金

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

开源一款个人博客OneBlog 前言 线上地址 Github

发表于 2021-10-31

前言

前三个版本的个人博客因为代码写的不怎么样,就都没有开源,这星期又重写了一下,取名叫OneBlog,One意为一个,因为我的这个博客,全程操作都是在一个页面中,没有跳转,没有a标签,都是通过动画来切换的。

如下面效果

录屏_选择区域_20211031192956.gif

新增加了日记功能,因为前几天看过一本书,叫程序员你伤不起,这书其实就是一本日记,讲述了一个.Net程序员的历程,所以我也想通过这样的方式,记录下生活中的点点滴滴。

值得一提的是,我把他做成了一本真正的”书”,虽然书的封面不是很好看。

录屏_选择区域_20211031193419.gif

线上地址

houxinlin.com:6060

至于为什么要有端口6060,因为80运行这一个别的服务,短时间还不能下线。

Github地址

最后附上Github地址

github.com/houxinlin/O…

希望给个star

本文转载自: 掘金

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

linux下如何安装mysql57(超详细)

发表于 2021-10-31

一、安装mysql5.7

1.去到mysql官网

image-20211031175519907

2.找到所需的版本

image-20211031175649921

从网上获取数据源:

1
sql复制代码 wget https://dev.mysql.com/get/mysql57-community-release-el7-9.noarch.rpm

image-20211031175753739

安装数据源

1
arduino复制代码 yum -y install  mysql57-community-release-el7-9.noarch.rpm

查看数据源是否成功安装

1
bash复制代码 ls /etc/yum.repos.d/

image-20211031175127150

这里要注意,在安装完成之后要先查看是否有之前的mysql

1
perl复制代码 rpm -qa |grep mysql

如果有之前安装过的版本需要全部删除

1
复制代码 rpm -e xxxxxx

开始安装mysql

1
vbscript复制代码 yum -y install mysql-community-server

安装完成后,查看mysql是否安装成功

1
复制代码 mysql -V

然后启动mysql服务

1
sql复制代码 systemctl start mysqld

设置mysql开机自启

1
2
bash复制代码systemctl enable mysqld
systemctl daemon-reload

设置mysql的默认编码为utf-8

1
bash复制代码vi /etc/my.cnf

在[mysqld]下添加如下编码配置,如下所示:

1
2
ini复制代码character_set_server=utf8
init_connect='SET NAMES utf8'

image-20211031192301433

查看mysql服务的运行状态

1
lua复制代码 systemctl status mysqld

image-20211031180549479

刚安装后,不能够直接登陆

1
c复制代码 grep 'temporary password' /var/log/mysqld.log

查看临时密码

image-20211031184344516

使用临时密码登陆

二、登陆

使用临时密码进行登陆后,修改密码!!!!

运行下面指令修改密码为liu123456! (备注 mysql5.7默认密码策略要求密码必须是大小写字母数字特殊字母的组合,至少8位)

1
sql复制代码 ALTER USER 'root'@'localhost' IDENTIFIED BY 'liu123456!';

如果密码过于简单,可能会出现修改失败

image-20211031185943166

这是因为mysql5.7有对应的密码策略

输入语句 “ SHOW VARIABLES LIKE ‘validate_password%’; ” 进行查看,

1
sql复制代码 SHOW VARIABLES LIKE 'validate_password%'; #查看密码策略

image-20211031190126470

  • 关于 mysql 密码策略相关参数; 1)validate_password_length 固定密码的总长度; 2)validate_password_mixed_case_count 整个密码中至少要包含大/小写字母的总个数; 3)validate_password_number_count 整个密码中至少要包含阿拉伯数字的个数; 4)validate_password_policy 指定密码的强度验证等级,默认为 MEDIUM; 关于 validate_password_policy 的取值: 0/LOW:只验证长度; 1/MEDIUM:验证长度、数字、大小写、特殊字符; 2/STRONG:验证长度、数字、大小写、特殊字符、字典文件; 5)validate_password_special_char_count 整个密码中至少要包含特殊字符的个数;
  • 这样就可以设置简单密码
1
2
3
4
5
ini复制代码mysql> set global validate_password_policy=0;
Query OK, 0 rows affected (0.00 sec)

mysql> set global validate_password_length=4;
Query OK, 0 rows affected (0.00 sec)

修改密码的四种方式

a)shell命令行下mysqladmin -uroot -poldpassword password newpassword b)sql命令行下 set password = password(newpassword); c)sql命令行下 update user set authentication_string=password(newpassword) where user=’root’; d)sql命令行下 alter user root@’localhost’ identified by ‘newpassword’;//使用随机密码登录,如果要使用数据,查看数据库等操作时mysql会推荐这种写法改变密码。

三、远程连接数据库

MySQL默认是没有开启远程控制的,必须添加远程访问的用户,即默认是只能自己访问,别的机器是访问不了的。

1
sql复制代码GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'liu123' WITH GRANT OPTION;

最后刷新一下权限:

1
arduino复制代码flush privileges;

并且开放防火墙端口(也可以直接关闭防火墙)

如果是云服务器的话,要将安全组的端口也要开放,才可以进行远程访问

1
2
csharp复制代码firewall-cmd --zone=public --add-port=3306/tcp --permanent
systemctl stop firewalld #关闭防火墙

image-20211031192512499

远程连接成功

四、修改mysql版本

1
bash复制代码vim/etc/yum.repos.d/mysql-community.repo

image-20211031193549326

\

本文转载自: 掘金

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

Jackson 之 LocalDateTime 序列化与反序

发表于 2021-10-31

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言

在 Java 8 中对 LocalDateTime、LocalDate 的序列化和反序列化有很多种操作

全局

在 ObjectMapper 对象中配置 JavaTimeModule,此为全局配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码    @Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();

// other serializer and deSerializer config ...

JavaTimeModule javaTimeModule = new JavaTimeModule();

javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));

javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));

objectMapper.registerModule(javaTimeModule);
return objectMapper;
}

DateTimeFormatter.ofPattern 可以设置不同的时间日期模板,来实现不同的效果

局部

使用 @JsonFormat 注解

pattern 可以配置不同的时间格式模板

1
2
3
4
5
kotlin复制代码@Data
public static class Article {
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDateTime date;
}

Serializer 和 DeSerializer

Jackson 提供了默认的 LocalDate 和 LocalDateTime 的 Serializer 和 DeSerializer,不过需要引入额外的 maven 依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>2.9.5</version>
</dependency>
1
2
3
4
5
6
less复制代码@Data
public static class Article {
@JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateDeserializer.class)
private LocalDateTime date;
}

与此同时,还可以自定义 Serializer 和 DeSerializer,以满足某些独特场景中的时间日期格式。
比如对任意格式的时间同一反序列化为标准的 LocalDateTime 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {

@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return this.deserialize(p.getText().trim());
}

private LocalDateTime deserialize(String source) {
if (StringUtils.isBlank(source)) {
return null;
} else if (source.matches("^\\d{4}-\\d{1,2}$")) {
// yyyy-MM
return LocalDateTime.parse(source + "-01T00:00:00.000", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
} else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}T{1}\\d{1,2}")) {
// yyyy-MM-ddTHH
return LocalDateTime.parse(source + ":00:00.000", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
} else {
// yyyy-MM-ddTHH:mm:ss.SSS
return LocalDateTime.parse(source, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
}

}

本文转载自: 掘金

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

【从零开始的账本项目08】 Seata 实现分布式事务

发表于 2021-10-31

在之前的账单模块中, 有增删改查的方法. 但是在增加账单的时候, 只是简单的将账单导入数据库. 而假定该账单有绑定的账户, 则对应的账户余额也应该发生变化, 因此需要同时处理两张数据库表, 但是假设只是简单的将两个逻辑进行实现, 若是对账单的插入操作完毕后出现了异常还未执行账户的操作, 则就会出现已经写入了账单但是却没有修改账户余额的问题. 因此需要数据库的事务操作, 但是由于分布式的缘故, 因此需要分布式事务, 也就有了本片中需要使用的Seata

Seata官网

本次的账本中使用的是1.4.2的版本, 也就是截至目前的最新版.

首先在官网进行对应版本的下载.

之后则需要进行对应的配置.
本次选用的是nacos注册中心 + nacos配置中心的组合.
有关nacos的使用, 可以参照: nacos官网
基本上是打开即用的级别. 对我这样的新手十分友好.

进入正题: 首先需要引入seata的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>de.javakaffee</groupId>
<artifactId>kryo-serializers</artifactId>
<version>0.36</version>
</dependency>

这里先在starter中排除了seata-spring-boot-starter

这是因为需要避免版本的不统一, 先移除后再手动选择适合的版本进行引入(与服务器中开启的seata版本保持一致).

导入了依赖之后. 进入对应的seata的目录中, 编辑/conf/registry.conf文件进行相应的配置处理.

首先是注册中心registry板块.

1
2
3
4
5
6
7
8
9
10
11
conf复制代码type = "nacos"

nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}

本地开启的话, 最简单的就这么配即可.

其次, 是配置中心config板块:

同理可得:

1
2
3
4
5
6
7
8
9
10
ini复制代码  type = "nacos"

nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "service.vgroupMapping.bngel_tx_group"
}

其中需要注意的是, bngel_tx_group中的bngel换成你自定义的组名即可. 并没有特殊要求.

在文件中配完之后回到/bin目录下打开seata-server.bat(windows)即可运行.

记得要先打开nacos

这里有一个坑点, 如果显示内存不足的话可以编辑seata-server.bat文件, 修改其中的配置信息含有%JAVACMD% %JAVA_OPTS%的一行将2048改为1024或者更小即可.

运行成功后就是进行对应代码的配置.

在bootstrap.yml或者application.yml中添加如下配置:

1
2
3
4
5
6
7
8
9
yaml复制代码seata:
tx-service-group: bngel_tx_group
enable-auto-data-source-proxy: false
service:
vgroup-mapping:
bngel_tx_group: default
client:
undo:
log-serialization: kryo

并且新增配置类:

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
java复制代码@Configuration
public class DataSourceProxyConfig {

@Value("${mybatis.mapper-locations}")
private String mapperLocations;

@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDatasource() {
return new DruidDataSource();
}

@Bean
public DataSourceProxy dataSourceProxy(DataSource datasource) {
return new DataSourceProxy(datasource);
}

@Bean
@ConfigurationProperties(prefix = "mybatis.configuration")
public org.apache.ibatis.session.Configuration globalConfiguration() {
return new org.apache.ibatis.session.Configuration();
}

@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy, org.apache.ibatis.session.Configuration configuration) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
sqlSessionFactoryBean.setConfiguration(configuration);
return sqlSessionFactoryBean.getObject();
}
}

在主启动类上加上注解

1
java复制代码@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

就可以关闭自动的数据源配置转而使用我们自定义的seata配置.

其中由于自动的配置失效的缘故, 对应的驼峰转换也会自动关闭.

在配置类中加上了对应prefix = "mybatis.configuration"的读取. 手动打开驼峰命名, 否则后续会出现一定的问题.

在seata配置完成并启动之后.接着便是业务逻辑相关的数据库分布式事务的编写.

目前的需求是(当accountId不为空的情况下):

  1. 在新增账单时, 假如是收入的账单, 对应账户的余额增多, 若是支出则减少.

  2. 在删除账单时, 账户进行相应余额的修改.

首先是使用openfeign操作account模块.

通过对应的service接口创建AccountService (同consumer模块)

之后在Impl类中进行使用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Autowired
private BillDao billDao;

@Autowired
private AccountService accountService;

@Override
@GlobalTransactional(name = "bngelbook-bill-save", rollbackFor = Exception.class)
public Integer saveBill(Bill bill) {
Integer result = billDao.saveBill(bill);
if (bill.getAccountId() != null) {
CommonResult<Account> commonResult = accountService.getAccountById(bill.getAccountId());
if (commonResult.getCode().equals(CommonResult.SUCCESS_CODE)) {
Account account = commonResult.getData();
Account newAccount = new Account();
newAccount.setId(account.getId());
newAccount.setBalance(account.getBalance() + ((bill.getIo() == 1) ? bill.getBalance() : -bill.getBalance()));
accountService.updateAccountById(newAccount);
}
}
return result;
}

此处以保存账本为例.

判断对应的account存在后, 获取该账户, 并且进行余额的修正.

在对应的方法上方使用@GlobalTransactional注解, 开启分布式事务.

  • name: 保证唯一性即可, 自定义.
  • rollbackFor: 当发生某一异常时进行数据库回滚.Exception.class则表示只要有异常均回滚.

方法完成后, 运行Spring Boot. 与之前的方法并无二样. 可以直接使用. 就实现了对应的分布式事务处理.


欢迎各位来对我的小项目提出各种宝贵的意见, 这对我的进步非常重要, 谢谢大家.

GitHub地址: Bngel/bngelbook

本文转载自: 掘金

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

1…448449450…956

开发者博客

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