简栈文化

Java技术人的成长之路~

环境与背景

Ext1:本文源码解析基于 mybatis-spring-boot-starter 2.1.1,即 mybatis 3.5.3 版本。

Ext2:本文主要是对源码的讲解,着重点会是在源码上。

Ext3:阅读本文前,最好对 mapperProxy、 sqlSession 有一定的了解

一、 XMLMapperBuilder、mapperProxy 与 mapperMethod

上篇文章 讲了 mapper 文件是怎么解析的,在文章开头提到了 SqlSessionFactory 这个重要的对象,是的就是我们经常需要配置的:

1
2
3
4
5
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// 略
}

这里面做了很多自动化的配置,当然我们可以通过重写它来自定义我们自己的 sqlSessionFactory,借用一下上篇文章的图片: img

spring 借助 SqlSessionFactoryBean 来创建 sqlSessionFactory,这可以视作是一个典型的建造者模式,来创建 SqlSessionFactory

上篇文章说到,spring 拿到我们配置的 mapper 路径去扫描我们 mapper.xml 然后进行一个循环进行解析(上篇文章第二章节:二、SqlSessionFactory 的初始化与 XMLMapperBuilder):

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
-- 代码位于 org.mybatis.spring.SqlSessionFactoryBean#buildSqlSessionFactory --

if (this.mapperLocations != null) {
if (this.mapperLocations.length == 0) {
LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
} else {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
}
}
} else {
LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
}

-- 代码位于 org.apache.ibatis.builder.xml.XMLMapperBuilder#parse --

public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper")); // 上篇文章主要说的
configuration.addLoadedResource(resource);
bindMapperForNamespace();// 创建mapperProxy的工厂对象
}

parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}

1.1 从 xml 到 mapperStatement

上篇文章实际上就是在讲解 configurationElement(parser.evalNode("/mapper")); 里面发生的故事,实际上还有后续的步骤,如果对 mybatis 有所了解的,应该知道,mybatis 会为我们的接口创建一个叫做 mapperProxy 的代理对象(划重点),其实就是在这后续的步骤 bindMapperForNamespace(); 做的(不尽然,实际上是创建并绑定了 mapperProxyFactory)。

img

不贴太多代码,bindMapperForNamespace() 方法里核心做的主要就是调用 configuration.addMapper() 方法

1
2
3
4
5
6
7
8
9
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}

这个 boundType 就是我们在 mapper 文件里面指定的 namespace,比如:

1
2
3
<mapper namespace="com.anur.mybatisdemo.test.TrackerConfigMapper">
XXXXXXXXXXXXXXXXXX 里面写的sql语句,resultMap 等等,略
</mapper>

configuration.addMapper() 中调用了 mapperRegistry.addMapper(),看到 knowMappers ,这个就是存储我们生产 MapperProxy 的工厂映射 map,我们稍微再讲,先继续往下看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}

1.2 从注解到 mapperStatement

看到 MapperAnnotationBuilder#parse()parse() 中主要是对这个接口里面定义的方法做了 parseStatement 这件事

1
2
3
4
5
6
7
8
9
10
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}

parseStatement() 就是解析注解语句的地方, 如果说我们没有写 xml,将语句以注解的形式写在方法上,则会在这里进行语句解析。它和我们上篇文章讲到的解析xml很像,就是拿到一大堆属性,比如 resultMapkeyGenerator 等等,生成一个 MappedStatement 对象,这里就不赘述了。

1
2
3
4
5
6
7
8
void parseStatement(Method method) {
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
// 解析注解式的 sql 语句,略
}
}

1.3 如果写了 xml,也写了注解会怎么样(调皮)

我们知道承载 mapperStatement 的是一个 map 映射,通过我们在上篇文章中反复强调的 id 来作为 key,那么重复添加会出现什么呢?

答案在这里,mybatis 的这个 map 被重写了,同时写这两者的话,会抛出 ...already contains value for... 的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 代码位置 org.apache.ibatis.session.Configuration.StrictMap#put --
@Override
@SuppressWarnings("unchecked")
public V put(String key, V value) {
if (containsKey(key)) {
throw new IllegalArgumentException(name + " already contains value for " + key
+ (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
}
if (key.contains(".")) {
final String shortKey = getShortName(key);
if (super.get(shortKey) == null) {
super.put(shortKey, value);
} else {
super.put(shortKey, (V) new Ambiguity(shortKey));
}
}
return super.put(key, value);
}

1.4 回到 MapperProxy

1.4.1 MapperProxy 的创建

刚才在1.1中我们提到了,mapperProxy,也就是刚才 org.apache.ibatis.binding.MapperRegistry#addMapper 的代码:knownMappers.put(type, new MapperProxyFactory<>(type));

看到 MapperProxyFactory 的内部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 有删减 --
public class MapperProxyFactory<T> {

private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}

@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}

了解JDK动态代理的小伙伴应该很清楚了, newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 意为,为接口创建一个实现了 InvocationHandler 的代理对象。我们在调用接口方法的时候,实际上要看代理类是如何实现的。

那么看看 mapperProxy 的内部的 invoke 是如何实现的,这里有三类方法,

  • 一种是一些 Object 对象带来的方法,这里不进行代理,直接 invoke
  • 一种是default方法,一种比较蛋疼的写法,把接口当抽象类写,里面可以放一个default方法写实现,这种代理了也没太大意义
  • 最后一种也就是我们准备代理的方法, 它会为每个非上面两者的方法,懒加载一个 MapperMethod 对象,并调用 MapperMethod#execute 来执行真正的 mybatis 逻辑。

1.4.2 MapperMethod 的创建

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
-- 有删减 --
public class MapperProxy<T> implements InvocationHandler, Serializable {

public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {// 来自 Object 的方法,比如 toString()
return method.invoke(this, args);
} else if (method.isDefault()) {// 静态方法,我们可以直接忽略
if (privateLookupInMethod == null) {
return invokeDefaultMethodJava8(proxy, method, args);
} else {
return invokeDefaultMethodJava9(proxy, method, args);
}
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}

private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method,
k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
}

MapperMethod 的逻辑是怎么样的,也很好猜到,它的构造函数中创建了两个对象,

1
2
3
4
5
6
7
8
9
public class MapperMethod {

private final SqlCommand command;
private final MethodSignature method;

public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
  • sqlCommand

sqlCommand 实际上就是从 configuration 里面把它对应的 MappedStatement 取出来,持有它的唯一 id 和执行类型。

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
public static class SqlCommand {

private final String name;
private final SqlCommandType type;

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
  • MethodSignature MethodSignature 是针对接口返回值、参数等值的解析,比如我们的 @Param 注解,就是在 new ParamNameResolver(configuration, method); 里面解析的,比较简单,在之前的文章 简单概括的mybatis sqlSession 源码解析 里也提到过,这里就不多说了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
this.returnsCursor = Cursor.class.equals(this.returnType);
this.returnsOptional = Optional.class.equals(this.returnType);
this.mapKey = getMapKey(method);
this.returnsMap = this.mapKey != null;
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
this.paramNameResolver = new ParamNameResolver(configuration, method);
}

1.4.3 MapperMethod 的执行

mapperMethod 就是 sqlSessionmappedStatement 的一个整合。它的执行是一个策略模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
// 略..
}

具体是怎么执行的在文章 简单概括的mybatis sqlSession 源码解析 提到过,这里也不过多赘述。

这里对 MapperProxy 在初始化与调用过程中的关系做一下罗列:

img

二、 下文序言

上面的 MapperProxy 讲解的比较粗略,因为真的很简单(复杂一点的在 MepperMethod 的策略模式,也就是调用 sqlSession 去执行语句的时候,但是那个本文不会详细说明,后续的文章会解析这部分代码)

本文要讲的是几个在很多文章或者书里都没有提到,或者只是简单提了一下的点:本文将会把 sqlSession、MapperProxy、Spring事务管理几个关联密切的功能点进行总结,比如如下这样的疑问:

  • 1、我们知道一个 sqlSession 对应一个数据库连接,在创建 MapperProxy 的时候,又注入了 sqlSession ,难道我们用的一直是同一个 sqlSession?或者难道每次使用不同的数据库连接,会创建不同的 MapperProxy 代理?
  • 2、事务传播等级是怎么实现的,和 sqlSession 有关系吗?
  • 3、代理对象 MapperProxy 是如何和 spring 产生关联的?

三、 SqlSession 的初始化及其运作总览

为了避免有小伙伴对 sqlSession 完全没有概念,这里将接口代码贴出,可以看出 sqlSession 是执行语句的一个入口,同时也提供了事务的一些操作,实际上就是如此:

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
public interface SqlSession extends Closeable {
<T> T selectOne(String statement);
<T> T selectOne(String statement, Object parameter);
<E> List<E> selectList(String statement);
<E> List<E> selectList(String statement, Object parameter);
<E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);
<K, V> Map<K, V> selectMap(String statement, String mapKey);
<K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey);
<K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);
<T> Cursor<T> selectCursor(String statement);
<T> Cursor<T> selectCursor(String statement, Object parameter);
<T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds);
void select(String statement, Object parameter, ResultHandler handler);
void select(String statement, ResultHandler handler);
void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);
int insert(String statement);
int insert(String statement, Object parameter);
int update(String statement);
int update(String statement, Object parameter);
int delete(String statement);
int delete(String statement, Object parameter);
void commit();
void commit(boolean force);
void rollback();
void rollback(boolean force);
List<BatchResult> flushStatements();
void close();
void clearCache();
Configuration getConfiguration();
<T> T getMapper(Class<T> type);
Connection getConnection();
}

3.1 sqlSession 的创建

3.1.1 Environment 与 Transaction

首先忘掉 spring 为我们提供的便利,看一下基础的,脱离了 spring 托管的 mybatis 是怎么进行 sql 操作的:

1
2
3
SqlSession sqlSession = sqlSessionFactory.openSession();
TrackerConfigMapper mapper = sqlSession.getMapper(TrackerConfigMapper.class);
TrackerConfigDO one = mapper.getOne(1);

SqlSessionFactory 有两个子类实现:DefaultSqlSessionFactorySqlSessionManagerSqlSessionManager 使用动态代理 + 静态代理对 DefaultSqlSessionFactory 进行了代理,不过不用太在意这个 SqlSessionManager,后面会说明原因。

上面不管怎么代理,实际逻辑的执行者都是 DefaultSqlSessionFactory,我们看看它的创建方法,也就是 openSession() 实际执行的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

environment 可用于数据源切换,那么提到数据源切换,就很容易想到了,连接的相关信息是这货维持的。 所以看到我们的代码: tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);TransactionFactory 有三个实现,它们分别是 JdbcTransactionFactoryManagedTransactionFactorySpringManagedTransactionFactory

