01-basic
1. Summary
1. 架构演变之路
- 单一应用架构
- 当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化CRUD工作量的数据访问框架(ORM)是关键
- 垂直应用架构
- 当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键
- 分布式服务架构
- 当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键
- 流动计算架构
- 当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键
2. Spring Boot特点
BUILD ANYTHING WITH SPRING BOOT
Spring Boot is the starting point for building all Spring-based applications. Spring Boot is designed to get you up and running as quickly as possible, with minimal upfront configuration of Spring.
- Get started in seconds using Spring Initializr
- Build anything: REST APl, Websocket, web, streaming, tasks, and more
- Simplified security
- Rich support for SQL and NoSQL
- Embedded runtime support: Tomcat, Jetty, and Undertow
- Developer productivity tools such as LiveReload and Auto Restart
- Curated dependencies that just work
- Production-ready features such as tracing, metrics, and health status
- Works in your favorite IDE: Spring Tool Suite, IntelliJ IDEA, and NetBeans
3. new SpringBoot
@SpringBootApplication
@ComponentScan("com.listao") // 启动类位置乱放,需要@ComponentScan指定扫描路径
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
1. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.listao</groupId>
<artifactId>spring_boot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring_boot</name>
<description>spring_boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<?xml version="1.0" encoding="utf-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
<artifactId>spring-boot-starter-parent</artifactId>
<packaging>pom</packaging>
<name>Spring Boot Starter Parent</name>
<description>Parent pom providing dependency and plugin management for applications
built with Maven</description>
<url>https://projects.spring.io/spring-boot/#/spring-boot-starter-parent</url>
<?xml version="1.0" encoding="utf-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<packaging>pom</packaging>
<name>Spring Boot Dependencies</name>
<description>Spring Boot Dependencies</description>
<url>https://projects.spring.io/spring-boot/#</url>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
<scm>
<url>https://github.com/spring-projects/spring-boot</url>
</scm>
<properties>
<activemq.version>5.15.11</activemq.version>
<byte-buddy.version>1.10.4</byte-buddy.version>
<caffeine.version>2.8.0</caffeine.version>
<cassandra-driver.version>3.7.2</cassandra-driver.version>
<classmate.version>1.5.1</classmate.version>
<commons-codec.version>1.13</commons-codec.version>
<!-- ... -->
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>antlr</groupId>
<artifactId>antlr</artifactId>
<version>${antlr2.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- ... -->
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.johnzon</groupId>
<artifactId>johnzon-maven-plugin</artifactId>
<version>${johnzon.version}</version>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
</plugin>
<!-- ... -->
</plugins>
</pluginManagement>
</build>
</project>
2. starter
- 官网引入各种 Starters
3. banner
- patorjk官网
- bootschool官网
resources
目录下banner.txt
.__ .__ __
| | |__| _______/ |______ ____
| | | |/ ___/\ __\__ \ / _ \
| |_| |\___ \ | | / __ \( <_> )
|____/__/____ > |__| (____ /\____/
\/ \/
4. 配置文件
1. yml
- YAML不是一种标记语言(YAML Ain't Markup Language)的递归缩写。在开发这种语言时,"Yet Another Markup Language"(仍是一种标记语言),但为了强调这种语言以数据做为中心,而不是以标记语言为中心,而用反向缩略语重命名
- 是一种直观的能够被电脑识别的数据序列化格式,是一个可读性高并且容易被人类阅读,容易和脚本语言交互,用来表达资料序列的编程语言。类似于标准通用标记语言(XML)的子集,语法却比XML简单很多
- 基本原则
- 大小写敏感
- 使用缩进表示层级关系
- 禁止使用tab缩进,只能使用空格键
- 缩进长度没有限制,只要元素对齐就表示这些元素属于一个层级
- 使用#表示注释
- 字符串可以不用引号标注
- 多环境配置
spring:
profiles:
active: test
---
spring:
profiles: dev
server:
port: 8080
---
spring:
profiles: test
server:
port: 8081
- 参数的设置
person:
name: zhangsan
age: 12
sex: 男
desc: my name is ${person.name},my age is ${person.age}
person.name=zhangsan
person.age=12
person.gender=男
person.desc=my name is ${person.name},my age is ${person.age}
# application.yml
person:
last_name: zhangsan
age: 12
sex: 男
email: ooxx@qq.com
likes:
- book
- movie
- girl
@ConfigurationProperties(prefix = "person")
@Component
@Validated
@Data
public class Person {
@Value("${person.name}")
private String lastName;
@Value("#{1+2}")
private Integer age;
private String sex;
@Email
private String email;
private List<String> likes;
}
@SpringBootTest
class ApplicationTests {
@Autowired
Person person;
@Test
void contextLoads() {
System.out.println("person = " + person);
}
}
2. @ConfigProp & @Value
- 松散绑定即下划线和驼峰的转化
@ConfigurationProperties | @Value | |
---|---|---|
功能 | 批量注入配置文件中的属性 | 一个个指定 |
松散绑定(松散语法) | 支持 | 不支持 |
SpEL表达式 | 不支持 | 支持 |
JSR303数据校验 | 支持 | 不支持 |
复杂类型封装 | 支持 | 不支持 |
3. 配置文件优先级
/config
/
classpath:/config/
classpath:/
4. 多环境配置
- 在实际开发中,一套代码可能会被同时部署到开发、测试、生产等多个服务器中
- 每个环境中诸如数据库密码等这些个性化配置是避免不了的。虽然可以通过自动化运维部署的方式使用外部参数在服务启动时临时替换属性值,但运维成本增高
# application-dev.yaml
server:
port: 8080
# application-test.yaml
server:
port: 8081
# application.yaml
spring:
profiles:
active: test
5. annotation
- 元注解的作用:负责注解其他注解,java中定义了四个标准的meta-annotation类型,用来提供对其他annotation类型作说明
- 这些类型和它们所支持的类在
java.lang.annotation
包中@Target
:用来描述注解的使用范围(注解可以用在什么地方)@Retention
:表示需要在什么级别保存该注释信息,描述注解的生命周期- Source < Class < Runtime
@Documented
:说明该注解将被包含在javadoc中@Inherited
:说明子类可以继承父类中的该注解
1. 自定义注解
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnno {
String name() default "ooxx";
}
class Test {
@MyAnno(name = "hhhh")
private String name;
@MyAnno
public void ooxx() {
}
}
6. web
- SpringBoot非常适合用于开发web应用程序。可以使用嵌入式Tomcat、Jetty、Undertow来创建一个独立(self-contained)的HTTP服务器。大多数web应用程序使用
spring-boot-starter-web
模块来快速搭建和运行,也可以选择使用spring-boot-starter-webflux
模块来构建响应式(reactive)web应用程序 spring-boot-autoconfigure-2.2.2.RELEASE.jar
=>spring.factories
# 最重要,自动装配
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
# ...
2. integration
1. Servlet
package com.listao.spring_boot.servlet;
@WebServlet(name = "myServlet", urlPatterns = "/servlet")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("MyServlet ......");
super.doGet(req, resp);
}
}
@SpringBootApplication
@ServletComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
// 将自定义MyServlet添加到IOC,当配置了urlMappings后,MyServlet上的`@WebServlet`的`urlPatterns`不再生效
@Bean
public ServletRegistrationBean<MyServlet> getServletRegistrationBean() {
ServletRegistrationBean<MyServlet> bean = new ServletRegistrationBean<>(new MyServlet(), "/servletBean");
bean.setLoadOnStartup(1);
return bean;
}
}
2. Filter
package com.listao.spring_boot.filter;
@WebFilter(filterName = "MyFilter", urlPatterns = "/filter")
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("init......");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("filter......");
chain.doFilter(request, response);
}
@Override
public void destroy() {
System.out.println("destory......");
}
}
3. Listener
- Listener是Servlet规范定义的一种特殊类,用于监听
ServletContext, HttpSession, ServletRequest
等域对象的创建、销毁、属性变更事件。在事件发生前、后做一些必要的处理:- 统计在线人数、在线用户
- 系统启动时加载初始化信息
- 统计网站访问量
- 记录用户访问路径
package com.listao.spring_boot.listener;
public class MyHttpSessionListener implements HttpSessionListener {
public static int online = 0;
@Override
public void sessionCreated(HttpSessionEvent hse) {
System.out.println("创建session......");
online++;
}
@Override
public void sessionDestroyed(HttpSessionEvent hse) {
System.out.println("销毁session......");
}
}
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@ServletComponentScan
// @ComponentScan("com.listao")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
// 将自定义MyServlet添加到IOC,当配置了urlMappings之后,MyServlet自己的urlPatterns不再生效
@Bean
public ServletRegistrationBean<MyServlet> getServletRegistrationBean() {
ServletRegistrationBean<MyServlet> bean = new ServletRegistrationBean<>(new MyServlet(), "/servletBean");
bean.setLoadOnStartup(1);
return bean;
}
@Bean
public ServletListenerRegistrationBean listenerRegister() {
ServletListenerRegistrationBean slrb = new ServletListenerRegistrationBean();
slrb.setListener(new MyHttpSessionListener());
return slrb;
}
}
package com.listao.spring_boot.controller;
@Controller
public class MyController {
@RequestMapping("/session")
public String session(HttpSession httpSession) {
httpSession.setAttribute("k", "v");
return "session";
}
@RequestMapping("/online")
public String online() {
return "当前在线人数:" + MyHttpSessionListener.online + "人";
}
}
4. thymeleaf
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
显示消息为:<p th:text="${msg}"></p>
</body>
</html>
@RequestMapping("/thymeleaf1")
public String thymeleaf1(Model model) {
model.addAttribute("msg", "hello, thymeleaf");
return "01_html";
}
1. 静态资源加载路径
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
/**
* Whether to enable default resource handling.
*/
private boolean addMappings = true;
private final Chain chain = new Chain();
private final Cache cache = new Cache();
}
- 使用了MVC的
ResourceHttpRequestHandler
,可以添加自定义的WebMvcAutoConfigurationAdapter
并重写addResourceHandlers()
来修改此行为
package org.springframework.boot.autoconfigure.web.servlet;
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
// Defined as a nested config to ensure WebMvcConfigurer is not read when not
// on the classpath
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class);
private final ResourceProperties resourceProperties;
private final WebMvcProperties mvcProperties;
private final ListableBeanFactory beanFactory;
private final ObjectProvider<HttpMessageConverters> messageConvertersProvider;
final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
}
}
2. webjars
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>jquery</artifactId>
<version>3.7.1</version>
</dependency>
3. 首页配置
package org.springframework.boot.autoconfigure.web.servlet;
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
/**
* Configuration equivalent to {@code @EnableWebMvc}.
*/
@Configuration(proxyBeanMethods = false)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
private final ResourceProperties resourceProperties;
private final WebMvcProperties mvcProperties;
private final ListableBeanFactory beanFactory;
private final WebMvcRegistrations mvcRegistrations;
private ResourceLoader resourceLoader;
public EnableWebMvcConfiguration(ResourceProperties resourceProperties,
ObjectProvider<WebMvcProperties> mvcPropertiesProvider,
ObjectProvider<WebMvcRegistrations> mvcRegistrationsProvider, ListableBeanFactory beanFactory) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcPropertiesProvider.getIfAvailable();
this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique();
this.beanFactory = beanFactory;
}
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
return welcomePageHandlerMapping;
}
}
}
5. SpringMVC
SpringMVC框架是一个mvc的web框架,允许创建@Controller
和@RestController
的bean来处理传入的HTTP请求,控制器中的方法通过@RequestMapping
注解映射到HTTP。SpringBoot提供了适用于大多数SpringMVC应用的自动配置
- 引入
ContentNegotiatingViewResolver
和BeanNameViewResolver
的bean视图解析器 - 支持服务静态资源,包括对
WebJar
的支持 - 自动注册
Converter
(网页传入的数据封装成对象,完成数据类型的转化)、GenericConverter
、Formatter
的bean(将日期转换成规定的格式) - 支持
HttpMessageConverter
,用来转换http请求和响应 - 自动注册
MessageCodesResolver
,定义错误代码生成规则 - 支持静态
index.html
- 支持自定义
Favicon
- 自动使用
ConfigurableWebBindingInitializer
的bean,将请求树绑定到JavaBean中
package org.springframework.boot.autoconfigure.web.servlet;
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
// Defined as a nested config to ensure WebMvcConfigurer is not read when not
// on the classpath
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class);
private final ResourceProperties resourceProperties;
private final WebMvcProperties mvcProperties;
private final ListableBeanFactory beanFactory;
private final ObjectProvider<HttpMessageConverters> messageConvertersProvider;
final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
}
@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
// ContentNegotiatingViewResolver uses all the other view resolvers to locate
// a view so it should have a high precedence
resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
return resolver;
}
}
}
1. ContentNegotiatingViewResolver
package org.springframework.web.servlet.view;
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered, InitializingBean {
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
// 获得 MediaType 数组
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) {
// 1.. 获得匹配的 View 数组
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
// 筛选最匹配的 View 对象
View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
// 如果筛选成功,则返回
if (bestView != null) {
return bestView;
}
}
String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
" given " + requestedMediaTypes.toString() : "";
// 如果匹配不到 View 对象,则根据 useNotAcceptableStatusCode ,返回 NOT_ACCEPTABLE_VIEW 或 null
if (this.useNotAcceptableStatusCode) {
if (logger.isDebugEnabled()) {
logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
}
return NOT_ACCEPTABLE_VIEW;
}
else {
logger.debug("View remains unresolved" + mediaTypeInfo);
return null;
}
}
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
throws Exception {
// 创建 View 数组
List<View> candidateViews = new ArrayList<>();
// 来源一,通过 viewResolvers 解析出 View 数组结果,添加到 candidateViews 中
if (this.viewResolvers != null) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
// 遍历 viewResolvers 数组
for (ViewResolver viewResolver : this.viewResolvers) {
// 1... 情况一,获得 View 对象,添加到 candidateViews 中
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
// 情况二,带有拓展后缀的方式,获得 View 对象,添加到 candidateViews 中
for (MediaType requestedMediaType : requestedMediaTypes) {
// 获得 MediaType 对应的拓展后缀的数组(默认情况下未配置)
List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
// 遍历拓展后缀的数组
for (String extension : extensions) {
// 带有拓展后缀的方式,获得 View 对象,添加到 candidateViews 中
String viewNameWithExtension = viewName + '.' + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
if (view != null) {
candidateViews.add(view);
}
}
}
}
}
// 来源二,添加 defaultViews 到 candidateViews 中
if (!CollectionUtils.isEmpty(this.defaultViews)) {
candidateViews.addAll(this.defaultViews);
}
return candidateViews;
}
@Override
protected void initServletContext(ServletContext servletContext) {
// 扫描所有 ViewResolver 的 Bean 们
Collection<ViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
// 情况一,如果 viewResolvers 为空,则将 matchingBeans 作为 viewResolvers
// BeanNameViewResolver、ThymeleafViewResolver、ViewResolverComposite、InternalResourceViewResolver
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList<>(matchingBeans.size());
for (ViewResolver viewResolver : matchingBeans) {
if (this != viewResolver) {
// 排除自己
this.viewResolvers.add(viewResolver);
}
}
}
// 情况二,如果 viewResolvers 非空,则和 matchingBeans 进行比对,判断哪些未进行初始化,进行初始化
else {
for (int i = 0; i < this.viewResolvers.size(); i++) {
ViewResolver vr = this.viewResolvers.get(i);
// 已存在在 matchingBeans 中,说明已经初始化,则直接 continue
if (matchingBeans.contains(vr)) {
continue;
}
// 不存在在 matchingBeans 中,说明还未初始化,则进行初始化
String name = vr.getClass().getName() + i;
obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
}
}
// 排序 viewResolvers 数组
AnnotationAwareOrderComparator.sort(this.viewResolvers);
// 设置 cnmFactoryBean 的 servletContext 属性
this.cnmFactoryBean.setServletContext(servletContext);
}
}
- 上面代码分析,SpringBoot在IOC中去找视图解析器。因此,给容器自定义添加视图解析器
// 自定义视图解析器
@Bean
public ViewResolver myViewResolver(){
return new MyViewResolver();
}
// 一个静态内部类,视图解析器需要实现ViewResolver接口
private static class MyViewResolver implements ViewResolver{
@Override
public View resolveViewName(String s, Locale locale) throws Exception {
return null;
}
}
- 想保留SpringBoot的MVC功能,并且需要添加其他MVC配置(interceptor、formatter、视图控制器等),可以添加自定义的
WebMvcConfigurerAdapter
类型的@Configuration
类,但不能带@EnableWebMvc
注解 - 如果想自定义
RequestMappingHandlerMapping
、RequestMappingHandlerAdapter
或者ExceptionHandlerExceptionResolver
实例,可以声明一个WebMvcRegistrationsAdapter
实例来提供这些组件 - 想完全掌控SpringMVC,可以添加自定义注解了
@EnableWebMvc
的@Configuration
配置类
package com.listao.spring_boot.config;
@Configuration
class MyConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/ooxx").setViewName("01_html");
registry.addViewController("/login.html").setViewName("login");
}
}
6. JDBC
- SpringFramework为SQL数据库提供了广泛的支持。从直接使用
JdbcTemplate
进行JDBC访问到完全的对象关系映射(Object Relational Mapping)技术。Spring_Data提供了更多级别的功能,直接从接口创建Repository实现,并约定使用方法名生成查询
<!-- springboot_data -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
spring:
messages:
basename: i18n/login
datasource:
username: root
password: ******
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://ip:port/mca?serverTimezone=UTC&useUnicode=true@characterEncoding=utf-8
@SpringBootTest
class DBTests {
@Autowired
DataSource dataSource;
@Test
void jdbc() throws Exception {
System.out.println("dataSource = " + dataSource.getClass());
Connection connection = dataSource.getConnection();
System.out.println("connection = " + connection);
connection.close();
}
}
dataSource = class com.zaxxer.hikari.HikariDataSource
2023-12-09 18:12:35.436 INFO 6918 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2023-12-09 18:12:35.438 WARN 6918 --- [ main] com.zaxxer.hikari.util.DriverDataSource : Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation.
2023-12-09 18:12:36.060 INFO 6918 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
connection = HikariProxyConnection@1656197285 wrapping com.mysql.cj.jdbc.ConnectionImpl@7c52fc81
1. CRUD
- 有了数据源
com.zaxxer.hikari.HikariDataSource
,然后拿到数据库连接java.sql.Connection
,使用连接和原生的JDBC语句来操作数据库 - 即使不使用第三方数据库操作框架(eg:MyBatis等),Spring本身也对原生的JDBC做了轻量级的封装
- 即
org.springframework.jdbc.core.JdbcTemplate
。CRUD方法都在JdbcTemplate
中
- 即
- SpringBoot不仅提供了默认的数据源,同时默认已经配置好了
JdbcTemplate
放在了容器中,程序员只需自己注入即可使用 JdbcTemplate
的自动配置原理是依赖org.springframework.boot.autoconfigure.jdbc
包下的org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
类
@RestController
public class JdbcController {
@Autowired
JdbcTemplate jdbcTemplate;
@GetMapping("/selAll")
public List<Map<String, Object>> selAll() {
String sql = "select * from tmp";
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
return maps;
}
@GetMapping("/addTmp")
public String addTmp() {
String sql = "insert into tmp(id, name) values(1111, 'zhangsan')";
jdbcTemplate.update(sql);
return "success";
}
@GetMapping("/updTmp/{id}")
public String updTmp(@PathVariable("id") Integer id) {
String sql = "update tmp set name=? where id = " + id;
String name = "list";
jdbcTemplate.update(sql, name);
return "update success";
}
@GetMapping("/delTmp/{id}")
public String delTmp(@PathVariable("id") Integer id) {
String sql = "delete from tmp where id = " + id;
jdbcTemplate.update(sql);
return "delete success";
}
}
7. DruidDataSource
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
spring:
messages:
basename: i18n/login
datasource:
username: root
password: ******
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://ip:port/mca?serverTimezone=UTC&useUnicode=true@characterEncoding=utf-8
type: com.alibaba.druid.pool.DruidDataSource
2023-12-15 15:41:04.631 INFO 68604 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
connection = com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@21e45a6f
1. SC
package org.springframework.boot.autoconfigure.jdbc;
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
public DataSourceAutoConfiguration() {
}
static class EmbeddedDatabaseCondition extends SpringBootCondition {
private final SpringBootCondition pooledCondition = new PooledDataSourceCondition();
EmbeddedDatabaseCondition() {
}
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("EmbeddedDataSource", new Object[0]);
if (this.anyMatches(context, metadata, new Condition[]{this.pooledCondition})) {
return ConditionOutcome.noMatch(message.foundExactly("supported pooled data source"));
} else {
EmbeddedDatabaseType type = EmbeddedDatabaseConnection.get(context.getClassLoader()).getType();
return type == null ? ConditionOutcome.noMatch(message.didNotFind("embedded database").atAll()) : ConditionOutcome.match(message.found("embedded database").items(new Object[]{type}));
}
}
}
static class PooledDataSourceAvailableCondition extends SpringBootCondition {
PooledDataSourceAvailableCondition() {
}
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("PooledDataSource", new Object[0]);
return DataSourceBuilder.findType(context.getClassLoader()) != null ? ConditionOutcome.match(message.foundExactly("supported DataSource")) : ConditionOutcome.noMatch(message.didNotFind("supported DataSource").atAll());
}
}
static class PooledDataSourceCondition extends AnyNestedCondition {
PooledDataSourceCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@Conditional({PooledDataSourceAvailableCondition.class})
static class PooledDataSourceAvailable {
PooledDataSourceAvailable() {
}
}
@ConditionalOnProperty(
prefix = "spring.datasource",
name = {"type"}
)
static class ExplicitType {
ExplicitType() {
}
}
}
@Configuration(
proxyBeanMethods = false
)
@Conditional({PooledDataSourceCondition.class})
@ConditionalOnMissingBean({DataSource.class, XADataSource.class})
@Import({DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class})
protected static class PooledDataSourceConfiguration {
protected PooledDataSourceConfiguration() {
}
}
@Configuration(
proxyBeanMethods = false
)
@Conditional({EmbeddedDatabaseCondition.class})
@ConditionalOnMissingBean({DataSource.class, XADataSource.class})
@Import({EmbeddedDataSourceConfiguration.class})
protected static class EmbeddedDatabaseConfiguration {
protected EmbeddedDatabaseConfiguration() {
}
}
}
2. config
spring:
datasource:
username: root
password: ******
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://ip:port/mca?serverTimezone=UTC&useUnicode=true@characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL
type: com.alibaba.druid.pool.DruidDataSource
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
package com.listao.spring_boot.config;
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource() {
return new DruidDataSource();
}
}
@Test
void druid() throws Exception{
System.out.println(dataSource.getClass());
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
System.out.println("druidDataSource = " + druidDataSource);
System.out.println("InitialSize() = " + druidDataSource.getInitialSize());
System.out.println("MaxActive() = " + druidDataSource.getMaxActive());
Connection connection = dataSource.getConnection();
System.out.println(connection);
connection.close();
}
class com.alibaba.druid.pool.DruidDataSource
druidDataSource = {
CreateTime:"2023-12-15 16:13:39",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}
[
]
InitialSize() = 5
MaxActive() = 20
package org.springframework.boot.autoconfigure.http;
@ConfigurationProperties(
prefix = "spring.http"
)
public class HttpProperties {
private boolean logRequestDetails;
private final Encoding encoding = new Encoding();
}
package org.springframework.boot.autoconfigure.jdbc;
@ConfigurationProperties(
prefix = "spring.datasource"
)
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
private ClassLoader classLoader;
private String name;
private boolean generateUniqueName;
private Class<? extends DataSource> type;
private String driverClassName;
private String url;
private String username;
private String password;
private String jndiName;
private DataSourceInitializationMode initializationMode;
private String platform;
private List<String> schema;
private String schemaUsername;
private String schemaPassword;
private List<String> data;
private String dataUsername;
private String dataPassword;
private boolean continueOnError;
private String separator;
private Charset sqlScriptEncoding;
private EmbeddedDatabaseConnection embeddedDatabaseConnection;
private Xa xa;
private String uniqueName;
}
package com.alibaba.druid.pool;
public class DruidDataSource extends DruidAbstractDataSource
implements DruidDataSourceMBean, ManagedDataSource, Referenceable, Closeable, Cloneable, ConnectionPoolDataSource, MBeanRegistration {
private static final Log LOG = LogFactory.getLog(DruidDataSource.class);
private static final long serialVersionUID = 1L;
private volatile long recycleErrorCount;
private long connectCount;
private long closeCount;
private volatile long connectErrorCount;
private long recycleCount;
private long removeAbandonedCount;
private long notEmptyWaitCount;
private long notEmptySignalCount;
private long notEmptyWaitNanos;
private int keepAliveCheckCount;
private int activePeak;
public void configFromPropety(Properties properties) {
{
String property = properties.getProperty("druid.name");
if (property != null) {
this.setName(property);
}
}
{
String property = properties.getProperty("druid.url");
if (property != null) {
this.setUrl(property);
}
}
{
String property = properties.getProperty("druid.username");
if (property != null) {
this.setUsername(property);
}
}
{
String property = properties.getProperty("druid.password");
if (property != null) {
this.setPassword(property);
}
}
// ...
{
String property = properties.getProperty("druid.initialSize");
if (property != null && property.length() > 0) {
try {
int value = Integer.parseInt(property);
this.setInitialSize(value);
} catch (NumberFormatException e) {
LOG.error("illegal property 'druid.initialSize'", e);
}
}
}
// ...
{
String property = properties.getProperty("druid.maxActive");
if (property != null && property.length() > 0) {
try {
int value = Integer.parseInt(property);
this.setMaxActive(value);
} catch (NumberFormatException e) {
LOG.error("illegal property 'druid.maxActive'", e);
}
}
}
// ...
}
}
public abstract class DruidAbstractDataSource extends WrapperAdapter implements DruidAbstractDataSourceMBean, DataSource, DataSourceProxy, Serializable {
private static final long serialVersionUID = 1L;
private final static Log LOG = LogFactory.getLog(DruidAbstractDataSource.class);
public final static int DEFAULT_INITIAL_SIZE = 0;
public final static int DEFAULT_MAX_ACTIVE_SIZE = 8;
public final static int DEFAULT_MAX_IDLE = 8;
public final static int DEFAULT_MIN_IDLE = 0;
public final static int DEFAULT_MAX_WAIT = -1;
public final static String DEFAULT_VALIDATION_QUERY = null; //
public final static boolean DEFAULT_TEST_ON_BORROW = false;
public final static boolean DEFAULT_TEST_ON_RETURN = false;
public final static boolean DEFAULT_WHILE_IDLE = true;
public static final long DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS = 60 * 1000L;
public static final long DEFAULT_TIME_BETWEEN_CONNECT_ERROR_MILLIS = 500;
public static final int DEFAULT_NUM_TESTS_PER_EVICTION_RUN = 3;
public static final long DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS = 1000L * 60L * 30L;
public static final long DEFAULT_MAX_EVICTABLE_IDLE_TIME_MILLIS = 1000L * 60L * 60L * 7;
public static final long DEFAULT_PHY_TIMEOUT_MILLIS = -1;
protected volatile boolean defaultAutoCommit = true;
protected volatile Boolean defaultReadOnly;
protected volatile Integer defaultTransactionIsolation;
protected volatile String defaultCatalog = null;
protected String name;
protected volatile String username;
protected volatile String password;
protected volatile String jdbcUrl;
protected volatile String driverClass;
protected volatile int initialSize = DEFAULT_INITIAL_SIZE;
protected volatile int maxActive = DEFAULT_MAX_ACTIVE_SIZE;
protected volatile int minIdle = DEFAULT_MIN_IDLE;
protected volatile int maxIdle = DEFAULT_MAX_IDLE;
protected volatile long maxWait = DEFAULT_MAX_WAIT;
protected int notFullTimeoutRetryCount = 0;
// ...
}
3. 监控
- Druid数据源还具有监控的功能,并提供了一个web界面方便用户进行查看
- 加入log4j的日志依赖
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
spring:
datasource:
username: root
password: ******
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://ip:port/mca?serverTimezone=UTC&useUnicode=true@characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL
type: com.alibaba.druid.pool.DruidDataSource
# druid数据源专有配置。SpringBoot默认是不注入这些属性值,需要自己绑定
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# 配置监控统计拦截的filters
# stat:监控统计、log4j:日志记录、wall:防御sql注入
# 报错:`java.lang.ClassNotFoundException: org.apache.log4j.Priority`则导入log4j依赖
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
package com.listao.spring_boot.config;
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource() {
return new DruidDataSource();
}
@Bean
public ServletRegistrationBean druidServletRegistrationBean() {
ServletRegistrationBean<Servlet> servletRegistrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
Map<String, String> initParams = new HashMap<>();
initParams.put("loginUsername", "admin");
initParams.put("loginPassword", "admin");
// 表示只有本机可以访问
// initParams.put("allow", "localhost");
// deny:Druid 后台拒绝谁访问
// initParams.put("msb", "192.168.1.20"); // 表示禁止此ip访问
// 为空或者为null时,表示允许所有访问
initParams.put("allow", "");
servletRegistrationBean.setInitParameters(initParams);
return servletRegistrationBean;
}
// 配置Druid监控之web监控的filter
// WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计
@Bean
public FilterRegistrationBean webStatFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());
// exclusions:设置哪些请求进行过滤排除掉,从而不进行统计
Map<String, String> initParams = new HashMap<>();
initParams.put("exclusions", "*.js,*.css,/druid/*");
bean.setInitParameters(initParams);
// '/*':表示过滤所有请求
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
}
8. 多数据源配置
1. DataSourceAspect
package com.listao.spring_boot.multiDB;
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.listao.spring_boot.multiDB.DataSource)")
public void dsPointCut() {
}
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
// 1.
DataSource dataSource = method.getAnnotation(DataSource.class);
if (dataSource != null) {
// 2.
DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
}
try {
return point.proceed();
} finally {
// 销毁数据源,在执行方法之后
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
}
2. @DataSource
- 设置拦截数据源的注解,可以设置在具体的类上,或者在具体的方法上
package com.listao.spring_boot.multiDB;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
/**
* 切换数据源
*/
DataSourceType value() default DataSourceType.REMOTE;
}
package com.listao.spring_boot.multiDB;
public enum DataSourceType {
REMOTE,
LOCAL
}
3. DynamicDataSourceContextHolder
package com.listao.spring_boot.multiDB;
public class DynamicDataSourceContextHolder {
/**
* 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
System.out.printf("切换到{%s}数据源", dataSourceType);
System.out.println();
CONTEXT_HOLDER.set(dataSourceType);
}
/**
* 获取数据源变量
*/
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
/**
* 清空数据源变量
*/
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
4. DataSourceConfig
package com.listao.spring_boot.multiDB;
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.remote")
public DataSource remoteDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.local")
public DataSource localDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dataSource(DataSource remoteDataSource, DataSource localDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.REMOTE.name(), remoteDataSource);
targetDataSources.put(DataSourceType.LOCAL.name(), localDataSource);
// (默认数据源, 数据源组)
return new DynamicDataSource(remoteDataSource, targetDataSources);
}
}
spring:
datasource:
local:
username: root
password: ******
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://ip:port/mca?serverTimezone=UTC&useUnicode=true@characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL
remote:
username: root
password: ******
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://ip:port/kgbase?serverTimezone=UTC&useUnicode=true@characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL
5. DynamicDataSource
- 动态切换数据源,创建一个
AbstractRoutingDataSource
的子类,重写determineCurrentLookupKey()
package com.listao.spring_boot.multiDB;
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* @param defaultTargetDataSource 默认目标数据源
* @param targetDataSources 切换的数据源
*/
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
// afterPropertiesSet()将targetDataSources的属性写入resolvedDataSources中的
super.afterPropertiesSet();
}
/**
* 根据Key获取数据源的信息
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
1. AbstractRoutingDataSource
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
// 1.
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
protected Object resolveSpecifiedLookupKey(Object lookupKey) {
return lookupKey;
}
@Override
public Connection getConnection() throws SQLException {
// 1..
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
/**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 1... DynamicDataSource
Object lookupKey = determineCurrentLookupKey();
// 2..
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
}
6. 循环依赖bug
- 启动项目过程会发生循环依赖,直接修改启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@ServletComponentScan
@ComponentScan("com.listao")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
9. mybatis
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Tmp {
private String id;
private String name;
private Date updtime;
}
@Mapper
@Repository
public interface TmpMapper {
List<Tmp> selAll();
Tmp selTmpById(Integer empno);
Integer addTmp(Tmp emp);
Integer updTmp(Tmp emp);
Integer delTmp(Integer empno);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.listao.spring_boot.mapper.TmpMapper">
<select id="selAll" resultType="Tmp">
select *
from tmp
</select>
<select id="selTmpById" resultType="Tmp">
select *
from tmp
where id = #{id}
</select>
<insert id="addTmp" parameterType="Tmp">
insert into tmp (id, name)
values (#{id}, #{name})
</insert>
<update id="updTmp" parameterType="Tmp">
update tmp
set name=#{name}
where id = #{id}
</update>
<delete id="delTmp" parameterType="int">
delete
from tmp
where id = #{id}
</delete>
</mapper>
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.listao.spring_boot.entity
@SpringBootTest
class MybatisTests {
@Autowired
TmpMapper tmpMapper;
@Test
void selAll() {
List<Tmp> tmps = tmpMapper.selAll();
System.out.println("tmps = " + tmps);
}
// 根据id选择用户
@Test
void selTmpById() {
Tmp tmp = tmpMapper.selTmpById(1234);
System.out.println("tmp = " + tmp);
}
// 添加一个用户
@Test
void addEmp() {
Integer row = tmpMapper.addTmp(new Tmp("1", "ooxx", new Date()));
System.out.println("row = " + row);
}
// 修改一个用户
@Test
void updEmp() {
Integer upd = tmpMapper.updTmp(new Tmp("1", "upd", new Date()));
System.out.println("upd = " + upd);
}
// 根据id删除用户
@Test
void deleteEmp() {
Integer row = tmpMapper.delTmp(1234);
System.out.println("row = " + row);
}
}