介绍下spring数据源连接的源码类:|

  1 spring动态切换连接池需要类AbstractRoutingDataSource的源码
2 /*
3 * Copyright 2002-2017 the original author or authors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.springframework.jdbc.datasource.lookup;
19
20 import java.sql.Connection;
21 import java.sql.SQLException;
22 import java.util.HashMap;
23 import java.util.Map;
24 import javax.sql.DataSource;
25
26 import org.springframework.beans.factory.InitializingBean;
27 import org.springframework.jdbc.datasource.AbstractDataSource;
28 import org.springframework.lang.Nullable;
29 import org.springframework.util.Assert;
30
31 /**
32 * Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
33 * calls to one of various target DataSources based on a lookup key. The latter is usually
34 * (but not necessarily) determined through some thread-bound transaction context.
35 *
36 * @author Juergen Hoeller
37 * @since 2.0.1
38 * @see #setTargetDataSources
39 * @see #setDefaultTargetDataSource
40 * @see #determineCurrentLookupKey()
41 */
42 public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
43
44 @Nullable
45 private Map<Object, Object> targetDataSources;
46
47 @Nullable
48 private Object defaultTargetDataSource;
49
50 private boolean lenientFallback = true;
51
52 private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
53
54 @Nullable
55 private Map<Object, DataSource> resolvedDataSources;
56
57 @Nullable
58 private DataSource resolvedDefaultDataSource;
59
60
61 / * *
62 *指定目标数据源的映射,查找键为键。
63 *映射的值可以是对应的{@link javax.sql.DataSource}
64 实例或数据源名称字符串(通过{@link setDataSourceLookup DataSourceLookup}解析)。
65 * <p>密钥可以是任意类型;该类只实现泛型查找过程。
66 具体的键表示将由{
67 @link # resolvespeciedlookupkey (Object)}和{@link #行列式urrentlookupkey()}处理。
68 * /
69 public void setTargetDataSources(Map<Object, Object> targetDataSources) {
70 this.targetDataSources = targetDataSources;
71 }
72
73
74
75
76 / * *
77 *指定默认的目标数据源(如果有的话)。
78 * <p>映射值可以是对应的
79 {@link javax.sql.DataSource}
80 实例或数据源名称字符串
81 (通过{@link #setDataSourceLookup DataSourceLookup}解析)。
82 * <p>如果key {@link #setTargetDataSources targetDataSources}
83 没有匹配{@link #决定ecurrentlookupkey()}当前查找键,
84 则此数据源将被用作目标。
85 * /
86
87 public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
88 this.defaultTargetDataSource = defaultTargetDataSource;
89 }
90
91 / * *
92 *指定是否对默认数据源应用宽松的回退
93 *如果无法为当前查找键找到特定的数据源。
94 * <p>默认为“true”,接受在目标数据源映射中没有对应条目的查找键——在这种情况下,简单地返回到默认数据源。
95 * <p>将此标志切换为“false”,如果您希望回退仅在查找键为{@code null}时应用。
96 没有数据源项的查找键将导致IllegalStateException。
97 * @see # setTargetDataSources
98 * @see # setDefaultTargetDataSource
99 * @see # determineCurrentLookupKey ()
100 * /
101 public void setLenientFallback(boolean lenientFallback) {
102 this.lenientFallback = lenientFallback;
103 }
104
105 / * *
106 *设置DataSourceLookup实现来解析数据源
107 {@link #setTargetDataSources targetDataSources}映射中的名称字符串。
108 * <p>默认是{@link JndiDataSourceLookup},允许JNDI名称
109 *直接指定的应用服务器数据源。
110 * /
111 public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) {
112 this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
113 }
114
115
116 @Override
117 public void afterPropertiesSet() {
118 if (this.targetDataSources == null) {
119 throw new IllegalArgumentException("Property 'targetDataSources' is required");
120 }
121 this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
122 this.targetDataSources.forEach((key, value) -> {
123 Object lookupKey = resolveSpecifiedLookupKey(key);
124 DataSource dataSource = resolveSpecifiedDataSource(value);
125 this.resolvedDataSources.put(lookupKey, dataSource);
126 });
127 if (this.defaultTargetDataSource != null) {
128 this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
129 }
130 }
131
132 / * *
133 *将给定的查找键对象
134 *(如{@link #setTargetDataSources targetDataSources}映射中指定的)解析为实际的查找键,
135 *用于与{@link #决定ecurrentlookupkey()当前查找键}匹配。
136 * <p>默认实现只是简单地返回给定的键值。
137 * @param lookupKey用户指定的查找键对象
138 * @根据需要返回查找键以进行匹配
139 * /
140 protected Object resolveSpecifiedLookupKey(Object lookupKey) {
141 return lookupKey;
142 }
143
144
145 / * *
146 *将指定的数据源对象解析为数据源实例。
147 * <p>默认实现处理数据源实例和数据源名称(通过{@link #setDataSourceLookup DataSourceLookup}解析)。
148 {@link #setTargetDataSources targetDataSources}映射中指定的数据源值对象
149 * @返回已解析的数据源(绝不是{@code null})
150 * @抛出不支持的值类型的IllegalArgumentException
151 * /
152
153 protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
154 if (dataSource instanceof DataSource) {
155 return (DataSource) dataSource;
156 }
157 else if (dataSource instanceof String) {
158 return this.dataSourceLookup.getDataSource((String) dataSource);
159 }
160 else {
161 throw new IllegalArgumentException(
162 "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
163 }
164 }
165
166
167 @Override
168 public Connection getConnection() throws SQLException {
169 return determineTargetDataSource().getConnection();
170 }
171
172 @Override
173 public Connection getConnection(String username, String password) throws SQLException {
174 return determineTargetDataSource().getConnection(username, password);
175 }
176
177 @Override
178 @SuppressWarnings("unchecked")
179 public <T> T unwrap(Class<T> iface) throws SQLException {
180 if (iface.isInstance(this)) {
181 return (T) this;
182 }
183 return determineTargetDataSource().unwrap(iface);
184 }
185
186 @Override
187 public boolean isWrapperFor(Class<?> iface) throws SQLException {
188 return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface));
189 }
190
191
192 /**
193 *检索当前目标数据源。决定了
194 * {@link # definecurrentlookupkey()当前查找键},在{@link #setTargetDataSources targetDataSources}映射中执行查找,返回指定的
195 * {@link #setDefaultTargetDataSource默认目标数据源}如果需要。
196 * @see # determineCurrentLookupKey ()
197
198 通多debug会发现DataSource dataSource = this.resolvedDataSources.get(lookupKey);非常关键。获取数据源类似key的标识
199
200 */
201
202 protected DataSource determineTargetDataSource() {
203 Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
204 Object lookupKey = determineCurrentLookupKey();
205 DataSource dataSource = this.resolvedDataSources.get(lookupKey);
206 if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
207 dataSource = this.resolvedDefaultDataSource;//没有数据源设置为默认的数据源
208 }
209 if (dataSource == null) {//没切换数据源并且没有设置默认数据源,此处抛出异常
210 throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
211 }
212 return dataSource;//返回数据源对象
213 }
214
215 /*
216 * 确定当前查找键。这通常是
217 *用于检查线程绑定的事务上下文。
218 * <p>允许任意键。返回的密钥需要方法解析后匹配存储的查找键类型
219 * {@link # resolvespeciedlookupkey}方法。
220 */
221 @Nullable
222 protected abstract Object determineCurrentLookupKey();//抽像方法,需要重写然后在protected DataSource determineTargetDataSource() 中调用
223
224 }

源码中关键类的介绍:

上面的方法走完后下辖一步debug就是获取驱动连接:

数据源切换代码标记图:

下面是配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
"> <!--原始默认数据源配置C3P0--> <!--<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">-->
<!--<property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>-->
<!--<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/quanxian?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=false&amp;allowPublicKeyRetrieval=true"/>-->
<!--<property name="user" value="wangbiao"/>-->
<!--<property name="password" value="w@2014221317b"/>-->
<!--&lt;!&ndash;默认为0,单位为秒,表示在连接池中未被使用的连接最长存活多久不被移除&ndash;&gt;-->
<!--<property name="maxIdleTime" value="3600"/>-->
<!--&lt;!&ndash;默认为3表示连接池中任何时候可以存放的连接最小数量。&ndash;&gt;-->
<!--<property name="minPoolSize" value="1"/>-->
<!--&lt;!&ndash; 默认为15,表示连接池中任何时候可以存放的连接最大数量。&ndash;&gt;-->
<!--<property name="maxPoolSize" value="5"/>-->
<!--&lt;!&ndash;默认为3,表示初始化连接池时获取的连接个数。该数值在miniPoolSize和maxPoolSize之间。&ndash;&gt;-->
<!--<property name="initialPoolSize" value="2"/>-->
<!--&lt;!&ndash;表示当连接池中连接用完时,客户端调用getConnection获取连接等待的时间 如果超时,则抛出SQLException异常。特殊值0表示无限期等待&ndash;&gt;-->
<!--<property name="checkoutTimeout" value="4800000"/>-->
<!--</bean>--> <!--数据源0-->
<bean id="dataSource0" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/quanxian?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=false&amp;allowPublicKeyRetrieval=true"/>
<property name="username" value="wangbiao"/>
<property name="password" value="w@2014221317b"/>
</bean> <!--数据源1-->
<bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/qrtz_timer?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=false&amp;allowPublicKeyRetrieval=true"/>
<property name="username" value="root"/>
<property name="password" value="w@2014221317b"/>
</bean> <!--多数据源配置-->
<bean id="multiDataSource" class="com.ry.project.dataSouces.DynamicDataSource">
<property name="targetDataSources">
<map>
<entry key="dataSource0" value-ref="dataSource0"></entry>
</map>
</property>
<property name="defaultTargetDataSource" ref="dataSource1"></property>
</bean> <!--<bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">-->
<!--<property name="dataSource" ref="multiDataSource"/>-->
<!--&lt;!&ndash;<property name="configLocation" value="classpath:mybatis-config.xml"/>&ndash;&gt;-->
<!--<property name="mapperLocations" value="classpath*:/mapper/User.xml"/>-->
<!--</bean>--> <!--会话工厂-->
<bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="multiDataSource"/>
<!--<property name="configLocation" value="classpath:mybatis-config.xml"/>-->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<!--使用下面的方式配置参数,一行配置一个 -->
<value>
helperDialect=mysql
reasonable=true
supportMethodsArguments=true
params=count=countSql
autoRuntimeDialect=true
</value>
</property>
</bean>
</array>
</property>
<property name="mapperLocations" value="classpath:mapper/*.xml" />
</bean> <!--mybatis扫描 映射-->
<bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.ry.project.mapper"/>
<property name="sqlSessionFactoryBeanName" value="sessionFactory"/>
</bean> <!--事务管理-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="multiDataSource"/>
</bean> </beans>

在spring配置文件中加上这个Order管控事务与AOP顺序问题,防止实物卡住数据源无法切换:

    <tx:annotation-driven transaction-manager="transactionManager" order="2"/>

下面是我的java代码:相关类引用网友:

https://blog.csdn.net/u013034378/article/details/82469368
 1 package com.ry.project.dataSouces;
2
3 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
4
5 public class DynamicDataSource extends AbstractRoutingDataSource {
6
7 /* ThreadLocal,叫线程本地变量或线程本地存储。
8 * ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
9 * 这里使用它的子类InheritableThreadLocal用来保证父子线程都能拿到值。
10 */
11 private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<String>();
12
13 /**
14 * 设置dataSourceKey的值
15 * @param dataSource
16 */
17 public static void setDataSourceKey(String dataSource) {
18 dataSourceKey.set(dataSource);
19 }
20 /**
21 * 清除dataSourceKey的值
22 */
23 public static void toDefault() {
24 dataSourceKey.remove();
25 }
26
27 @Override
28 protected Object determineCurrentLookupKey() {
29 return dataSourceKey.get();
30 }
31 /**
32 * 返回当前dataSourceKey的值
33 */
34
35
36 }
 1 package com.ry.project.dataSouces;
2
3 import java.lang.annotation.*;
4
5 @Target({ElementType.METHOD,ElementType.TYPE})
6 @Retention(RetentionPolicy.RUNTIME)
7 @Documented
8 public @interface DynamicRoutingDataSource {
9 String value() default "dataSource1";//本文默认dataSource
10 }
 1 package com.ry.project.dataSouces;
2
3 import org.aspectj.lang.JoinPoint;
4 import org.aspectj.lang.annotation.After;
5 import org.aspectj.lang.annotation.Aspect;
6 import org.aspectj.lang.annotation.Before;
7 import org.aspectj.lang.annotation.Pointcut;
8 import org.springframework.core.Ordered;
9 import org.springframework.stereotype.Component;
10
11 import java.lang.reflect.Method;
12
13 @Aspect
14 @Component
15 public class HandlerDataSourceAop implements Ordered {
16
17 /**
18 * @within匹配类上的注解
19 * @annotation匹配方法上的注解
20 */
21 @Pointcut("@within(com.ry.project.dataSouces.DynamicRoutingDataSource)||@annotation(com.ry.project.dataSouces.DynamicRoutingDataSource)")
22 public void pointcut(){}
23
24 @Before(value = "pointcut()")
25 public void beforeOpt(JoinPoint joinPoint) throws NoSuchMethodException {
26 /** 先查找方法上的注解,没有的话再去查找类上的注解
27 *-----------------------------------------------------------------------
28 * 这里使用的是接口的模式,注解在实现类上,所以不能使用如下方式获取目标方法的对象,
29 * 因为该方式获取的是该类的接口或者顶级父类的方法的对象.
30 * MethodSignature methodSignature = (MethodSignature)point.getSignature();
31 * Method method = methodSignature.getMethod();
32 * DynamicRoutingDataSource annotation = method.getAnnotation(DynamicRoutingDataSource.class);
33 * 通过上面代码是获取不到方法上的注解的,如果真要用上面代码来获取,可以修改aop代理模式,修改为cglib代理
34 * 在xml配置文件修改为<aop:aspectj-autoproxy proxy-target-class="true" /> ,
35 * proxy-target-class属性true为cglib代理,默认false为jdk动态代理 。
36 * ---------------------------------------------------------
37 * 本文使用是jdk动态代理, 这里使用反射的方式获取方法
38 */
39 //反射获取Method 方法一
40 Object target = joinPoint.getTarget();
41 Class<?> clazz = target.getClass();
42 Method[] methods = clazz.getMethods();
43 DynamicRoutingDataSource annotation = null;
44 for (Method method : methods) {
45 if (joinPoint.getSignature().getName().equals(method.getName())) {
46 annotation = method.getAnnotation(DynamicRoutingDataSource.class);
47 if (annotation == null) {
48 annotation = joinPoint.getTarget().getClass().getAnnotation(DynamicRoutingDataSource.class);
49 if (annotation == null) {
50 return;
51 }
52 }
53 }
54 }
55
56
57 // 反射获取Method 方法二
58 // Object[] args = joinPoint.getArgs();
59 // Class<?>[] argTypes = new Class[joinPoint.getArgs().length];
60 // for (int i = 0; i < args.length; i++) {
61 // argTypes[i] = args[i].getClass();
62 // }
63 // Method method = joinPoint.getTarget().getClass().getMethod(joinPoint.getSignature().getName(), argTypes);
64 // DynamicRoutingDataSource annotation = method.getAnnotation(DynamicRoutingDataSource.class);
65 // if (annotation == null) {
66 // annotation = joinPoint.getTarget().getClass().getAnnotation(DynamicRoutingDataSource.class);
67 // if (annotation == null) {
68 // return;
69 // }
70 // }
71
72 String dataSourceName = annotation.value();
73 DynamicDataSource.setDataSourceKey(dataSourceName);
74 System.out.println("切到" + dataSourceName + "数据库");
75 }
76 @After(value="pointcut()")
77 public void afterOpt(){
78 DynamicDataSource.toDefault();
79 System.out.println("切回默认数据库");
80 }
81
82 @Override
83 public int getOrder() {
84 return 1;
85 }
86 }

spring动态切换数据源(一)的更多相关文章

  1. Spring动态切换数据源及事务

    前段时间花了几天来解决公司框架ssm上事务问题.如果不动态切换数据源话,直接使用spring的事务配置,是完全没有问题的.由于框架用于各个项目的快速搭建,少去配置各个数据源配置xml文件等.采用了动态 ...

  2. Spring动态切换数据源

    11 //定义数据源枚举public enum DataSourceKey { master, slave, } 22 /** * 数据源路由 */ @Slf4j public class Dynam ...

  3. Spring AOP动态切换数据源

    现在稍微复杂一点的项目,一个数据库也可能搞不定,可能还涉及分布式事务什么的,不过由于现在我只是做一个接口集成的项目,所以分布式就先不用了,用Spring AOP来达到切换数据源,查询不同的数据库就可以 ...

  4. Spring + Mybatis 项目实现动态切换数据源

    项目背景:项目开发中数据库使用了读写分离,所有查询语句走从库,除此之外走主库. 最简单的办法其实就是建两个包,把之前数据源那一套配置copy一份,指向另外的包,但是这样扩展很有限,所有采用下面的办法. ...

  5. Spring+Mybatis动态切换数据源

    功能需求是公司要做一个大的运营平台: 1.运营平台有自身的数据库,维护用户.角色.菜单.部分以及权限等基本功能. 2.运营平台还需要提供其他不同服务(服务A,服务B)的后台运营,服务A.服务B的数据库 ...

  6. Spring动态切换多数据源事务开启后,动态数据源切换失效解决方案

    关于某操作中开启事务后,动态切换数据源机制失效的问题,暂时想到一个取巧的方法,在Spring声明式事务配置中,可对不改变数据库数据的方法采用不支持事务的配置,如下: 对单纯查询数据的操作设置为不支持事 ...

  7. Spring Boot 如何动态切换数据源

    本章是一个完整的 Spring Boot 动态数据源切换示例,例如主数据库使用 lionsea 从数据库 lionsea_slave1.lionsea_slave2.只需要在对应的代码上使用 Data ...

  8. 在使用 Spring Boot 和 MyBatis 动态切换数据源时遇到的问题以及解决方法

    相关项目地址:https://github.com/helloworlde/SpringBoot-DynamicDataSource 1. org.apache.ibatis.binding.Bind ...

  9. Spring学习总结(16)——Spring AOP实现执行数据库操作前根据业务来动态切换数据源

    深刻讨论为什么要读写分离? 为了服务器承载更多的用户?提升了网站的响应速度?分摊数据库服务器的压力?就是为了双机热备又不想浪费备份服务器?上面这些回答,我认为都不是错误的,但也都不是完全正确的.「读写 ...

随机推荐

  1. VUE SpringCloud 跨域资源共享 CORS 详解

    VUE  SpringCloud 跨域资源共享 CORS 详解 作者:  张艳涛 日期: 2020年7月28日 本篇文章主要参考:阮一峰的网络日志 » 首页 » 档案 --跨域资源共享 CORS 详解 ...

  2. java正则匹配字符串例子

    import java.util.regex.Matcher;import java.util.regex.Pattern; public class sss { public static void ...

  3. Django JSONField/HStoreField SQL注入漏洞(CVE-2019-14234)

    复现 访问http://192.168.49.2:8000/admin 输入用户名admin ,密码a123123123 然后构造URL进行查询,payload: http://192.168.49. ...

  4. Adaptive AUTOSAR 学习笔记 12 - 通信管理

    本系列学习笔记基于 AUTOSAR Adaptive Platform 官方文档 R20-11 版本 AUTOSAR_EXP_PlatformDesign.pdf 缩写 CM:Communicatio ...

  5. CCS box-flex属性

    box-flex==按比例分配父标签的宽度or高度空间 1.非固定分配 eg.一块地总150平方分配给三孩子,按照2:1:1分 #老大 { 房子-分配: 2; } = 75平 #老二 { 房子-分配: ...

  6. Windows协议 LDAP篇 - Actite Directory

    LDAP简介 先说下ldap,轻量目录访问协议.LDAP就是设计用来访问目录数据库的一个协议.也就是为了能访问目录数据库,ldap是其中一种协议 LDAP的基本模型 目录树:在一个目录服务系统中,整个 ...

  7. 如何将fidd上抓的包移到jmete中

    1.fiddler的安装配置就不说了, 网上有很多资源, 不会太难 2.使用fiddler抓包, 相信进来看这篇文章的博友都已经会使用fiddler抓包 3.打开jmeter, 添加>测试计划& ...

  8. 2021大厂Android面试高频100题最新汇总(附答案详解)

    前言 现在越来越多的人应聘工作时都得先刷个几十百来道题,不刷题感觉都过不了面试. 无论是前后端.移动开发,好像都得刷题,这么多人通过刷题过了面试,说明刷题对于找工作还是有帮助的. 不过这其中有一个问题 ...

  9. 自动化可用到的Java读取Excel文件指定的行列数据

    前言 在做接口自动化的时候,通常会遇到数据取用及存放的问题,一般有三种方式可选择 1.数据库存取 2.表格存取 3.项目配置文件存取 这里仅展示下第二种方式表格取数据的 示例 import org.a ...

  10. Azure 实践(1)- Azure Devops Server 安装

    1.Azure Devops介绍 Azure DevOps Server 2020 (之前的名称为TFS),作为微软Azure DevOps 的企业私有(on-premises)服务器,是一个为开发团 ...