JdbcTransactionFactoryManagedTransactionFactory 最大的区别就在于 ManagedTransactionFactory 实现了空的 commit 与 rollback,源码中这样说道:付与容器来管理 transaction 的生命周期,这个博主不是特别熟悉,因为没这么用过,tomcat、jetty 等容器实现了对 jdbc 的代理。要注意,不管如何都是使用的 jdbc 这套接口规范进行数据库操作的。

1
2
3
4
5
6
7
8
9
10
/**
* {@link Transaction} that lets the container manage the full lifecycle of the transaction.
* Delays connection retrieval until getConnection() is called.
* Ignores all commit or rollback requests.
* By default, it closes the connection but can be configured not to do it.
*
* @author Clinton Begin
*
* @see ManagedTransactionFactory
*/

Transaction 是 mybatis 创建的一个对象,它实际上是对 jdbc connection 对象的一个封装:

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
-- 代码位于 org.apache.ibatis.transaction.jdbc.JdbcTransaction --

@Override
public Connection getConnection() throws SQLException {
if (connection == null) {
openConnection();
}
return connection;
}

@Override
public void commit() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Committing JDBC Connection [" + connection + "]");
}
connection.commit();
}
}

@Override
public void rollback() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Rolling back JDBC Connection [" + connection + "]");
}
connection.rollback();
}
}

3.1.2 Executor 与 SqlSession

我们知道 sqlSession 的 四大对象之一,Executor,负责统领全局,从语句获取(从 mappedStatement),到参数拼装(parameterHandler),再到执行语句(statementHandler),最后结果集封装(resultHandler),都是它负责“指挥”的。

我们看到它使用 Transaction 进行初始化,另外的一个参数是它的类型,这里不多说,REUSE 是带语句缓存的,和普通的 SimpleExecutor 没有特别大的区别,BATCH 类型则是通过 jdbc 提供的批量提交来对网络请求进行优化。

1
public enum ExecutorType {  SIMPLE, REUSE, BATCH}

最后将持有 Transaction 的 Executor 置入 SqlSession ,完成一个 SqlSession 对象的创建。

可以看到,我们的确是一个SqlSession 对应一个连接(Transaction),MapperProxy 这个业务接口的动态代理对象又持有一个 SqlSession 对象,那么总不可能一直用同一个连接吧?

当然有疑问是好的,而且通过对 SqlSession 初始化过程的剖析,我们已经完善了我们对 mybatis 的认知:

img

接下来就是来打消这个疑问,MapperProxy 持有的 sqlSessionSqlSessionFactory 创建的这个有什么关系?

3.2 SqlSessionTemplate 对 sqlSession 的代理

实际上答案就在 SqlSessionTemplateSqlSessionTemplate 是 spring 对 mybatis SqlSessionFactory 的封装,同时,它还是 SqlSession 的代理。

SqlSessionTemplate 和 mybatis 提供的 SqlSessionManager( SqlSessionFactory 的另一个实现类,也是DefaultSqlSessionFactory 的代理类,可以细想一下,业务是否共用同一个 sqlSession 还要在业务里面去传递,去控制是不是很麻烦) 是一样的思路,不过 spring 直接代理了 sqlSession

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
-- 代码位于 org.mybatis.spring.SqlSessionTemplate --

private final SqlSessionFactory sqlSessionFactory;

private final ExecutorType executorType;

private final SqlSession sqlSessionProxy;

private final PersistenceExceptionTranslator exceptionTranslator;

/**
* Constructs a Spring managed {@code SqlSession} with the given
* {@code SqlSessionFactory} and {@code ExecutorType}.
* A custom {@code SQLExceptionTranslator} can be provided as an
* argument so any {@code PersistenceException} thrown by MyBatis
* can be custom translated to a {@code RuntimeException}
* The {@code SQLExceptionTranslator} can also be null and thus no
* exception translation will be done and MyBatis exceptions will be
* thrown
*
* @param sqlSessionFactory a factory of SqlSession
* @param executorType an executor type on session
* @param exceptionTranslator a translator of exception
*/
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {

notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");

this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}

还是熟悉的配方,就是 jdk 的动态代理,SqlSessionTemplate 在初始化时创建了一个 SqlSession 代理,也内置了 ExecutorTypeSqlSessionFactory defaultSqlSession 初始化的必要组件。

想必看到这里,已经有很多小伙伴知道这里是怎么回事了,是的,我们对 SqlSession 的操作都是经由这个代理来完成,代理的内部,实现了真正 SqlSession 的创建与销毁,回滚与提交等,我们先纵览以下它的代理实现。

3.2.1 sqlSession 常规代理流程赏析

对于这种jdk动态代理,我们看到 SqlSessionInterceptor#invoke 方法就明了了。我们先过一遍常规的流程,也就是没有使用 spring 事务功能支持,执行完 sql 就直接提交事务的常规操作:

  • 1、getSqlSession() 创建 sqlSession
  • 2、执行 MapperProxy,也就是前面讲了一大堆的,MapperProxy 中,通过 MapperMethod 来调用 sqlSession 和我们生成好的 mappedStatement 操作 sql 语句。
  • 3、提交事务
  • 4、关闭事务

注:代码有很大删减

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator); // 创建或者获取真正需要的 SqlSession
try {
Object result = method.invoke(sqlSession, args); // 执行原本想对 SqlSession 做的事情
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);// 如非 spring 管理事务,则直接提交
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}

注意:注释掉的代码在此类型的操作中没有什么意义,getSqlSession() 在这里只是简单通过 sessionFactory 创建了一个 sqlSession

1
2
3
4
5
6
7
8
9
10
11
12
13
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
// SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

// SqlSession session = sessionHolder(executorType, holder);
// if (session != null) {
// return session;
// }

LOGGER.debug(() -> "Creating a new SqlSession");
session = sessionFactory.openSession(executorType);
// registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}

3.2.2 sqlSession 借助 TransactionSynchronizationManager 代理流程赏析

看完前面的实现,有小伙伴会好奇,我的 @Transactional 注解呢?我的事务传播等级呢?

实际上,除去上述常规流程,更多的是要借助 TransactionSynchronizationManager 这个对象来完成,比如刚才步骤一,getSqlSession() 我暂时注释掉的代码里面,有一个很重要的操作:

我们把刚才 getSqlSession() 中注释掉的代码再拿回来看看:

1
2
3
4
5
6
7
8
9
10
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}

session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;

我们可以看到首先获取一个叫做 SqlSessionHolder 的东西,如果里面没有 sqlSession 则调用 sessionFactory.openSession(executorType); 创建一个,并把它注册到 TransactionSynchronizationManager

sqlSessionHolder 没什么可说的,它就只是个纯粹的容器,里面主要就是装着一个 SqlSession

1
2
3
4
5
6
7
8
9
10
11
public SqlSessionHolder(SqlSession sqlSession,
ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {

notNull(sqlSession, "SqlSession must not be null");
notNull(executorType, "ExecutorType must not be null");

this.sqlSession = sqlSession;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
}

所以说我们只需要把目光焦距在 TransactionSynchronizationManager 就可以了,它的内部持有了很多个元素为 MapThreadLocal(代码示例中只贴出了 resources 这一个 ThreadLocal ):

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
public abstract class TransactionSynchronizationManager {

private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");

@Nullable
public static Object getResource(Object key) {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Object value = doGetResource(actualKey);
if (value != null && logger.isTraceEnabled()) {
logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +
Thread.currentThread().getName() + "]");
}
return value;
}

@Nullable
private static Object doGetResource(Object actualKey) {
Map<Object, Object> map = resources.get();
if (map == null) {
return null;
}
Object value = map.get(actualKey);
// Transparently remove ResourceHolder that was marked as void...
if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
map.remove(actualKey);
// Remove entire ThreadLocal if empty...
if (map.isEmpty()) {
resources.remove();
}
value = null;
}
return value;
}

也就是说,spring 的事务,是借助 TransactionSynchronizationManager + SqlSessionHoldersqlSession 的控制来实现的。

那么这样就很清晰了,如下总结,也如下图:

  • MapperProxy 内置的 sqlSessionsqlSessiontemplate
  • sqlSessiontemplate 通过持有 SqlSessionFactory 来创建真正的 SqlSession
  • TransactionSynchronizationManager + SqlSessionHolder 则扮演着 SqlSession 管理的角色

img

四、spring 如何管理 sqlSession

上一个小节只是讲了是什么,没有讲为什么,到了这里如果有好奇宝宝一定会好奇诸如 spring 的一系列事务控制是怎么实现的,当然本文不会讲太多 spring 事务管理相关的太多东西,以后会有后续文章专门剖析事务管理。

我们可以简单看下 TransactionInterceptor ,这是 @Transactional 注解的代理类。

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
/**
* AOP Alliance MethodInterceptor for declarative transaction
* management using the common Spring transaction infrastructure
* ({@link org.springframework.transaction.PlatformTransactionManager}/
* {@link org.springframework.transaction.ReactiveTransactionManager}).
*
* <p>Derives from the {@link TransactionAspectSupport} class which
* contains the integration with Spring's underlying transaction API.
* TransactionInterceptor simply calls the relevant superclass methods
* such as {@link #invokeWithinTransaction} in the correct order.
*
* <p>TransactionInterceptors are thread-safe.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @see TransactionProxyFactoryBean
* @see org.springframework.aop.framework.ProxyFactoryBean
* @see org.springframework.aop.framework.ProxyFactory
*/
@SuppressWarnings("serial")
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {

/**
* Create a new TransactionInterceptor.
* <p>Transaction manager and transaction attributes still need to be set.
* @see #setTransactionManager
* @see #setTransactionAttributes(java.util.Properties)
* @see #setTransactionAttributeSource(TransactionAttributeSource)
*/
public TransactionInterceptor() {
}

@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

// Adapt to TransactionAspectSupport's invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

可以看到它的代理方法 invoke() 的执行逻辑在 invokeWithinTransaction() 里:

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
--代码位于 org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction --
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {

// If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final TransactionManager tm = determineTransactionManager(txAttr);

if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
// 响应式事务相关
}

PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}

if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}

commitTransactionAfterReturning(txInfo);
return retVal;
}

else {
// CallbackPreferringPlatformTransactionManager 的处理逻辑
}
}

invokeWithinTransaction() 的代码虽然长,我们还是把它分段来看:

4.1 TransactionDefinition 与 TransactionManager 的创建

  • 第一部分,准备阶段

也就是这部分代码:

1
2
3
4
5
6
// If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final TransactionManager tm = determineTransactionManager(txAttr);
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

获取 TransactionAttribute(TransactionDefinition(底层接口),这里面装载了事务传播等级,隔离级别等属性。 TransactionAttribute 的创建依据配置,或者我们的事务传播等级注解,对什么异常进行回滚等,后续会继续对它的应用做说明, PlatformTransactionManager 则是进行事务管理的主要操作者。

4.2 获取 TransactionInfo

  • 第二部分,事务开启或者获取与准备,也就是我们执行逻辑的第一行代码 createTransactionIfNecessary()(是不是和前面说到的 SqlSession的创建或者获取很像?)

我们可以看到 createTransactionIfNecessary() 的实现就做了两件事,其一是获取一个叫做 TransactionStatus 的东西,另外则是调用 prepareTransactionInfo(),获取一个 TransactionInfo

1
2
3
4
5
6
7
8
9
10
11
	// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

--代码位于 org.springframework.transaction.interceptor.TransactionAspectSupport#createTransactionIfNecessary --

protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {

TransactionStatus status = tm.getTransaction(txAttr);
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

先看看第一件事,也就是获取 TransactionStatus,它保存了事务的 savePoint ,是否新事物等。删减掉一些判断方法,代码如下:

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
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException {

// Use defaults if no transaction definition given.
TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());

Object transaction = doGetTransaction();
boolean debugEnabled = logger.isDebugEnabled();

if (isExistingTransaction(transaction)) {
// Existing transaction found -> check propagation behavior to find out how to behave.
return handleExistingTransaction(def, transaction, debugEnabled);
}
if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
try {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
def, transaction, true, newSynchronization, debugEnabled, suspendedResources);
doBegin(transaction, def);
prepareSynchronization(status, def);
return status;
}
catch (RuntimeException | Error ex) {
resume(null, suspendedResources);
throw ex;
}
}
else {
// Create "empty" transaction: no actual transaction, but potentially synchronization.
if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
logger.warn("Custom isolation level specified but no actual transaction initiated; " +
"isolation level will effectively be ignored: " + def);
}
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
}
}

代码很长,但是不急,我们可以简单看出它分为两个部分:

  • 第一部分是获取事务 doGetTransaction()
  • 第二部分则是判断是否新事物,
    • 如果不是新事物,则执行 handleExistingTransaction
    • 如果是新事物
      • TransactionDefinition.PROPAGATION_REQUIREDTransactionDefinition.PROPAGATION_REQUIRES_NEWTransactionDefinition.PROPAGATION_NESTED 是一种逻辑
      • 其余是另一种逻辑,信息量有点大,但是慢慢来:

4.2.1 doGetTransaction

1
2
3
4
5
6
7
8
protected Object doGetTransaction() {
DataSourceTransactionObject txObject = new DataSourceTransactionObject();
txObject.setSavepointAllowed(isNestedTransactionAllowed());
ConnectionHolder conHolder =
(ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
txObject.setConnectionHolder(conHolder, false);
return txObject;
}

doGetTransaction 获取我们的事务对象,这里也使用了 TransactionSynchronizationManager(前面说到的 SqlSession 的管理类),事务对象会尝试获取本事务所使用的连接对象,这个和事务传播等级有关,先立个 flag。

我们可以看到这里面主要逻辑就是去获取 ConnectionHolder,实际上很简单,只要能获取到,就是已经存在的事务,获取不到(或者事务已经关闭)就是新事物。

4.2.2 新事物的处理之创建一个真正的事务对象

如果说前面无法从 TransactionSynchronizationManager 获取到 conHolder,或者说,我们的线程中并没有 ConnectionHolder 那么将会进入此分支,此分支的支持的三个事务传播等级 TransactionDefinition.PROPAGATION_REQUIREDTransactionDefinition.PROPAGATION_REQUIRES_NEWTransactionDefinition.PROPAGATION_NESTED 都是需要创建新事务的,所以它们在同一个分支里面:

1
2
3
4
5
6
7
SuspendedResourcesHolder suspendedResources = suspend(null);
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
def, transaction, true, newSynchronization, debugEnabled, suspendedResources);
doBegin(transaction, def);
prepareSynchronization(status, def);
return status;

SuspendedResourcesHolder 与事务的挂起相关,doBegin() 则是对连接对象 connection 的获取和配置,prepareSynchronization() 则是对新事物的一些初始化操作。我们一点点看:

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
/**
* This implementation sets the isolation level but ignores the timeout.
*/
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;

if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}

txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();

Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
txObject.setReadOnly(definition.isReadOnly());

// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
// so we don't want to do it unnecessarily (for example if we've explicitly
// configured the connection pool to set it already).
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}

prepareTransactionalConnection(con, definition);
txObject.getConnectionHolder().setTransactionActive(true);

// Bind the connection holder to the thread.
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}
}

可以看到,ConnectionHolder 的创建和连接的打开就是在这里进行的,创建后,设置其隔离级别,取消 connection 的自动提交,将提交操作纳入到 spring 管理,并且将其存到 TransactionSynchronizationManager 使得 4.2.1 提到的 doGetTransaction() 可以拿到此 ConnectionHolder


做完连接的获取与配置后,下一步就是对事物的一些初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Initialize transaction synchronization as appropriate.
*/
protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) {
if (status.isNewSynchronization()) {
TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction());
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(
definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ?
definition.getIsolationLevel() : null);
TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());
TransactionSynchronizationManager.initSynchronization();
}
}

这个代码都是代码字面意义的简单设置,就不赘述了。

4.2.3 新事物的处理之创建一个虚假的事务对象

刚才讲的是 “无法从 TransactionSynchronizationManager 获取到 conHolder”,并且属于一些需要创建新事物的传播等级的情况。

如果说方才没有事务,也不需要创建新的事务,则会进入此分支,创建一个空的 TransactionStatus,内部的事务对象为空,代码很简单就不贴了,有兴趣可以去看看 org.springframework.transaction.support.AbstractPlatformTransactionManager#getTransaction 的最后一个分支。

4.2.4 事务嵌套

刚才说的都是无法获取到 conHolder 的情况,如果获取到了,则又是另一套代码了,handleExistingTransaction 很长,它的第一个部分是对传播等级的控制,有兴趣的小伙伴可以去看看源码,我这里只挑一个简单的传播等级 PROPAGATION_NESTED_NEW 做说明(其他的会在专门的事务一期做讲解):

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
-- 代码位于 org.springframework.transaction.support.AbstractPlatformTransactionManager#handleExistingTransaction --
private TransactionStatus handleExistingTransaction(
TransactionDefinition definition, Object transaction, boolean debugEnabled)
throws TransactionException {

if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
if (debugEnabled) {
logger.debug("Suspending current transaction, creating new transaction with name [" +
definition.getName() + "]");
}
SuspendedResourcesHolder suspendedResources = suspend(transaction);
try {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;
}
catch (RuntimeException | Error beginEx) {
resumeAfterBeginException(transaction, suspendedResources, beginEx);
throw beginEx;
}
}

... 略
}

我们可以发现和 4.2.2 新事物的处理 代码是一样的,唯一的区别就是此 TransactionStatus 对象会真正内嵌一个事务挂起对象 SuspendedResourcesHolder

4.3 封装 TransactionInfo

拿到 TransactionStatus 之后, prepareTransactionInfo() 里简单的将刚才那些 PlatformTransactionManager TransactionAttributeTransactionStatus 包装成一个 TransactionInfo 对象,并将其保存在 ThreadLocal 中,这个 bindToThread() 还会将当前已经持有的 TransactionInfo 对象暂存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected TransactionInfo prepareTransactionInfo(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, String joinpointIdentification,
@Nullable TransactionStatus status) {

TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification);
if (txAttr != null) {
// The transaction manager will flag an error if an incompatible tx already exists.
txInfo.newTransactionStatus(status);
}

// We always bind the TransactionInfo to the thread, even if we didn't create
// a new transaction here. This guarantees that the TransactionInfo stack
// will be managed correctly even if no transaction was created by this aspect.
txInfo.bindToThread();
return txInfo;
}

到这里思路就很清晰了,代理为我们做的事情就是生成了一个叫做 TransactionInfo 的东西,里面的 TransactionManager 可以使得 spring 去对最底层的 connection 对象做一些回滚,提交操作。TransactionStatus 则保存挂起的事务的信息,以及当前事务的一些状态,如下图:

img

4.4 纵览流程

让我们回到第四节开头的那段很长的代码,到这里是不是很明了了:

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
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {

// If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final TransactionManager tm = determineTransactionManager(txAttr);
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}

if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}

commitTransactionAfterReturning(txInfo);
return retVal;
}
}
  • 1、获取 TransactionInfo
  • 2、执行切面
  • 3、将之前挂起的 TransactionInfo 找回:
1
2
3
4
5
6
7
8
9
10
11
12
private void bindToThread() {
// Expose current TransactionStatus, preserving any existing TransactionStatus
// for restoration after this transaction is complete.
this.oldTransactionInfo = transactionInfoHolder.get();
transactionInfoHolder.set(this);
}

private void restoreThreadLocalStatus() {
// Use stack to restore old transaction TransactionInfo.
// Will be null if none was set.
transactionInfoHolder.set(this.oldTransactionInfo);
}
  • 4、如果需要,则提交当前事务
  • 5、返回切面值

4.5 最后一块拼图,spring 如何与 sqlSession 产生关联:

我们在第三章讲到,mybatis有一个叫做 defualtSqlSessionFactory 的类,负责创建 sqlSession,但是它和 spring 又是怎么产生关联的呢?

答案就在于,spring 实现了自己的 TransactionFactory,以及自己的 Transaction 对象 SpringManagedTransaction 。回顾一下 SqlSession 的创建过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

看一下 SpringManagedTransaction 是如何管理 connection的:

1
2
3
4
5
6
7
8
private void openConnection() throws SQLException {
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);

LOGGER.debug(() -> "JDBC Connection [" + this.connection + "] will"
+ (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring");
}

DataSourceUtils.getConnection(this.dataSource); 划重点,里面的实现不用我多说了,我们可以看到熟悉的身影,也就是 ConnectionHolder,连接是从这里(优先)拿的:

1
2
3
4
5
6
7
8
9
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}

更新整套体系图:

img

我们整体简单过一次:

  • mybatis 启动时根据xml、注解创建了 mapperedStatement,用于sql执行,创建了 SqlSessionFactory 用于创建 SqlSession 对象。
  • mybatis 启动时创建了 MapperProxyFactory 用于创建接口的代理对象 MapperProxy
  • 在创建 MapperProxy 时,spring 为其注入了一个 sqlSession 用于 sql执行,但是这个 sqlSession 是一个代理对象,叫做 sqlSessionTemplate,它会自动选择我们该使用哪个 sqlSession 去执行
  • 在执行时,spring 切面在执行事务之前,会创建一个叫做 TransactionInfo 的对象,此对象会根据事务传播等级来控制是否创建新连接,是否挂起上一个连接,将信息保存在 TransactionSynchronizationManager
  • 到了真正需要创建或者获取 sqlSession 时,spring 重写的 TransactionFactory 会优先去 TransactionSynchronizationManager 中拿连接对象。

背景

前不久《深入理解Java虚拟机》第三版发布了,赶紧买来看了看新版的内容,这本书更新了很多新版本虚拟机的内容,还对以前的部分内容进行了重构,还是值得去看的。本着复习和巩固的态度,我决定来编译一个简单的类文件来分析Java的字节码内容,来帮助理解和巩固Java字节码知识,希望也对阅读本文的你有所帮助。

说明:本次采用的环境是OpenJdk12

编译“1+1”代码

首先我们需要写个简单的小程序,1+1的程序,学习就要从最简单的1+1开始,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package top.luozhou.test;

/**
* @description:
* @author: luozhou
* @create: 2019-12-25 21:28
**/
public class TestJava {
public static void main(String[] args) {
int a=1+1;
System.out.println(a);
}
}

写好java类文件后,首先执行命令javac TestJava.java 编译类文件,生成TestJava.class。 然后执行反编译命令javap -verbose TestJava,字节码结果显示如下:

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
  Compiled from "TestJava.java"
public class top.luozhou.test.TestJava
minor version: 0
major version: 56
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // top/luozhou/test/TestJava
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 TestJava.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 top/luozhou/test/TestJava
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
{
public top.luozhou.test.TestJava();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_2
1: istore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: iload_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 10: 0
line 11: 2
line 12: 9
}

解析字节码

1.基础信息

上述结果删除了部分不影响解析的冗余信息,接下来我们便来解析字节码的结果。

1
2
3
 minor version: 0 次版本号,为0表示未使用
major version: 56 主版本号,56表示jdk12,表示只能运行在jdk12版本以及之后的虚拟机中
flags: ACC_PUBLIC, ACC_SUPER

ACC_PUBLIC:这就是一个是否是public类型的访问标志。

ACC_SUPER: 这个falg是为了解决通过 invokespecial 指令调用 super 方法的问题。可以将它理解成 Java 1.0.2 的一个缺陷补丁,只有通过这样它才能正确找到 super 类方法。从 Java 1.0.2 开始,编译器始终会在字节码中生成 ACC_SUPER 访问标识。感兴趣的同学可以点击这里来了解更多。

2.常量池

接下来,我们将要分析常量池,你也可以对照上面整体的字节码来理解。

1
#1 = Methodref          #5.#14         // java/lang/Object."<init>":()V

这是一个方法引用,这里的#5表示索引值,然后我们可以发现索引值为5的字节码如下

1
#5 = Class              #20            // java/lang/Object

它表示这是一个Object类,同理#14指向的是一个"":()V表示引用的是初始化方法。

1
#2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;

上面这段表示是一个字段引用,同样引用了#15#16,实际上引用的就是java/lang/System类中的PrintStream对象。其他的常量池分析思路是一样的,鉴于篇幅我就不一一说明了,只列下其中的几个关键类型和信息。

NameAndType:这个表示是名称和类型的常量表,可以指向方法名称或者字段的索引,在上面的字节码中都是表示的实际的方法。

Utf8我们经常使用的是字符编码,但是这个不是只有字符编码的意思,它表示一种字符编码是Utf8的字符串。它是虚拟机中最常用的表结构,你可以理解为它可以描述方法,字段,类等信息。 比如:

1
2
#4 = Class              #19 
#19 = Utf8 top/luozhou/test/TestJava

这里表示#4这个索引下是一个类,然后指向的类是#19,#19是一个Utf8表,最终存放的是top/luozhou/test/TestJava,那么这样一连接起来就可以知道#4位置引用的类是top/luozhou/test/TestJava了。

3.构造方法信息

接下来,我们分析下构造方法的字节码,我们知道,一个类初始化的时候最先执行它的构造方法,如果你没有写构造方法,系统会默认给你添加一个无参的构造方法。

1
2
3
4
5
6
7
8
9
10
public top.luozhou.test.TestJava();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0

descriptor: ()V :表示这是一个没有返回值的方法。

flags: ACC_PUBLIC:是公共方法。

stack=1, locals=1, args_size=1 :表示栈中的数量为1,局部变量表中的变量为1,调用参数也为1。

这里为什么都是1呢?这不是默认的构造方法吗?哪来的参数?其实Java语言有一个潜规则:在任何实例方法里面都可以通过this来访问到此方法所属的对象。而这种机制的实现就是通过Java编译器在编译的时候作为入参传入到方法中了,熟悉python语言的同学肯定会知道,在python中定义一个方法总会传入一个self的参数,这也是传入此实例的引用到方法内部,Java只是把这种机制后推到编译阶段完成而已。所以,这里的1都是指this这个参数而已。

1
2
3
4
5
     0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0

经过上面这个分析对于这个构造方法表达的意思也就很清晰了。

aload_0:表示把局部变量表中的第一个变量加载到栈中,也就是this

invokespecial:直接调用初始化方法。

return:调用完毕方法结束。

LineNumberTable:这是一个行数的表,用来记录字节码的偏移量和代码行数的映射关系。line 8: 0表示,源码中第8行对应的就是偏移量0的字节码,因为是默认的构造方法,所以这里并无法直观体现出来。

另外这里会执行Object的构造方法是因为,Object是所有类的父类,子类的构造要先构造父类的构造方法。

4.main方法信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_2
1: istore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: iload_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 10: 0
line 11: 2
line 12: 9

有了之前构造方法的分析,我们接下来分析main方法也会熟悉很多,重复的我就略过了,这里重点分析code部分。

stack=2, locals=2, args_size=1:这里的栈和局部变量表为2,参数还是为1。这是为什么呢?因为main方法中声明了一个变量a,所以局部变量表要加一个,栈也是,所以他们是2。那为什么args_size还是1呢?你不是说默认会把this传入的吗?应该是2啊。注意:之前说的是在任何实例方法中,而这个main方法是一个静态方法,静态方法直接可以通过类+方法名访问,并不需要实例对象,所以这里就没必要传入了

0: iconst_2:将int类型2推送到栈顶。

1: istore_1:将栈顶int类型数值存入第二个本地变量。

2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;:获取PrintStream类。

5: iload_1: 把第二个int型本地变量推送到栈顶。

6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V:调用println方法。

9: return:调用完毕结束方法。

这里的LineNumberTable是有源码的,我们可以对照下我前面描述是否正确: img

line 10: 0: 第10行表示 0: iconst_2字节码,这里我们发现编译器直接给我们计算好了把2推送到栈顶了。

line 11: 2:第11行源码对应的是 2: getstatic 获取输出的静态类PrintStream

line 12: 9:12行源码对应的是return,表示方法结束。

这里我也画了一个动态图片来演示main方法执行的过程,希望能够帮助你理解: img

总结

这篇文章我从1+1的的源码编译开始,分析了生成后的Java字节码,包括类的基本信息,常量池,方法调用过程等,通过这些分析,我们对Java字节码有了比较基本的了解,也知道了Java编译器会把优化手段通过编译好的字节码体现出来,比如我们的1+1=2,字节码字节赋值一个2给变量,而不是进行加法运算,从而优化了我们的代码,提搞了执行效率。

参考

  1. https://bugs.openjdk.java.net/browse/JDK-6527033

获取镜像

在实际的投产中,公司一般所有企业的私有镜像:我们公司选择的是:https://goharbor.io/。

首先获取一个nginx的镜像,待会儿需要直接启动这个镜像

1
docker pull nginx

确认镜像已经在列表中:

1
2
3
[root@CentOS7-Node1 parallels]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 231d40e811cd 2 weeks ago 126MB

创建Pod

直接在master节点上运行一个镜像,并且启动2台机器。

1
2
3
4
5
6
7
[root@CentOS7-Node1 parallels]# kubectl run my-nginx --image=nginx --replicas=2 --port=80
kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
deployment.apps/my-nginx created
[root@CentOS7-Node1 parallels]# kubectl get pod
NAME READY STATUS RESTARTS AGE
my-nginx-75897978cd-87dnh 1/1 Running 0 4m36s
my-nginx-75897978cd-nwnrm 1/1 Running 0 4m36s

这里需要等待一定的时间,容器的状态由ContainerCreating变为Running

查看日志

1
2
kubectl logs <pod_name>
kubectl logs -f <pod_name> # 类似tail -f的方式查看(tail -f 实时查看日志文件 tail -f 日志文件log)

实际的验证操作:

1
2
3
[root@CentOS7-Node1 parallels]# kubectl logs my-nginx-75897978cd-87dnh
Error from server: Get https://10.211.55.8:10250/containerLogs/default/my-nginx-75897978cd-87dnh/my-nginx: dial tcp 10.211.55.8:10250: connect: no route to host
[root@CentOS7-Node1 parallels]# kubectl logs my-nginx-75897978cd-nwnrm

显示Pod资源的详细信息

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
[root@CentOS7-Node1 parallels]# kubectl describe pod my-nginx-75897978cd-87dnh
Name: my-nginx-75897978cd-87dnh
Namespace: default
Priority: 0
Node: centos7-node2/10.211.55.8
Start Time: Thu, 12 Dec 2019 21:26:18 +0800
Labels: pod-template-hash=75897978cd
run=my-nginx
Annotations: <none>
Status: Running
IP: 10.32.0.2
IPs:
IP: 10.32.0.2
Controlled By: ReplicaSet/my-nginx-75897978cd
Containers:
my-nginx:
Container ID: docker://43bda4badd180e540ae95bb8b4cd1b2e174b702c616f4e705e48ff6c57a30d40
Image: nginx
Image ID: docker-pullable://nginx@sha256:50cf965a6e08ec5784009d0fccb380fc479826b6e0e65684d9879170a9df8566
Port: 80/TCP
Host Port: 0/TCP
State: Running
Started: Thu, 12 Dec 2019 21:29:00 +0800
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-8s7rp (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
default-token-8s7rp:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-8s7rp
Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled <unknown> default-scheduler Successfully assigned default/my-nginx-75897978cd-87dnh to centos7-node2
Normal Pulling 15m kubelet, centos7-node2 Pulling image "nginx"
Normal Pulled 13m kubelet, centos7-node2 Successfully pulled image "nginx"
Normal Created 13m kubelet, centos7-node2 Created container my-nginx
Normal Started 13m kubelet, centos7-node2 Started container my-nginx

查看所有pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@CentOS7-Node1 parallels]# kubectl get pods --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
default my-nginx-75897978cd-87dnh 1/1 Running 0 21m
default my-nginx-75897978cd-nwnrm 1/1 Running 0 21m
kube-system coredns-5644d7b6d9-tfkwk 1/1 Running 0 2d9h
kube-system coredns-5644d7b6d9-zwpg9 1/1 Running 0 2d9h
kube-system etcd-centos7-node1 1/1 Running 0 2d9h
kube-system kube-apiserver-centos7-node1 1/1 Running 0 2d
kube-system kube-controller-manager-centos7-node1 1/1 Running 4 2d9h
kube-system kube-proxy-8tqsb 1/1 Running 0 2d9h
kube-system kube-proxy-rxdfv 1/1 Running 0 2d6h
kube-system kube-proxy-vdkhd 1/1 Running 0 47h
kube-system kube-scheduler-centos7-node1 1/1 Running 4 2d9h
kube-system weave-net-4q5kr 2/2 Running 0 2d8h
kube-system weave-net-767sf 2/2 Running 0 2d6h
kube-system weave-net-f2pgl 2/2 Running 1 47h

进入一个Pod

1
2
[root@CentOS7-Node1 parallels]#  kubectl exec -it my-nginx-75897978cd-87dnh -n default -- /bash/sh
Error from server: error dialing backend: dial tcp 10.211.55.8:10250: connect: no route to host

出现这个错误是因为在机器10.211.55.8没有关闭掉防火墙:

1
2
3
[root@CentOS7-Node2 Workspace]# systemctl disable firewalld && systemctl stop firewalld
Removed symlink /etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service.
Removed symlink /etc/systemd/system/basic.target.wants/firewalld.service.

再次进入Pod就成功了,但是什么命令都没有。

1
2
3
4
5
6
[root@CentOS7-Node1 parallels]#  kubectl exec -it my-nginx-75897978cd-87dnh -n default bash
root@my-nginx-75897978cd-87dnh:/# ifconfig
bash: ifconfig: command not found
root@my-nginx-75897978cd-87dnh:/# ps aux | grep nginx
bash: ps: command not found
root@my-nginx-75897978cd-87dnh:/#

参考地址:

kube-apiserver默认配置

查看一下kube-apiserver的一些启动配置项,确认存放地址。

1
2
3
4
5
6
7
8
[root@CentOS7-Node1 manifests]# ll
total 16
-rw-------. 1 root root 1759 Dec 10 12:15 etcd.yaml
-rw-------. 1 root root 2602 Dec 10 12:15 kube-apiserver.yaml
-rw-------. 1 root root 2531 Dec 10 12:15 kube-controller-manager.yaml
-rw-------. 1 root root 1119 Dec 10 12:15 kube-scheduler.yaml
[root@CentOS7-Node1 manifests]# pwd
/etc/kubernetes/manifests

可以通过查看yaml文件的方式查看

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
[root@CentOS7-Node1 manifests]# cat kube-apiserver.yaml 
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
component: kube-apiserver
tier: control-plane
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --advertise-address=10.211.55.7
- --allow-privileged=true
- --authorization-mode=Node,RBAC
- --client-ca-file=/etc/kubernetes/pki/ca.crt #指定CA根证书文件
- --enable-admission-plugins=NodeRestriction
- --enable-bootstrap-token-auth=true
- --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
- --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
- --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
- --etcd-servers=https://127.0.0.1:2379
- --insecure-port=0
- --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
- --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt
- --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
- --requestheader-allowed-names=front-proxy-client
- --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
- --requestheader-extra-headers-prefix=X-Remote-Extra-
- --requestheader-group-headers=X-Remote-Group
- --requestheader-username-headers=X-Remote-User
- --secure-port=6443
- --service-account-key-file=/etc/kubernetes/pki/sa.pub
- --service-cluster-ip-range=10.96.0.0/12
- --tls-cert-file=/etc/kubernetes/pki/apiserver.crt #指定ApiServer证书文件
- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key #指定ApiServer私钥文件
image: k8s.gcr.io/kube-apiserver:v1.16.0
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 8
httpGet:
host: 10.211.55.7
path: /healthz
port: 6443
scheme: HTTPS
initialDelaySeconds: 15
timeoutSeconds: 15
name: kube-apiserver
resources:
requests:
cpu: 250m
volumeMounts:
- mountPath: /etc/ssl/certs
name: ca-certs
readOnly: true
- mountPath: /etc/pki
name: etc-pki
readOnly: true
- mountPath: /etc/kubernetes/pki
name: k8s-certs
readOnly: true
hostNetwork: true
priorityClassName: system-cluster-critical
volumes:
- hostPath:
path: /etc/ssl/certs
type: DirectoryOrCreate
name: ca-certs
- hostPath:
path: /etc/pki
type: DirectoryOrCreate
name: etc-pki
- hostPath:
path: /etc/kubernetes/pki
type: DirectoryOrCreate
name: k8s-certs
status: {}

我们注意到有如下三个启动参数:

  • --client-ca-file: 指定CA根证书文件为/etc/kubernetes/pki/ca.pem,内置CA公钥用于验证某证书是否是CA签发的证书
  • --tls-private-key-file: 指定ApiServer私钥文件为/etc/kubernetes/pki/apiserver-key.pem
  • --tls-cert-file:指定ApiServer证书文件为/etc/kubernetes/pki/apiserver.pem
直接获取nodes节点

用默认的CA认证尝试一下从其他节点查看pods的信息,直接来获取master节点的pods信息。

1
2
[root@CentOS7-Node2 Workspace]# kubectl --server=https://10.211.55.7:6443 get nodes
Please enter Username: # 这里会提示需要输入用户名,但我们并不知道用户名是什么?
通过CA认证方式

然后我们尝试用默认的CA认证,首先把master节点上的CA文件copy到我们的其他节点去。

1
2
3
scp /etc/kubernetes/pki/ca.crt root@10.211.55.8:/home/parallels/Workspace/
scp /etc/kubernetes/pki/apiserver-kubelet-client.crt root@10.211.55.8:/home/parallels/Workspace/
scp /etc/kubernetes/pki/apiserver-kubelet-client.key root@10.211.55.8:/home/parallels/Workspace/

通过设置certificate-authorityclient-certificateclient-key来访问。

1
2
3
4
5
6
7
8
[root@CentOS7-Node2 Workspace]# kubectl --server=https://10.211.55.7:6443 \
--certificate-authority=/home/parallels/Workspace/ca.crt \
--client-certificate=/home/parallels/Workspace/apiserver-kubelet-client.crt \
--client-key=/home/parallels/Workspace/apiserver-kubelet-client.key \
get nodes
NAME STATUS ROLES AGE VERSION
centos7-node1 Ready master 5h28m v1.16.2
centos7-node2 Ready <none> 149m v1.16.2

操作成功!

参考地址:

定义

**数据结构(Data Structure)**是带有结构特性的数据元素的集合,它研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系,并对这种结构定义相适应的运算,设计出相应的算法,并确保经过这些运算以后所得到的新结构仍保持原来的结构类型。简而言之,数据结构是相互之间存在一种或多种特定关系的数据元素的集合,即带“结构”的数据元素的集合。“结构”就是指数据元素之间存在的关系,分为逻辑结构和存储结构。

分类:

  • 数组(英语:Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储。利用元素的索引(index)可以计算出该元素对应的存储地址。

  • (英语:stack)又称为堆叠,是计算机科学中的一种抽象数据类型,只允许在有序的线性数据集合的一端(称为堆栈顶端,英语:top)进行加入数据(英语:push)和移除数据(英语:pop)的运算。因而按照后进先出(LIFO, Last In First Out)的原理运作。

  • 队列,又称为伫列(queue),是先进先出(FIFO, First-In-First-Out)的线性表。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。

  • 链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。最常见的三种链表结构有:单链表、双向链表、循环链表。

  • (英语:tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:

    • 每个节点都只有有限个子节点或无子节点;
    • 没有父节点的节点称为根节点;
    • 每一个非根节点有且只有一个父节点;
    • 除了根节点外,每个子节点可以分为多个不相交的子树;
    • 树里面没有环路(cycle)
  • 散列表Hash table,也叫哈希表),是根据(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表

  • (英语:Heap)是计算机科学中的一种特别的树状数据结构。若是满足以下特性,即可称为堆:“给定堆中任意节点P和C,若P是C的母节点,那么P的值会小于等于(或大于等于)C的值”。若母节点的值恒小于等于子节点的值,此堆称为最小堆(min heap);反之,若母节点的值恒大于等于子节点的值,此堆称为最大堆(max heap)。在堆中最顶端的那一个节点,称作根节点(root node),根节点本身没有母节点(parent node)。

  • (Graph)是由顶点的有穷非空集合和顶点之间的边的集合组成,通常表示为:G(V,E)。其中,G 表示一个图,V是图G中顶点的集合,E是图G中边的集合。

    • 图中数据元素叫做顶点(Vertext)。

    • 在图中,不允许没有顶点。若 V 是图的顶点的集合,那么,V 是非空
      有穷集合。

    • 图的任意两个顶点之间都可能有关系,它们的关系用边来表示。边集可
      以是空的。

http://static.cyblogs.com/WX20191219-151801@2x.png

再了解树之前,我们对一些专业术语普及一下:度、阶、高度、深度、根、叶子、兄弟节点、关键字。

参考地址:

背景

我是Mac系统与Win10系统同时使用,但大部分还是在Mac系统上,有时候为了保持2个系统的“一致性”,会用到一些云盘或者Git等。这次是属于保证学习spring源代码的时候版本内容一致。

https://github.com/spring-projects/spring-framework/tree/v5.1.4.RELEASE

spring-framework是用gradle来管理包依赖的,在编译过程中,Win系统同遇到了一些问题。

升级gradle

之前我是用的gradle-4.10.2,找了一些文章建议升级,所以我干脆就升级到最新的版本。

官网下载地址:https://gradle.org/releases/,然后我选择的是6.0的版本。

配置GRADLE_HOMEpath

配置GRADLE_HOME:

http://static.cyblogs.com/QQ截图20191117000656.png

配置path:

http://static.cyblogs.com/QQ截图20191117000716.png

修改build.gradle

对比图:

http://static.cyblogs.com/QQ截图20191117001201.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
buildscript {
repositories {
maven { url "https://repo.spring.io/plugins-release" }
maven {url "https://plugins.gradle.org/m2/"}
}
dependencies {
classpath("io.spring.gradle:propdeps-plugin:0.0.9.RELEASE")
classpath("org.asciidoctor:asciidoctorj-pdf:1.5.0-alpha.16")
classpath("org.jetbrains.dokka:dokka-gradle-plugin:0.9.15")
}
}

// 3rd party plugin repositories can be configured in settings.gradle
plugins {
id "io.spring.dependency-management" version "1.0.5.RELEASE" apply false
id "org.jetbrains.kotlin.jvm" version "1.2.71" apply false
id "org.jetbrains.dokka" version "0.9.15"
id "org.asciidoctor.convert" version "1.5.8"
}

验证

http://static.cyblogs.com/QQ截图20191117070823.png

参考地址

背景

作为一名后端开发人员,其实需要掌握的知识还真的需要很多的很多的。这也是我自己一直为什么觉得作为程序员是一种幸福,因为很多的知识与技术都掌握在了其他的人前面,可能享受到这个世界知识带来的红利。但技术也是孤独的,越是往深的地方走,越是觉得自己的无知。

这几个月对于知识的深入也是养成了一些习惯,对于源码与原理性的东西越发的感兴趣。也想着自己多输出一些东西出来。也算是记录自己的一个成长吧!

TCP的三次握手

三次握手主要是针对于Client与Server建立连接来描述的。

https://img-blog.csdn.net/20170911145000723

**第一次握手:**由客户端发起TCP连接的请求,此时客户端发送一条报文,其中包含SYN标志位,将SYN设置为1; 以及seq位。设seq = x ; 该报文段成为SYN报文段;

**第二次握手:**服务器收到这条报文后,返回给客户端一条报文,包含ack位、SYN、以及seq位。 其中ack = x+1; SYN = 1; seq = y。该报文段称为SYNACK报文段;

**第三次握手:**当客户端收到SYNACK报文段之后,客户端需要再给服务器发送另外一个报文段,进行确认。该报文段的SYN = 0, seq = x +1,ack = y+1;

总结一下,就是客户端先跟服务端做一个试探,看看服务端是不是可以被连接的,如果回复是可以的,那么客户端才真的发起请求过去。

TCP的四次挥手

四次挥手主要是针对于Client与Server关闭连接来描述的。

https://img-blog.csdnimg.cn/20191102213543484.png

TCP连接数据传输结束后,通信的双方client与server都可以选择释放当前TCP连接,此时client与server都处于ESTABLISHED(连接确立)状态,TCP连接释放从此状态开始,我们假设是client的应用进程主动发起TCP连接释放:

**第一次挥手:**client向server主动发送连接释放报文(FIN=1,seq=u),报文的首部控制位FIN=1,代表自己的数据已经发送完毕,并且要求释放TCP连接。序号seq=u,u的值为client前面已传送数据的最后一个字节序号加1,client发送完后进入FIN-WAIT-1(终止等待1)状态。

**第二次挥手:**server收到client的连接释放报文后即给出确认报文(ACK=1,ack=u+1,seq=v),序号seq=v,值为server前面已传送数据的最后一个字节序号加1,然后server进入CLOSE-WAIT(关闭等待)状态。这时候client到server这个方向连接就释放了,TCP连接处于 半关闭(half-close)状态,client不再发送数据,但是server仍然可以发送数据给client。client收到server确认后进入FIN-WAIT-2(终止等待2)状态,然后等待server发出连接释放报文。

**第三次挥手:**处于CLOSE-WAIT状态的server发送完所有数据后,主动释放连接,server发送的连接释放报文(FIN=1,ACK=1,seq=W,ack=u+1),因为半关闭状态,server可能又发送了一些数据,所以序号的值为W,同时保持确认号ack=U+1与上次一致,发送完毕后,server进入LAST-ACK(最后确认)状态,等待client确认。

**第四次挥手:**client收到server连接释放报文后,给出确认报文(ACK=1,ack=w+1,seq=u+1),此时连接还没释放掉,client要时间等待计时器设置的2MSL的时间,才最终进入CLOSED状态。时间MSL是最长报文寿命时长,RFC793建议为2分钟,但是现在网络中,这个时间设置更小。也就是说client要等待4分钟才能进入CLOSED状态,开始下一个连接。

疑问点

1、为什么client在TIME-WAIT状态必须等待2MSL的时间?
  • 为了保证client的最后发送的ACK报文能到达server。因为这个ACK报文可能丢失,导致处于LAST-ACK状态的server收不到对自己释放连接报文的确认。若是server超时重传了这个报文,client就能在2MSL时间内收到,并且重新一次确认,并重启2MSL计时器。
  • 防止出现“已失效的连接请求报文段”出现,2MSL时间,可以使本连接持续时间内的报文段都从网络中消失。建立下一个TCP连接时就不会出现上次旧连接请求报文段。
2、如果一方突然出故障了怎么办?

在TCP连接建立后,client与server传输过程中,假设client突然出故障了,server显然无法再收到client数据了,但是server不能白白等下去。这时TCP的保活计时器(keepalive timer)就登场了。server每收到一次client的数据,就重新设置一下计时器,时间通常是2小时,若2小时内没有再收到client数据,server就会发送一次探测报文段,以后每隔75分钟发送一次,若一连发送10次,client都没有任何响应,server会认为client故障了,直接关闭连接。

https://img-blog.csdnimg.cn/20191103114603194.png

该篇博文基本上属于一个科普性的文章,基本也是参考了@magic_1024@qq_39833075 的文章,尊重别人的原创。

参考地址

环境

spring-framework:5.1.x

spring-boot: v2.1.2.RELEASE

看一眼历史的感觉

先看一眼我们很久以前用的XML的配置方式,我举得用最原始的方式来学习会相对于简单,因为很多的配置都是显性的。我只截取最核心的部分,大概找一下感觉。

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
<?xml version="1.0" encoding="UTF-8" ?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="
http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

<context-param>
<param-name>contextConfigLocation</param-name> <!--参数名字不能随意取,约定的。-->
<param-value>classpath:context.xml</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
</filter>

<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:config/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

</web-app>

上面的配置基本就把一个SpringMVC的项目配置完成了,大家都了解。web.xml是一个WEB项目的入口,而这里面就把Spring与Servlet关联起来了。

Loader1:org.springframework.web.context.ContextLoaderListenerIOC容器,管理所有的Bean

Loader2:org.springframework.web.servlet.DispatcherServletIOC容器,主要关于与WEB相关的一些配置,比如:ControllerHandlerMapping等等。

这里粗略的描述一下WEB项目的一个加载顺序:listener → filter → servlet。

ContextLoaderListener

1
2
3
4
org.springframework.web.context.ContextLoaderListener
javax.servlet.ServletContextListener
javax.servlet.ServletContext
javax.servlet.ServletContextEvent

ContextLoaderListener(spring中的类)继承ContextLoader(spring中的类),并实现ServletContextListener(servlet中的接口),ServletContextListener监听ServletContext,当容器启动时,会触发ServletContextEvent事件,该事件由ServletContextListener来处理,启动初始化ServletContext时,调用contextInitialized方法。而ContextLoaderListener实现了ServletContextListener,所以,当容器启动时,触发ServletContextEvent事件,让ContextLoaderListener执行实现方法contextInitialized(ServletContextEvent sce);

引自:https://www.jianshu.com/p/c1384f3d5698

1
2
3
4
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}

我们细看一下我们是如何初始化一个Context的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
// 确定我们容器是哪个Context
protected Class<?> determineContextClass(ServletContext servletContext)
// public static final String CONTEXT_CLASS_PARAM = "contextClass";
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
if (contextClassName != null) {
try {
return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
} catch (ClassNotFoundException ex) {
throw new ApplicationContextException("Failed to load custom context class [" + contextClassName + "]", ex);
}
} else {
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException("Failed to load default context class [" + contextClassName + "]", ex);
}
}
}

servletContext.getInitParameter(CONTEXT_CLASS_PARAM); 这句话实际上是优先用用户配置的,否则才会取默认的。如果我们自己配置要在哪儿配置了。对的还是要在我们的web.xml里面。

1
2
3
4
<context-param>   
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.StaticWebApplicationContext</param-value>
</context-param>

那我们默认的是哪个呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static {
// Load default strategy implementations from properties file.
// This is currently strictly internal and not meant to be customized
// by application developers.
try {
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
}
}

private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";

是滴,有一个properties文件,里面就是默认的Context配置。

1
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

实际上,我们默认的就是XmlWebApplicationContext。继续扫读web.xml的配置来加载与Spring相关的配置。

1
2
3
4
5
6
// public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
// 这里就是all.xml/application.xml/context.xml 等的加载地方
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}

调用refresh开始构建

1
wac.refresh(); // 然而这里我觉得要单独拿一个篇章来讲Spring是如何来加载Bean。

DispatcherServlet

  • HttpServlet 及以上部分是 Servlet 标准中提供的接口及类
  • DispatcherServlet、FrameworkServlet、HttpServletBean 三者是 SpringMVC 提供的类,且后者依次分别是前者的父类。

http://static.cyblogs.com/WX20191114-20465.png

因为是Servlet,所有会调用init来初始化。

org.springframework.web.servlet.HttpServletBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final void init() throws ServletException {
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}

// Let subclasses do whatever initialization they like.
initServletBean();
}

org.springframework.web.servlet.FrameworkServlet

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
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();

try {
// 看到这里,回到之前说初始化ContextLoaderListener的initWebApplicationContext
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}

if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}

if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}

org.springframework.web.servlet.FrameworkServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
//public static final Class<?> DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class;
Class<?> contextClass = getContextClass();
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(
"Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

wac.setEnvironment(getEnvironment());
wac.setParent(parent);
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
configureAndRefreshWebApplicationContext(wac); // 这里面又会调用wac.refresh();
return wac;
}

看到这里,我们的2个容器都是默认用的XmlWebApplicationContext

那问哪些HandlerMappingHandlerAdapterViewResolver是在哪儿加载进来的?

org.springframework.web.servlet.DispatcherServlet#onRefresh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
// 初始化HandlerMapping
initHandlerMappings(context);
// 初始化HandlerAdapter
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping

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
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());

if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
  • 遍历Handler中的所有方法,找出其中被@RequestMapping注解标记的方法。
  • 然后遍历这些方法,生成RequestMappingInfo实例。
  • 将RequestMappingInfo实例以及处理器方法注册到缓存中。

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
// RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); 只要打了注解@RequestMapping的方法
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).build().combine(info);
}
}
return info;
}
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
public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
assertUniqueMethodMapping(handlerMethod, mapping);
this.mappingLookup.put(mapping, handlerMethod);

List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
this.urlLookup.add(url, mapping);
}

String name = null;
if (getNamingStrategy() != null) {
name = getNamingStrategy().getName(handlerMethod, mapping);
addMappingName(name, handlerMethod);
}

CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
this.corsLookup.put(handlerMethod, corsConfig);
}

this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
}
finally {
this.readWriteLock.writeLock().unlock();
}
}

把一些请求的映射关系放入到Map中,为后续的路由功能做数据初始化。

1
2
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();

对于Request参数的一些封装&映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public RequestMappingInfo combine(RequestMappingInfo other) {
String name = combineNames(other);
PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition);
RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition);
ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition);
HeadersRequestCondition headers = this.headersCondition.combine(other.headersCondition);
ConsumesRequestCondition consumes = this.consumesCondition.combine(other.consumesCondition);
ProducesRequestCondition produces = this.producesCondition.combine(other.producesCondition);
RequestConditionHolder custom = this.customConditionHolder.combine(other.customConditionHolder);

return new RequestMappingInfo(name, patterns,
methods, params, headers, consumes, produces, custom.getCondition());
}

一般我们对关心的是一个url是如何组装的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public PatternsRequestCondition combine(PatternsRequestCondition other) {
Set<String> result = new LinkedHashSet<>();
if (!this.patterns.isEmpty() && !other.patterns.isEmpty()) {
for (String pattern1 : this.patterns) {
for (String pattern2 : other.patterns) {
result.add(this.pathMatcher.combine(pattern1, pattern2));
}
}
}
else if (!this.patterns.isEmpty()) {
result.addAll(this.patterns);
}
else if (!other.patterns.isEmpty()) {
result.addAll(other.patterns);
}
else {
result.add("");
}
return new PatternsRequestCondition(result, this.pathHelper, this.pathMatcher,
this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions);
}

这是从注释上copy下来的注解,主要有这里的pathMatcher来组装。

http://static.cyblogs.com/WX20191115-093733@2x.png

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
public String combine(String pattern1, String pattern2) {
if (!StringUtils.hasText(pattern1) && !StringUtils.hasText(pattern2)) {
return "";
}
if (!StringUtils.hasText(pattern1)) {
return pattern2;
}
if (!StringUtils.hasText(pattern2)) {
return pattern1;
}

boolean pattern1ContainsUriVar = (pattern1.indexOf('{') != -1);
if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) {
// /* + /hotel -> /hotel ; "/*.*" + "/*.html" -> /*.html
// However /user + /user -> /usr/user ; /{foo} + /bar -> /{foo}/bar
return pattern2;
}

// /hotels/* + /booking -> /hotels/booking
// /hotels/* + booking -> /hotels/booking
if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) {
return concat(pattern1.substring(0, pattern1.length() - 2), pattern2);
}

// /hotels/** + /booking -> /hotels/**/booking
// /hotels/** + booking -> /hotels/**/booking
if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) {
return concat(pattern1, pattern2);
}

int starDotPos1 = pattern1.indexOf("*.");
if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) {
// simply concatenate the two patterns
return concat(pattern1, pattern2);
}

String ext1 = pattern1.substring(starDotPos1 + 1);
int dotPos2 = pattern2.indexOf('.');
String file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2));
String ext2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2));
boolean ext1All = (ext1.equals(".*") || ext1.isEmpty());
boolean ext2All = (ext2.equals(".*") || ext2.isEmpty());
if (!ext1All && !ext2All) {
throw new IllegalArgumentException("Cannot combine patterns: " + pattern1 + " vs " + pattern2);
}
String ext = (ext1All ? ext2 : ext1);
return file2 + ext;
}

还是稍微的有点粗,也只描述了我们最最关心的一些点。后面再继续的对每个细节点做一个总结。

参考地址:

证书申请

关于证书申请,其实我们可以申请免费的证书即可,在阿里云或者腾讯云等地方都能申请到免费一年的证书,具体的申请步骤这里就就不细细描述了,因为比较简单。但这里指的提醒一下的是,二级域名与三级域名是要区别开来的,一个证书对应一个域名。

比如:cyblogs.comgitlab.cyblogs.com是需要单独申请的,我这里的话因为域名解析是在dnspod解析的,所以我也就在它那里申请了。

如果是在阿里云申请,而在其他地方做的域名解析,第一次需要单独配置一次解析才行。

http://static.cyblogs.com/QQ截图20191103175026.png

Nginx的安装技巧

gitlab.cyblogs.com.conf文件内容,之类把你的证书存放在你想放的位置,我这里是:/usr/local/nginx/ssl

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

upstream gitlab {
server unix:/var/opt/gitlab/gitlab-rails/sockets/gitlab.socket fail_timeout=0;
}

upstream gitlab-workhorse {
server unix://var/opt/gitlab/gitlab-workhorse/socket fail_timeout=0;
}

server {
listen 0.0.0.0:80;
server_name gitlab.cyblogs.com;
server_tokens off;
return 301 https://$server_name$request_uri;
access_log /usr/local/nginx/conf/logs/gitlab_access.log;
error_log /usr/local/nginx/conf/logs/gitlab_error.log;
}

server {
listen 0.0.0.0:443 ssl;
server_name gitlab.cyblogs.com;
server_tokens off;
root /opt/gitlab/embedded/service/gitlab-rails/public;

client_max_body_size 20m;

ssl_certificate /usr/local/nginx/ssl/gitlab.cyblogs.com_bundle.crt;
ssl_certificate_key /usr/local/nginx/ssl/gitlab.cyblogs.com.key;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 5m;

access_log /usr/local/nginx/conf/logs/gitlab_access.log;
error_log /usr/local/nginx/conf/logs/gitlab_error.log;

location /uploads/ {
gzip off;
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_redirect off;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_pass https://gitlab;
}

location @gitlab {
gzip off;
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_redirect off;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_pass http://localhost:8081;
}

location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects {
client_max_body_size 0;
error_page 418 = @gitlab-workhorse;
return 418;
}

location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ {
client_max_body_size 0;
error_page 418 = @gitlab-workhorse;
return 418;
}

location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive {
client_max_body_size 0;
error_page 418 = @gitlab-workhorse;
return 418;
}

location ~ ^/api/v3/projects/.*/repository/archive {
client_max_body_size 0;
error_page 418 = @gitlab-workhorse;
return 418;
}

location ~ ^/[\w\.-]+/[\w\.-]+/builds/download {
client_max_body_size 0;
error_page 418 = @gitlab-workhorse;
return 418;
}

location ~ /ci/api/v1/builds/[0-9]+/artifacts {
client_max_body_size 0;
error_page 418 = @gitlab-workhorse;
return 418;
}

location @gitlab-workhorse {
client_max_body_size 0;
gzip off;
proxy_buffering off;

proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_redirect off;

proxy_http_version 1.1;

proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://gitlab-workhorse;
}

location ~ ^/(assets)/ {
root /opt/gitlab/embedded/service/gitlab-rails/public;
gzip_static on;
expires max;
add_header Cache-Control public;
}

location ~ / {
root /opt/gitlab/embedded/service/gitlab-rails/public;
try_files $uri $uri/index.html $uri.html @gitlab;
}

error_page 502 /502.html;
}

SSH拉取OK,Http方式不行

http://static.cyblogs.com/QQ截图20191103175454.png

首先这里显示的还是http的方式,并不是https的方式。

1
2
3
4
5
6
➜  Desktop  git clone https://gitlab.cyblogs.com/root/testdemo.git
Cloning into 'cyblogs-blog'...
fatal: unable to access 'https://gitlab.cyblogs.com/root/testdemo.git/': The requested URL returned error: 502
➜ Desktop git clone https://gitlab.cyblogs.com/cyblogs/cyblogs-blog.git
Cloning into 'cyblogs-blog'...
fatal: unable to access 'https://gitlab.cyblogs.com/cyblogs/cyblogs-blog.git/': The requested URL returned error: 502

通过看日志分析,发现错误的日志信息:

1
2
3
4
[root@iZ94tq694y3Z logs]# less gitlab_error.log 
2019/10/18 16:30:13 [crit] 15450#0: *97 stat() "/opt/gitlab/embedded/service/gitlab-rails/public/uploads/-/system/user/avatar/2/avatar.png.html" failed (13: Permission denied), client: xxx.xx.xx.xx, server: gitlab.cyblogs.com, request: "GET /uploads/-/system/user/avatar/2/avatar.png?width=23 HTTP/1.1", host: "gitlab.cyblogs.com", referrer: "https://gitlab.cyblogs.com/testcase/config-repo"

2019/11/02 16:40:10 [crit] 1374#0: *24502 connect() to unix://var/opt/gitlab/gitlab-workhorse/socket failed (13: Permission denied) while connecting to upstream, client: 210.22.21.66, server: gitlab.cyblogs.com, request: "GET /root/testdemo.git/info/refs?service=git-upload-pack HTTP/1.1", upstream: "http://unix://var/opt/gitlab/gitlab-workhorse/socket:/root/testdemo.git/info/refs?service=git-upload-pack", host: "gitlab.cyblogs.com"

这里会一直报一个权限问题。unix://var/opt/gitlab/gitlab-workhorse/socket failed (13: Permission denied),然后我就各种搜索,真心地没有几篇文章说的很好的。还不如耐心的看gitlab的官网配置,还算比较详细。

https://docs.gitlab.com/omnibus/settings/nginx.html

看了大量的文章,最终得到解决步骤。

对于nginx启动配置

首先,自己的搭建的nginx启动的时候不要用root启动,需要创建一个用户。我这里就是nginx用户了。

1
2
3
[root@iZ94tq694y3Z ~]# groups nginx
nginx : nginx gitlab-www # 这里的gitlab-www是gitlab-ctl reconfigure后加入进去的
[root@iZ94tq694y3Z ~]#

需要在nginx.confuser该用户。

1
2
3
[root@iZ94tq694y3Z conf]# cat nginx.conf
user nginx nginx;
worker_processes 1;
对于gitlab.rb配置
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
[root@iZ94tq694y3Z gitlab]# cat gitlab.rb | grep -v ^# 只要生效的配置
# 域名访问的配置
external_url 'https://gitlab.cyblogs.com'

gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.sina.com"
gitlab_rails['smtp_port'] = 465
gitlab_rails['smtp_user_name'] = "chengcheng222e@sina.com"
gitlab_rails['smtp_password'] = "xxxxxx"
gitlab_rails['smtp_domain'] = "sina.com"
gitlab_rails['smtp_authentication'] = "plain"
gitlab_rails['smtp_enable_starttls_auto'] = true
gitlab_rails['smtp_tls'] = false

# 配置gitlab_workhorse,nginx部分有用到这块
gitlab_workhorse['enable'] = true
gitlab_workhorse['ha'] = false
gitlab_workhorse['listen_network'] = "unix"
gitlab_workhorse['listen_umask'] = 000
gitlab_workhorse['listen_addr'] = "/var/opt/gitlab/gitlab-workhorse/socket"
gitlab_workhorse['auth_backend'] = "http://localhost:8081"

# 修改端口号为8081端口
unicorn['port'] = 8081

#特别是web_server部分,需要把nginx启动启用加入权限
web_server['external_users'] = ['nginx']
web_server['username'] = 'nginx'
web_server['group'] = 'nginx'
web_server['home'] = '/usr/local/nginx'

nginx['enable'] = false
nginx['redirect_http_to_https'] = true
nginx['listen_port'] = 8081

如何定位错误,之类需要看nginx的日志与gitlab的日志

1
2
3
4
# 查看nginx
tailf /usr/local/nginx/conf/logs/gitlab_error.log
# 查看gitlab
gitlab-ctl tail

验证

1
2
3
4
5
6
7
8
# 回家切换成Windows系统了
Administrator@CHENYUAN MINGW64 ~/Desktop
$ git clone https://gitlab.cyblogs.com/root/testdemo.git
Cloning into 'testdemo'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.

参考地址:

准备

大家在安装Kubernetes的时候,大多数人都遇到了一直处理starting的状态。其实都是因为依赖的docker images不存在。而且由于墙等问题的存在,大家可以安装之前提前把镜像下载好。

我这里找到了很多的文档,找齐了我这边能支持跑起来的image

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
➜  kubernetes  docker images
REPOSITORY TAG
k8s.gcr.io/kube-apiserver v1.16.0
registry.cn-hangzhou.aliyuncs.com/google_containers/kube-apiserver v1.16.0
k8s.gcr.io/kube-proxy v1.16.0
registry.cn-hangzhou.aliyuncs.com/google_containers/kube-proxy v1.16.0
k8s.gcr.io/kube-proxy v1.14.6
k8s.gcr.io/kube-apiserver v1.14.6
k8s.gcr.io/kube-scheduler v1.14.6
k8s.gcr.io/kube-controller-manager v1.14.6
docker/kube-compose-controller v0.4.23
docker/kube-compose-api-server v0.4.23
k8s.gcr.io/coredns 1.3.1
k8s.gcr.io/kubernetes-dashboard-amd64 v1.10.1
k8s.gcr.io/etcd 3.3.10
k8s.gcr.io/kube-proxy-amd64 v1.10.11
k8s.gcr.io/kube-apiserver-amd64 v1.10.11
k8s.gcr.io/kube-controller-manager-amd64 v1.10.11
k8s.gcr.io/kube-scheduler-amd64 v1.10.11
docker/kube-compose-controller v0.4.12
docker/kube-compose-api-server v0.4.12
k8s.gcr.io/etcd-amd64 3.1.12
k8s.gcr.io/k8s-dns-dnsmasq-nanny-amd64 1.14.8
k8s.gcr.io/k8s-dns-sidecar-amd64 1.14.8
k8s.gcr.io/k8s-dns-kube-dns-amd64 1.14.8
k8s.gcr.io/pause-amd64 3.1
k8s.gcr.io/pause 3.1
k8s.gcr.io/storage-provisioner v1.8.1
registry.cn-hangzhou.aliyuncs.com/google_containers/storage-provisioner v1.8.1

再启动,等一会儿就应该能看到Docker与Kubernetes都起来了。

http://static.cyblogs.com/WX20191015-184532@2x.png

创建kubernetes-dashboard

接下来我们可以使用 kubectl 命令来创建简单的 kubernetes-dashboard 服务:

1
2
3
4
5
6
7
8
9
10
11
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v1.10.1/src/deploy/recommended/kubernetes-dashboard.yaml
# 这个可以把文件下载下来,后面就可以本地了。

kubectl apply -f /Users/chenyuan/Tools/Docker/kubernetes/kubernetes-dashboard.yaml

secret "kubernetes-dashboard-certs" created
serviceaccount "kubernetes-dashboard" created
role "kubernetes-dashboard-minimal" created
rolebinding "kubernetes-dashboard-minimal" created
deployment "kubernetes-dashboard" created
service "kubernetes-dashboard" created

服务安装完毕后可以查看部署的容器与服务:

1
2
3
4
5
6
7
8
➜  kubernetes  kubectl get deployments --namespace kube-system
NAME READY UP-TO-DATE AVAILABLE AGE
coredns 2/2 2 2 4d3h
kubernetes-dashboard 1/1 1 1 3d8h
➜ kubernetes kubectl get services --namespace kube-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 4d3h
kubernetes-dashboard ClusterIP 10.96.229.197 <none> 443/TCP 4d2h

启动dashboard

在 Dashboard 启动完毕后,可以使用 kubectl 提供的 Proxy 服务来访问该面板

1
kubectl proxy --address='0.0.0.0'  --accept-hosts='^*$'

启动服务后,不要切断控制台,不然服务就中断了。

浏览器输入:

1
http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/

http://static.cyblogs.com/WX20191015-182857@2x.png

获取Token

然后并没有跳过的按钮,所以必须通过Kubeconfig或者Token的方式。

我这里是通过Token,那我们怎么知道Token的值是多少呢?

1
2
3
4
5
6
➜  kubernetes  kubectl get secret -n=kube-system
NAME TYPE DATA AGE
...
default-token-sznp4 kubernetes.io/service-account-token 3 4d3h
...
# 这里只列出default-token-sznp4

获取Token值,然后把得到的值输入进去就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  kubernetes  kubectl describe secret -n=kube-system default-token-sznp4
Name: default-token-sznp4
Namespace: kube-system
Labels: <none>
Annotations: kubernetes.io/service-account.name: default
kubernetes.io/service-account.uid: 064afefb-ebf6-11e9-ac8c-025000000001

Type: kubernetes.io/service-account-token

Data
====
ca.crt: 1025 bytes
namespace: 11 bytes
token: eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJkZWZhdWx0LXRva2VuLXN6bnA0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImRlZmF1bHQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIwNjRhZmVmYi1lYmY2LTExZTktYWM4Yy0wMjUwMDAwMDAwMDEiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06ZGVmYXVsdCJ9.u5HTqt7A_4H_0f9ny-AgfmWNo7TSWZsRpjXot1iN8G6oOnt4uDQiS_kiUduwtqqeYC2hjZ2yKPt0NNML9Op1RSAEuTkXiRvJxnCX8GjQeqCD4lzXeqqQ9mTxCVlGijJLaP5VJ2qQtLM0Gwt9eJCYxugGqqHqys7QXdPzcH3WESno0tXNt25klC5ZXNFSeyE-AqLpP3SjmW7W6IBHx89uY28SXmdvTjnCuZyaBlpkgOensdMS7-BpycTzq63NIcp5TR7tM3AdHjsUlSJ2D9YqW_xzMcEDncmjKpbVJ6W9w494L-Z0dOjHkI7gaQSE2Bwi6AqCaGEWKTgMCSWmIBfkrg

通过compose的case启动服务

去Github找了一个Demo,跑几个服务起来。案例地址:git@github.com:docker/compose-on-kubernetes.git

我把其中的案例copy到了我自己的目录,大概是这样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
➜  kubernetes  tree -L 2
.
├── config-exercise
│   └── config-demo
├── db
│   ├── Dockerfile
│   └── words.sql
├── docker-compose.yml
├── kubernetes-dashboard.yaml
├── web
│   ├── Dockerfile
│   ├── dispatcher.go
│   └── static
└── words
├── Dockerfile
├── pom.xml
└── src

着重看一下docker-compose内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
➜  kubernetes  cat docker-compose.yml
version: '3.3'

services:
web:
build: web
image: dockerdemos/lab-web
ports:
- "80:80"

words:
build: words
image: dockerdemos/lab-words
deploy:
replicas: 5

db:
build: db
image: dockerdemos/lab-db
volumes:
- test-volume:/test-volume

验证

然后刷新页面,就可以看到搭建的节点都在Kubernetes的控制台上面可以发现了。

http://static.cyblogs.com/WX20191015-174028@2x.png

用Docker命令查看本地的服务

1
2
3
4
5
6
7
8
9
➜  Desktop  docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
02d0691dee78 19b138d3318a "docker-entrypoint.s…" 11 minutes ago Up 11 minutes k8s_db_db-0_default_d8be11b3-ef2e-11e9-ac8c-025000000001_0
f719b60a99c8 b1e9c4adf655 "java -Xmx8m -Xms8m …" 11 minutes ago Up 11 minutes k8s_words_words-745db75bdf-4slj7_default_d8734615-ef2e-11e9-ac8c-025000000001_0
4a2ce12bd5e8 b1e9c4adf655 "java -Xmx8m -Xms8m …" 11 minutes ago Up 11 minutes k8s_words_words-745db75bdf-bfz5c_default_d8799138-ef2e-11e9-ac8c-025000000001_0
755679d0813b a7ba5776710d "/dispatcher" 11 minutes ago Up 11 minutes k8s_web_web-8ffd8b7f4-scdmz_default_d86e9b2e-ef2e-11e9-ac8c-025000000001_0
ab8bbda27700 b1e9c4adf655 "java -Xmx8m -Xms8m …" 11 minutes ago Up 11 minutes k8s_words_words-745db75bdf-w2dxd_default_d878bdf2-ef2e-11e9-ac8c-025000000001_0
5c5943bd4f34 b1e9c4adf655 "java -Xmx8m -Xms8m …" 11 minutes ago Up 11 minutes k8s_words_words-745db75bdf-bzdbg_default_d86ebe31-ef2e-11e9-ac8c-025000000001_0
6e1b7bbffaa9 b1e9c4adf655 "java -Xmx8m -Xms8m …" 11 minutes ago Up 11 minutes k8s_words_words-745db75bdf-2xwgr_default_d87525d6-ef2e-11e9-ac8c-025000000001_0

其他命令

应用栈创建完毕后,可以使用 kubectl 查看创建的 Pods:

1
kubectl get pods 

也可以来查看部署的集群与服务:

1
kubectl get deployments 

可以看到这里的 web 有所谓的 LoadBalancer 类型,即可以对外提供服务。最后我们还可以用 stack 与 kubectl 命令来删除应用:

1
2
3
docker stack remove demo 

kubectl delete deployment kubernetes-dashboard --namespace kube-system
0%