Spring应用
Spring介绍
介绍
Spring是一款开源的轻量级微服务开发框架,体系完善,可以快速实现业务开发和功能扩展,代码移植性强,只要服務器支持servlet就可以部署。
核心功能
IOC、AOP、事务、持久层、MVC、SpringBoot、注解
以下基于核心功能描述
maven引入
spring-context由spring-core、spring-bean和spring-aop等核心组件组成,core负责容器管理、bean负责bean的加载、定义和生命周期、aop负责增强扩展
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>
IOC——概述
IOC是一种设计原则,是指通过容器管理应用实例,调用方只需要向容器申请实例调用即可,不再需要对象本身来管理,提高了扩展性和复用性,做到了解耦,同时降低垃圾的产生。
IOC——IOC 容器
IOC——scope
1、scope类型
在Spring 2.0之前,有singleton和prototype两种;
在Spring 2.0之后,为支持web应用的ApplicationContext,增强另外三种:request,session和global session类型,它们只实用于web程序,通常是和XmlWebApplicationContext共同使用。
1、singleton
此取值时表明容器中创建时只存在一个实例,所有引用此bean都是单一实例。如同每个国家都有一个总统,国家的所有人共用此总统,而这个国家就是一个spring容器,总统就是spring创建的类的bean,国家中的人就是其它调用者,总统是一个表明其在spring中的scope为singleton,也就是单例模型。
此外,singleton类型的bean定义从容器启动到第一次被请求而实例化开始,只要容器不销毁或退出,该类型的bean的单一实例就会一直存活,典型单例模式,如同servlet在web容器中的生命周期。
单例情况下: 不能使用共享成员变量 , 在并发情况下本来是true , 但另一线程进来 , 变为false , 期望结果是true , 并发会导致数据不准确
2、prototype
请求方spring容器在进行输出prototype的bean对象时,会每次都重新生成一个新的对象,虽然这种类型的对象的实例化以及属性设置等工作都是由容器负责的,但是只要准备完毕,并且对象实例返回给请求方之后,容器就不在拥有当前对象的引用,请求方需要自己负责当前对象后继生命周期的管理工作,包括该对象的销毁。也就是说,容器每次返回请求方该对象的一个新的实例之后,就由这个对象“自生自灭”,最典型的体现就是spring与struts2进行整合时,要把action的scope改为prototype。
属性值为prototype时 , 并发时线程是安全的
如同分苹果,将苹果的bean的scope属性声明为prototype,在每个人领取苹果的时候,我们都是发一个新的苹果给他,发完之后,别人爱怎么吃就怎么吃,爱什么时候吃什么时候吃,但是注意吃完要把苹果核扔到垃圾箱!对于那些不能共享使用的对象类型,应该将其定义的scope设为prototype。
3、request
再次说明request,session和global session类型只实用于web程序,通常是和XmlWebApplicationContext共同使用。
<bean id ="requestPrecessor" class="...RequestPrecessor" scope="request" />
Spring容器,即XmlWebApplicationContext 会为每个HTTP请求创建一个全新的RequestPrecessor对象,当请求结束后,该对象的生命周期即告结束,如同java web中request的生命周期。当同时有100个HTTP请求进来的时候,容器会分别针对这10个请求创建10个全新的RequestPrecessor实例,且他们相互之间互不干扰,简单来讲,request可以看做prototype的一种特例,除了场景更加具体之外,语意上差不多。
4、session
对于web应用来说,放到session中最普遍的就是用户的登录信息,对于这种放到session中的信息,我们可以使用如下形式的制定scope为session:
<bean id ="userPreferences" class="...UserPreferences" scope="session" />
Spring容器会为每个独立的session创建属于自己的全新的UserPreferences实例,比request scope的bean会存活更长的时间,其他的方面没区别,如果java web中session的生命周期。
5、global session
<bean id ="userPreferences" class="...UserPreferences" scope="globalsession" />
global session只有应用在基于porlet的web应用程序中才有意义,它映射到porlet的global范围的session,如果普通的servlet的web 应用中使用了这个scope,容器会把它作为普通的session的scope对待。
总结
| 取值范围 | 说明 |
|---|---|
| singleton | 单例,并发情况线程不安全 生命周期: 随着ioc创建而创建 ioc容器会一直拥有此bean的引用以保证不被回收 随着ioc卸载而消亡 |
| prototype | 多例,影响效率,并发情况线程安全 生命周期: 随着线程调用getBean()而创建 ioc容器不会拥有此bean的引用,靠gc回收 |
| request | web环境下,为每个requset创建独立bean,生命周期同prototype |
| session | web环境下,为每个session创建独立bean,即存在于session域中,存活时间比request有所提升,生命周期同prototype |
| global session | web环境下,应用在portlet环境,如果没有portlet环境会被当作session,生命周期同prototype |
2、scope配置
1、xml方式
进行bean的配置时,指定scope。

2、注解方式
前提为配置spring为注解配置。

IOC——最基本的初始化、销毁方法
<bean id="person" name="n1,n2,n3" class="com.baizhiedu.basic.Person" init-method="init" destroy-method="destroy"></bean>

补充:bean实例化配置说明
| 特性 | id属性 | name属性 |
|---|---|---|
| 唯一性要求 | 必须唯一 | 可以重复(不推荐) |
| 特殊字符 | 不能包含特殊字符 | 可以包含特殊字符 |
| 多个名称 | 只能指定一个 | 可以指定多个,用逗号/分号/空格分隔 |
| XML规范 | 符合XML ID规范 | 更灵活 |
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Person p1 = (Person) ctx.getBean("person");
Person p2 = (Person) ctx.getBean("n1");
Person p3 = (Person) ctx.getBean("n2");
Person p4 = (Person) ctx.getBean("n3");
IOC——工厂创建
1、无参构造创建
之前的bean定义均是无参构造的应用
2、静态工厂方法创建
此时bean里加上一个静态方法 getFighterInstance1,一个实例方法getFighterInstance2
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Fighter {
public static Fighter getFighterInstance1(){
return new Fighter();
}
public Fighter getFighterInstance2(){
return new Fighter(10,"J-20","A");
}
// 服役年限
private int age;
// 战机名称
private String name;
// 战机型号
private String model;
}
然后applicationContext.xml中通过factory-method来创建
<bean id="fighter1" class="com.baizhiedu.basic.Fighter" factory-method="getFighterInstance1"></bean>
3、成员方法创建
这个也简单,是指用一个bean实例的方法来创建另一个bean
//基于上面的fighter1来执行
<bean id="fighter2" class="com.baizhiedu.basic.Fighter" factory-bean="fighter1" factory-method="getFighterInstance2"></bean>
【注:】
利用方法来创建实例是非常常见的,如DriverManager创建connector实例
IOC——DI(Dependency Injection)
1、介绍
在Spring框架中,依赖注入(Dependency Injection,DI)是控制反转(IoC)的一环,即从IoC容器中获取实例B并注入到实例A指定属性的过程,依赖注入是一种思想,具体实现该思想的是自动装配、xml装配或者注解装配。
2、属性填充
在Spring中,属性填充是实例创建后的准备流程之一,常见的属性填充有setter注入和字段注入,属性填充调用依赖注入时同时提供其注入方式。
例如自动装配、xml装配、注解装配等依赖注入方案,会根据属性填充提供的注入方式注入属性。
3、注入方法
示例
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FighterService {
private Fighter fighter;
public Fighter create(){
fighter.setAge(0);
fighter.setModel("C");
return fighter;
}
}
1、构造器注入
<!-- 实例方法创建实例-->
<bean id="fighter2" class="com.baizhiedu.basic.pojo.Fighter" factory-bean="fighter1" factory-method="getFighterInstance2"></bean>
<!-- 构造器注入-->
<bean id="fighterService1" class="com.baizhiedu.basic.service.FighterService" >
<!-- 标记构造参数和注入的value-->
<constructor-arg name="fighter" ref="fighter2"></constructor-arg>
<constructor-arg name="otherArg" ref="otherRef"></constructor-arg>
...
</bean>
2、set注入
- 普通数据类型
- 引用数据类型
- 数组数据类型
- 集合数据类型
-
<list> 注入,允许有相同的值。
-
<set> 注入,不允许有相同的值。
-
<map> 注入,键和值都可以为任意类型。
-
<props>注入,键和值都只能为String类型。
-
<!-- 1、引用类型注入:普通方式-->
<bean id="fighterService2" class="com.baizhiedu.basic.service.FighterService" >
<property name="fighter" ref="fighter2"></property>
</bean>
<!-- 2、添加p属性依赖注入-->
<bean id="fighterService3" class="com.baizhiedu.basic.service.FighterService" p:fighter-ref="fighter1"></bean>
<!-- 3、基本类型注入-->
<bean id="fighter3" class="com.baizhiedu.basic.pojo.Fighter">
<property name="age" >
<value>5</value>
</property>
<property name="name">
<value>歼20</value>
</property>
<property name="model">
<value>A</value>
</property>
</bean>
<!-- 4、array/list/map/properties注入 -->
<bean id="fighterList" class="com.baizhiedu.basic.pojo.FighterList">
<property name="age">
<value>5</value>
</property>
<!-- 数组类型属性注入 -->
<property name="courses">
<array>
<value>Java</value>
<value>C++</value>
<value>Python</value>
</array>
</property>
<!-- 注入list集合类型,值是字符串或者基本数据类型-->
<property name="stringList">
<list>
<value>1</value>
<value>2</value>
<value>3</value>
</list>
</property>
<!-- 注入list集合类型,值是对象-->
<property name="courseList">
<list>
<ref bean="course1"></ref>
<ref bean="course2"></ref>
</list>
</property>
<!-- 注入map集合类型,值是字符串或者基本数据类型-->
<property name="userMap">
<map>
<entry key="a" value="b"></entry>
<entry key="a" value="c"></entry>
</map>
</property>
<!-- 注入map集合类型,值是对象-->
<property name="userFighterMap">
<map>
<entry key="1" value-ref="fighter1"></entry>
<entry key="2" value-ref="fighter2"></entry>
<entry key="3" value-ref="fighter3"></entry>
</map>
</property>
<!-- 注入set集合类型,值是字符串或者基本数据类型-->
<property name="sets">
<set>
<value>北京</value>
<value>上海</value>
</set>
</property>
<!-- 注入set集合类型,值是对象-->
<property name="set2s">
<set>
<ref bean="city1"></ref>
<ref bean="city2"></ref>
</set>
</property>
<!-- 注入properties集合类型,值是字符串-->
<property name="properties">
<props>
<prop key="p1">pp1</prop>
<prop key="p2">pp2</prop>
</props>
</property>
</bean>
【注:】
1、set注入时的property标签的name值是set方法的部分内容,例如setUrl,那么name值为url,即去掉set后首字母小写,我之前一直以为name值是变量值,结果用setUrl 错了半天
2、构造器注入无法主动解决循环依赖,要么在设计时避免循环,要么用@Lazy懒加载,基本就是破坏循环的条件,使得一方可以先创建成功


IOC——Spring bean装配
1、介绍
1.1、实例的属性填充触发装配的执行,具体逻辑是将Bean的依赖关系建立起来后依赖注入根据获取的实例注入实例的属性;
1.2、装配方式:
(1)xml装配;
(2)注解装配;
(3)自动装配。
2、自动装配
Spring 容器支持自动装配,这意味着容器不需要<constructor-arg>、<property>配置,能通过Bean工厂自动处理bean之间的协作。
IOC——导入properties文件
<context:property-placeholder location="classpath:jdbc.properties"/>
IOC——切分配置文件
即主配置文件会引入其它多分配置文件,目的是方便功能区分
1、创建一个分文件

2、beans标签导入
xmlns:p="http://www.springframework.org/schema/p"

3、导入分文件

IOC——Spring相关API
applicationContext
接口类型,代表应用上下文,可以通过其实例获得spring容器中的Bean

IOC——getBean()
下面代码是ClassPathXmlApplicationContext的顶级父类AbstractApplicationContext定义的
public Object getBean(String name) throws BeansException {
this.assertBeanFactoryActive();
return this.getBeanFactory().getBean(name);
}
public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
this.assertBeanFactoryActive();
return this.getBeanFactory().getBean(name, requiredType);
}
public Object getBean(String name, Object... args) throws BeansException {
this.assertBeanFactoryActive();
return this.getBeanFactory().getBean(name, args);
}
public <T> T getBean(Class<T> requiredType) throws BeansException {
this.assertBeanFactoryActive();
return this.getBeanFactory().getBean(requiredType);
}
public <T> T getBean(Class<T> requiredType, Object... args) throws BeansException {
this.assertBeanFactoryActive();
return this.getBeanFactory().getBean(requiredType, args);
}
从中发现getBean()根据id、name、反射类等属性来获取Bean,其中根据反射类获取必须要求IOC容器中对应类型的bean有且只有一个
IOC——Spring配置数据源
maven再添上下面坐标
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.4</version>
</dependency>
配置1:jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.0.2:3306/test1
jdbc.account=root
jdbc.password=961003
配置2: applicationContext.xml
其中为了支持导入properties,添加了context依赖

bean调用引入的context依赖
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation=
"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"
>
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<bean id="datasource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName">
<value>${jdbc.driver}</value>
</property>
<property name="url">
<value>jdbc:mysql://192.168.0.2:3306/test1</value>
</property>
<property name="username">
<value>root</value>
</property>
<property name="password">
<value>961003</value>
</property>
</bean>
</beans>
测试
@Test
public void test2(){
// 基于spring启动druid
ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
DruidDataSource dataSource = (DruidDataSource) ioc.getBean("datasource");
System.out.println(dataSource.toString());
}
IOC——Spring注解


【补:】一些常用的注解
@ImportResource(locations = {""classpath:spring.xml}):导入bean配置文件
@ConfigurationProperties(prefix = "aliyun.oss"):将yml配置文件的相关数据实例化
@Aspect:切面配置
@Transactional(rollbackFor = Exception.class):事务处理
@CacheNamespace(implementation= MybatisRedisCache.class):Mybatis二级缓存配置
【注:】
1.@Configuration相当于application.xml,是实例加载的核心配置,XmlWebApplicationContext加载xml,而AnnotationConfigWebApplicationContext加载的是配置类;
2.注解核心分两种,一种是配置类注解,一种是功能类注解。配置类注解强调full/lite配置实例的生效,功能类注解强调特殊功能的生效。
3.Spring 的 ConfigurationClassPostProcessor 在判断一个类是否为 Lite 配置类 时,如果类上仅仅标注了 @Import、@ComponentScan、@ImportResource、@Component,或者仅有 @Bean 方法的类,就会将其作为配置类处理。但是官方推荐同步添加@Configuration来清晰语义并且可以引入cgLib成为完全配置类。
4.而且以上的情况(@Component除外)即便被ComponentScan扫描到也不会生效,必须被显示声明,例如配置类上添加@Import(MyConfig.class) 或者添加@Configuration、@Component等。
| 类上的注解 | 是否会被 @ComponentScan 自动发现? | 是否需要有额外的发现机制才能被处理? | 能否最终成为 Lite 配置类并生效? |
|---|---|---|---|
只有 @Import | ❌ 否 | ✅ 是(需要被 @Import 或手动注册) | ✅ 是(被发现后) |
只有 @ImportResource | ❌ 否 | ✅ 是(同上) | ✅ 是(被发现后) |
只有 @ComponentScan | ❌ 否 | ✅ 是(同上) | ✅ 是(被发现后) |
只有 @EnableConfigurationProperties | ❌ 否 | ✅ 是(同上) | ❌ 否(即使被发现也不会被当作 Lite 配置类) |
@Component + 上述任何注解 | ✅ 是 | 不需要 | ✅ 是 |
5.@Import 能合并@Configuratiioin配置信息,还能显式声明一些注解单一的配置类并将其实例化,原理:
当
ConfigurationClassPostProcessor解析带@Configuration的类上的@Import注解时:
为
@Import指定的类(例如OnlyImport.class)强制创建BeanDefinition。然后将这个
BeanDefinition注册到容器中。- 处理类上注解
被导入的类上的
@Bean方法会被解析,生成对应的 Bean 定义。被导入的类上的
@Import注解会被进一步解析,导入下一级类。被导入的类上的
@ImportResource、@ComponentScan等也会生效。被导入的类本身(除非是
abstract或接口)会被注册为一个单例 Bean。在容器刷新的后续过程中,这个
BeanDefinition会被实例化(除非是abstract类或指定了懒加载);
6.@ComponentScan生效原理
1 --> ConfigurationClassPostProcessor:Bean定义注册后置处理器递归解析所有配置类;
2 --> 发现 @ComponentScan注解;
3 --> 开启ComponentScanAnnotationParser执行实际扫描;
4 --> 解析 @Component 、@Configuration等注解;// 仅生效有实例化功能的注解
5 --> BeanDefinition 并注册;
6 --> H[继续解析其他配置类;
7 --> 所有配置类解析完成;
7.@SpringBootApplication就是@Configuration;
8.@ConfigurationProperties需要和@Component、@PropertySource合用,配置信息类才会正常实例化并导入匹配的配置信息。如果配置信息类没有@Component,则需要额外设置一个@Configuration配置类并声明@EnableConfigurationProperties,然后把配置信息类的字节码导入@EnableConfigurationProperties注解(例如@EnableConfigurationProperties(Student.class))
9.@PropertySource默认仅支持导入properties文件,如果想要导入yml文件需要自定义YamlPropertySourceFactory
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.DefaultPropertySourceFactory;
import org.springframework.core.io.support.EncodedResource;
import java.io.IOException;
public class YamlPropertySourceFactory extends DefaultPropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
String filename = resource.getResource().getFilename();
if (filename == null || !filename.endsWith(".yml")) {
return super.createPropertySource(name, resource);
}
return new YamlPropertySourceLoader().load(filename, resource.getResource()).get(0);
}
}
然后将yml地址和自定义工厂类的字节码作为属性信息注入
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.boot.context.properties.bind.DefaultValue;
@Configuration
@PropertySource(value = "classpath:custom.yml", factory = YamlPropertySourceFactory.class)
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private String name = "default";
private String version = "0.0.1";
// getters/setters
}
IOC——bean生命周期

源码描述:初始化bean执行init方法的流程

1、Spring容器从XML文件读取bean的定义,所有bean定义被集中到BeanDefinition;
2、BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry增强处理beanFactory;
3、BeanFactoryPostProcessor#postProcessBeanFactory增强处理beanFactory;
4、BeanFactory通过反射机制获取无参构造器,然后无参实例化;
5、Spring容器执行populateBean方法,即通过bean 的set 方法属性填充。
6、Spring容器执行initializeBean方法下的invokeAwareMethods方法:
6.1、若bean实现BeanNameAware接口,传递beanId到setBeanName方法;
6.2、若bean实现BeanClassLoaderAware接口,传递bean的类加载器到setBeanClassLoader方法;
6.3、若Bean实现BeanFactoryAware接口,传递beanfactory到setBeanFactory 方法;
7、Spring容器执行initializeBean方法下的applyBeanPostProcessorsBeforeInitialization
方法:
7.1、若bean实现了BeanPostProcessor接口,postProcessBeforeInitialization方法将被调用,例如,ApplicationContextAwareProcessor用于为bean注入扩展资源信息,InitDestroyAnnotationBeanPostProcessor用于为bean处理@PostConstruct声明的方法,BeanValidationPostProcessor用于为bean做JSR检验(@NotBlank,@Max,@Min,
@NotNull等)
8、Spring容器执行initializeBean方法下的invokeInitMethods方法:
8.1、首先判断组件类是否实现InitializingBean接口并且重写afterPropertiesSet 方法,afterPropertySet 方法会被调用;
8.2、然后判断是否声明了inti-method,通过配置文件xml 或者 配置类的@Bean注解来声明init-method,其对应方法会被调用;
<bean id="person" class="com.baizhiedu.basic.pojo.Person" init-method="init" destroy-method="destroy" ></bean>
@Configuration
public class Config {
@Bean(initMethod = "init")
public ThirdPartyLib lib() {
return new ThirdPartyLib();
}
}
class ThirdPartyLib{
// initMethod 属性指定当前方法,在实例初始化阶段调用
public void init(){
...
}
}
9、Spring容器执行initializeBean方法下的applyBeanPostProcessorsAfterInitialization
方法:
9.1、postProcessAfterInitialization() 方法将被调用,常用于创建AOP代理类(最重要)、Bean 包装/增强类、监控/追踪类、特定功能处理类(先判断beanPostProcessor的earlyBeanReferences是否已经有bean信息,有则说明属性填充阶段循环依赖场景已经被提前暴露,没有则备份进earlyBeanReferences,然后执行wrapifnecessary方法(代理核心模板方法))
public class ApplicationContextAwareProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (!(bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware ||
bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware ||
bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware)) {
return bean;
}
// 按顺序调用各个Aware接口
invokeAwareInterfaces(bean);
return bean;
}
private void invokeAwareInterfaces(Object bean) {
if (bean instanceof EnvironmentAware) {
((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
}
if (bean instanceof EmbeddedValueResolverAware) {
((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);
}
if (bean instanceof ResourceLoaderAware) {
((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
}
if (bean instanceof ApplicationEventPublisherAware) {
((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
}
if (bean instanceof MessageSourceAware) {
((MessageSourceAware) bean).setMessageSource(this.applicationContext);
}
if (bean instanceof ApplicationContextAware) {
((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
}
}
}
IOC——参与bean生命周期的方法
这些方法的重写定义可以让程序有很高的扩展性,从上述的第六步按序往后都是参与bean生命周期的方法;
IOC——最重要的bean生命周期函数
@PostConstructor注解方法和@PreDestory注解方法是最核心的生命周期函数,第一个是setup,在容器加载bean 后被调用。第二个方法是teardown,在容器卸载bean 前被调用。
IOC——依赖问题
一个完整的对象包含两部分:当前对象实例化和对象属性的实例化,在Spring中,对象的实例化是通过反射无参构造器实现的,然后做属性填充,如果填充引用对象,可以分为三类情况,即顺序依赖、循环依赖和代理依赖。
1、顺序依赖是指对象a 属性填充对象b,然后从容器获取或者创建b 并返回给a;
2、循环依赖是指对象a 引用的对象还没创建,此时需要创建对象b ,但是b 进行属性填充又刚好需要a;
3、循环代理依赖是指对象a 引用的对象还没创建,此时需要创建引用对象b ,但是b进行属性填充又刚好需要a 而且a 需要动态代理
refresh 方法针对以上三种情况给出了解决方案
顺序依赖

循环依赖

代理依赖

IOC——三个缓存
三个缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
基于依赖问题的解决可以发现,如果是顺序依赖,一级依赖足够,如果是循环依赖,二级缓存足够,如果是代理依赖,则需要三个缓存
三级换成能否取消
可以,最初spring不支持aop,只有两个缓存,后来支持aop,增加了三级换成,二级缓存完全可以改造后融入三级缓存功能,这里我理解是为了版本兼容和符合开闭原则
IOC——BeanDefinition
通过配置文件、BeanDefinitionRegistryPostProcessor加载关于bean的元数据以及增强处理的元数据;
- Bean 的类名
- Bean 配置信息,作用域、生命周期回调、延迟加载、初始方法、销毁方法等
- Bean 之间的依赖设置,dependencies
- 构造参数、属性设置
有哪些配置可以提供元数据
- XML配置文件
- 基于注解的配置
- 基于java的配置,如postProcessor接口。
标签格式:id是唯一标识,name支持多个标识,class指定bean类型(标识只绑定当前bean,其它bean禁止使用)
<bean id="person" name="n1,n2,n3" class="com.baizhiedu.basic.Person" scope="singleton"></bean>
<bean id="person_2" name="n1,n2,n3" class="com.baizhiedu.basic.Person" scope="singleton"></bean>
注解格式:只能指定唯一标识
@Component("person")
public class Person{
}
以ClassPathXmlApplicationContext为例
Person类
@Data
@AllArgsConstructor
public class Person {
public Person(){
System.out.println("new Persong");
}
private String name;
private int age;
private String password;
private void init(){
System.out.println("init");
}
private void destroy(){
System.out.println("destroy"+age);
}
}
测试代码
@Test
public void test1(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("/applicationContext.xml");
// Person p = (Person)ioc.getBean(id|name); 可以根据bean的id、name获取bean实例
Person p1 = (Person)ioc.getBean("person");
Person p2 = (Person)ioc.getBean("n1");
System.out.println(p1.toString());
p1.setAge(10);
System.out.println(p2.toString());
}
IOC——Spring框架的单例bean是线程安全的吗?
IOC——BeanFactory
IOC——XMLBeanFactory
IOC——ApplicationContext
ApplicationContext实现类
- FileSystemXmlApplicationContext :此容器从一个XML文件中加载bean定义,基于全路径名设置配置文件;
- ClassPathXmlApplicationContext:此容器也从一个XML文件中加载bean定义,基于classpath设置配置文件;
- XmlWebApplicationContext:此容器加载一个XML文件,此文件定义了包含web功能的所有bean;
- AnnotationConfigWebApplicationContext:用于配置类的加载;
- ServletWebServerApplicationContext:是SpringBoot默认的Context;
ApplicationContext 和BeanFactory的区别
- BeanFactory 是Spring 框架最核心的接口,用来管理bean的生命周期,生命周期指的是负责bean的实例化、属性填充、初始化、调用和消亡。
- ApplicationContext 是整个Spring的启动类,创建和管理BeanFactory ,加载配置信息,为BeanFactory 初始化BeanFactoryPostprocessor、BeanDefinitionMap 和BeanPostprocessor,最后由BeanFactory 管理BeanDefinitionMap 提供的业务bean
IOC——BeanDefinitionRegistryPostProcessor
BeanDefinitionRegistryPostProcessor 是BeanFactoryPostProcessor 子接口,用于注册BeanDefinition,在obtainFreshBeanFactory() 装配完BeanFactoryPostProcessor 实现类后执行BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry(BeanDefinitionRegistry bdr):void,比如ConfigurationClassPostProcessor就是以此函数解析@ComponentScan、@Component、@Service、@Repository 和 @Configuration的@Bean、@Import等标识 去生成BeanDefinition
【补充:】
以上说了ConfigurationClassPostProcessor会根据@ComponentScan描述的包路径去扫描配置类,这里补充下细节:
1.添加@Configuration声明类、添加@Bean声明方法:full模式,会为配置类设置代理对象
public class SingletonRequiredConfig {
@Bean // 初始化时执行该方法,将创建的结果实例缓存到容器
public ExpensiveResource expensiveResource() {
return new ExpensiveResource();
}
@Bean
public Service1 service1() {
return new Service1(expensiveResource()); // 从容器获取bean名称为expensiveResource的实例
}
@Bean
public Service2 service2(ExpensiveResource expensiveResource) {
return new Service2(expensiveResource); // 从容器获取bean名称为expensiveResource的实例
}
}
2.仅添加了@Bean声明方法,或者添加的@Configuration注解改成 ——> @Configuration(proxyBeanMethods = false):lite模式,即不会为其类型设置代理
public class SingletonRequiredConfig {
@Autowired
private ApplicationContext context;
@Bean // 初始化时执行该方法,将创建的结果实例缓存到容器
public ExpensiveResource expensiveResource() {
return new ExpensiveResource();
}
@Bean
public Service1 service1() {
return new Service1(expensiveResource()); // 调用方法方式:由于配置类无代理,无法先去容器获取实例,正常执行方法
}
@Bean
public Service2 service2(ExpensiveResource expensiveResource) {
return new Service2(expensiveResource); // 入参方式:可以从容器正常获取bean名称为expensiveResource的实例
}
@Bean
public Service3 service3() {
ExpensiveResource expensiveResource = context.getBean("expensiveResource"); // 容器获取方式:直接从容器获取bean名称为expensiveResource的实例
return new Service3(expensiveResource);
}
}
3.添加@Component等派生注解声明类、添加@Bean声明方法 :lite模式,不会为配置类设置代理对象,同2。
4.添加@Concentration注解,如果要导入其它配置实现扩展,则用@Import注解
IOC——BeanDefinitionRegistry
介绍
注册BeanDefiniton到BeanDefinitionMap的对象,提供registry(BeanDefinition bd)方法;
注册原理 /底层源码
执行beanFactory的beanDefinitionMap.put(BeanDefinition bd);
场景
一般是BeanDefinitionRegistryPostProcessor、ImportBeanDefinitionRegistrar的实现类的重写方法会获取该对象,方法以此来注册beanDefinition,beanFactory也提供注册服务
IOC——ImportBeanDefinitionRegistrar
ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry():void 用来解析配置类的@Import注解,不同于以往import的配置类,这次import的是实现了ImportBeanDefinitionRegistrar接口的实现类,以ConfigurationTest.java为例
package com.zzq.core.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import com.zzq.core.importbeandefinitionregistrar.ImportBeanDefinitionRegistrarTest;
@Configuration
@Import(ImportBeanDefinitionRegistrarTest.class) //导入
public class ConfigurationTest {
}
ImportBeanDefinitionRegistrarTest.java:重写registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry),重写内容是自定义beanDefinition并通过beanDefinitionRegistry注册beanDefinition
package com.zzq.core.importbeandefinitionregistrar;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import com.zzq.core.importbeandefinitionregistrar.bean.TestBean;
import com.zzq.other.autowired.autowiredtest1.AutowiredTest;
public class ImportBeanDefinitionRegistrarTest implements ImportBeanDefinitionRegistrar{
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
BeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClassName(TestBean.class.getName());
MutablePropertyValues values = beanDefinition.getPropertyValues();
values.addPropertyValue("id", 1);
values.addPropertyValue("name", "ZhangSan");
//这里注册bean
registry.registerBeanDefinition("testBean", beanDefinition );
}
}
IOC——BeanFactoryPostProcessor
用作BeanDefinition 的后置处理,常见的有PlaceholdConfigurerSupport和ConfigurationClassPostProcessor,一个是用来做参数校验并注入占位符,一个是用来识别相关注解生成待实例化的BeanDefinition。
IOC——BeanPostProcessor
在bean生命周期的初始化阶段(exportObj = initializingBean(bean))做前置后置或者环绕增强,如AbstractAutoProxyCreator,就是专用来做AOP增强的BeanPostProcessor
IOC——BeanFactory和FactoryBean的区别
beanFactory
beanFactory是applicationContext的实现接口,beanFactory也是applicationContext维护的IOC管理实例,项目启动时applicationContext要执行初始化流程,其中就包括beanFactory实例的创建和维护,beanFactory维护着一二三级缓存、beanDefinitionMap、beanFactoryPostProcessor、beanPostProcessor、resolvableDependencies、aliasMap等元信息(meta message),拥有getBean(beanName),contains(beanName)等方法。
factoryBean
factoryBean是一个泛型接口,你需要去实现getObject()、getObjectClass()、isSingleton(),并在实现类上添加@Service或者@Component等注解。在bean实例化阶段,spring仅将实现类实例放入IOC,然后调用getObject()将返回值放入factoryBeanObjectCache缓存起来,当getBean("&"+beanName)时从beanFactory获取factoryBean实例,当getBean(beanName)时候获取的则是beanFactory#getObject方法的返回值(先从factoryBeanObjectCache缓存获取,没有再调用getObject方法)。
factoryBean的目的是为一些复杂实例提供一个初始化的流程模板,相对于开发者自定义一个配置类然后通过@Bean调用配置方法来实现这个流程更加轻量化,即一个factoryBean仅用于一个bean实例的生成,但是一个配置类可以通过@Bean注解声明多个方法来获取多个bean实例。
【注:】
配置类的@Bean方法支持返回FactoryBean实例,而且其方法生成的FactoryBean实例的getObject方法会被正常执行生成bean实例,好处:
1.如果使用@Component声明FactoryBean的方式创建Bean实例,只能创建一个,但是通过配置类的@Bean就可以创建多个。
@Configuration
public class AppConfig {
@Bean
public FactoryBean<SomeType> factoryForA(SomeDependency depA) {
MyFactoryBean fb = new MyFactoryBean();
fb.setDependency(depA);
fb.setProperty("valueA");
fb.init(); // 调用自定义初始化方法
return fb;
}
@Bean
public FactoryBean<SomeType> factoryForB(SomeDependency depB) {
MyFactoryBean fb = new MyFactoryBean();
fb.setDependency(depB);
fb.setProperty("valueB");
return fb;
}
}
2.有些FactoryBean通过依赖添加进来的,开发者无权修改源码内容,但是配置类的@Bean方法可以声明变量参数,前置一些初始化操作,也就是说通过自定义配置类@Bean方法的方式来灵活创建FactoryBean实例。
IOC——@Autowired修饰静态变量注入失效解决方案
@Autowired修饰set方法
@Component
public class TestBean {
// 静态变量
private static DictService dictService1;
private static DictService dictService2;
@Autowired
public void setDictService(DictService dictService){
TestBean.dictService1 = dictService;
}
// 多个同类型实例需要额外指定
@Autowired
public void setDictService2(@Qualifier("dictService") DictService dictService){
TestBean.dictService2 = dictService;
}
public static void getDict(String type) throws Exception {
// 注入成功后,在静态方法里面使用静态变量
dictService.getDict(type);
}
}
@PostConstructor
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class RedisUtil {
private static RedisTemplate<Object, Object> redisTemplates;
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
//或者autowired来装配applicationContext实例,然后在initialize中调用applicationContext.getBean(...)来实现对象赋值
@PostConstruct
public void initialize() {
redisTemplates = this.redisTemplate;
}
/**
* 添加元素
*
* @param key
* @param value
*/
public static void set(Object key, Object value) {
if (key == null || value == null) {
return;
}
redisTemplates.opsForValue().set(key, value);
}
}
【扩展:】
扩展一:bean名称匹配(即bean的id/name匹配)
@Autowired先根据类型 和 bean名称(id/name)匹配bean,然后注入到其它实例的成员变量,例如:
情况一: @Component // 该bean实例的id/name默认是userRepository,且该类型的bean容器中仅有这一个 public class UserRepository { ... } @Autowired // 用该变量的变量名userRepository去容器中匹配类型为UserRepository且bean名称(id/name)为userRepository的bean,然后注入到该变量 private UserRepository userRepository;如果匹配不到bean名称,但是有一个类型匹配的bean,也能实现注入,例如:
情况二: @Component("myUserRepo") // Bean名称(id/name)为myUserRepo,且该类型的bean容器中仅有这一个 public class UserRepository { ... } @Autowired // 由于类型匹配,会将myUserRepo注入该变量userRepository private UserRepository userRepository;总结:
当只有一个类型匹配的bean时,会注入那个唯一的bean,变量名不影响注入。
当存在多个同类型的bean,且其中一个bean名称和变量名匹配,会注入那个Bean。
当存在多个同类型的bean,且任何bean名称都无法和变量名匹配,则报错:NoUniqueBeanDefinitionException。
当存在多个同类型的bean,且任何bean名称都无法和变量名匹配,可以为变量补充@Qualifier来匹配bean名称 或者限定标记符。
当存在多个同类型的Bean,且任何bean名称都无法和变量名匹配,但是有一个Bean标记了@Primary,那么会注入这个Bean。
当存在多个同类型的Bean,既有名称匹配的Bean A,又有@Primary的Bean B,那么优先按照名称匹配Bean A。
当没有任何类型匹配的Bean,报错:NoSuchBeanDefinitionException。
扩展二:注解使用方式
@Autowired的使用方式有很多
可以使用 @Autowired直接标注类中的字段,快速使用。
可以使用 @Autowired 标注构造器、方法,这样可以方便注入多个 Bean。
可以 @Autowired标注 Array (数组),Collections(集合),甚至是 Map (散列表),将所有匹配到的 Bean 注入进来。
【注:】
使用 @Autowired 注入属性到 Map 中,key 必须是 String 类型,用于引入bean名称。
IOC——@Qualifer应用方式(限定标记符)
方式一:和@Component等配置注解合用
@Component // 默认设置bean名称(id/name)为mainRepository
@Qualifier("persistent")
@Qualifier("cached")
@Qualifier("main") // 可以有多个限定符
public class MainRepository implements Repository { ... }
方式二:和@Autowired合用
@Repository // 默认bean名称(id/name)为jdbcUserRepository
@Qualifier("jdbc") // 为bean设置限定表记符为jdbc
public class JdbcUserRepository { ... }
@Repository
@Qualifier("jpa")
public class JpaUserRepository { ... }
// 业务层:根据环境选择不同的实现
@Service
public class UserService {
@Autowired
@Qualifier("jdbcUserRepository ") // 通过bean名称指定bean注入
private JdbcUserRepository jdbcUserRepository ;
@Autowired
@Qualifier("jdbc") // 通过限定表记符指定bean注入
private JdbcUserRepository jdbcUserRepository ;
}
限定标记符、id、name的区别:
1.定位不同
如果在bean名称中,id是唯一标识符,name是别名,那么限定标记符就是分类,如医生、外科医生、教师、语文教师。
2.发展背景
2.1.XML时代Spring 严格区分了 id 和 name:
-
id:在 XML 中必须唯一,且遵循 XML 命名规范(不能包含特殊字符,如,;等)。 -
name:可以指定多个别名(用逗号或分号分隔),不要求唯一,实际上可以重复,但通常用于提供别名。
例如:
<bean id="userService" name="us,userSvc" class="..."/>
这里 id="userService" 是主标识,name 里给了两个别名 us 和 userSvc。
2.2.在注解时代,这个概念被简化了
@Component("userService") 中的 value 值被定义为 bean name。Spring 内部将它作为该 Bean 在容器中的唯一标识。
-
开发者不需要再区分
id/name属性,统一描述为bean name,BeanDefinition会自动分配id和name。 -
这个名称默认是唯一的(重复注册时后一个可能覆盖前一个,取决于配置)。
-
如果需要给 Bean 起多个别名,需要使用
@Bean(name = {"bean1", "bean2"})(第一个名称会作为id)或通过 XML 的<alias>/<bean name="xxx,xxx,xxx">等标签,@Component不支持多名称。
2.3.qualifer
当@Autowired和@Qualifer合用时候,@Qualifer会先按照限定符查找,找不到 再按照Bean Name查找
// Spring 内部会为 Bean 存储这样的信息:
BeanDefinition {
beanName: "userRepository", // 主要名称
alias:["name1","name2"],
beanClass: JdbcUserRepository.class,
qualifiers: ["jdbc"], // 限定符列表
// ...
}
IOC——@Primary应用方式
public interface MyService {
void doSomething();
}
@Component("myService") // Bean名称设置为myService,没有@Primary
public class MyServiceImpl1 implements MyService {
@Override
public void doSomething() {
System.out.println("MyServiceImpl1");
}
}
@Component
@Primary // 标记为@Primary
public class MyServiceImpl2 implements MyService {
@Override
public void doSomething() {
System.out.println("MyServiceImpl2");
}
}
@Service
public class ClientService1 {
@Autowired
private MyService myService; // 变量名是myService,会匹配到Bean名称为myService的MyServiceImpl1
public void test() {
myService.doSomething(); // 输出:MyServiceImpl1
}
}
@Service
public class ClientService2 {
@Autowired
private MyService service; // 变量名是service,无法匹配到Bean,但是会获取标记@Primary的bean,即MyServiceImpl2
public void test() {
service.doSomething(); // 输出:MyServiceImpl2
}
}
IOC——@ComponentScan应用方式
-
@ComponentScan是一个配置类注解,Spring 在内部会将带有 @ComponentScan 的类也视为配置类,所以不用必须添加@Configuration。@Configuration @ComponentScan("com.example") public class ScanConfig { // 没有 @Bean 方法 } // 测试 public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigWebApplicationContext(ScanConfig.class); // 这个类会被识别为配置类吗?会的,因为 Spring 会检查 @ComponentScan 注解,有注解就会处理。 // 但实际上,Spring 在内部会将带有 @ComponentScan 的类也视为配置类。 } -
为了明确和一致,我们通常将
@ComponentScan与@Configuration一起使用。 -
配置类的定义:有
@Configuration、或有@Bean方法、或有@Component等注解(但通常我们用@Configuration来明确表示配置类)。 -
@SpringBootApplication:在 Spring Boot 中,通常使用 @SpringBootApplication,它包含了 @ComponentScan
IOC——@Import应用方式
目的:
将多个配置类组合起来,方便导入ApplicationContext,避免逐个注册或重复扫描。
应用:
-
导入配置类:在一个配置类上使用@Import导入另一个配置类,这样可以将多个配置类组合在一起。
@Configuration @Import(OtherConfig.class) public class AppConfig { // ... } -
导入普通的类:可以将一个普通的类导入到Spring容器中,这个类会被注册为一个Bean。通常这个类上可能有@Configuration、@Component等注解,但即使没有,Spring也会尝试将其作为配置类处理(如果它包含@Bean方法等)。
@Configuration @Import(SomeClass.class) public class AppConfig { // ... } -
导入ImportSelector接口的实现类:通过实现ImportSelector接口,可以动态地选择要导入的配置类。这个接口有一个selectImports方法,返回一个字符串数组,每个字符串是一个配置类的全限定名。
public class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[]{"com.example.Config1", "com.example.Config2"}; } } @Configuration @Import(MyImportSelector.class) public class AppConfig { // ... } -
导入ImportBeanDefinitionRegistrar接口的实现类:这个接口允许我们直接注册BeanDefinition到容器中,提供了更灵活的方式。
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { RootBeanDefinition beanDefinition = new RootBeanDefinition(SomeBean.class); registry.registerBeanDefinition("someBean", beanDefinition); } } @Configuration @Import(MyImportBeanDefinitionRegistrar.class) public class AppConfig { // ... } -
组合使用,将以上各种类一次性导入
@Configuration @Import({ ConfigA.class, // 配置类 MyImportSelector.class, // ImportSelector MyBeanRegistrar.class, // ImportBeanDefinitionRegistrar SomeComponent.class // 普通组件 }) public class AppConfig { // 多种导入方式组合 } -
在Spring Boot中,启动注解@SpringBootApplication内部的@EnableAutoConfiguration也捎带说明一下,其内部也有@Import用于导入自动配置类,实现Spring Boot自动配置。(在Spring Boot中,我们还会看到一些组合注解,比如@EnableAsync、@EnableScheduling等@EnableXXX形式的注解,这些注解没有具体的处理器来解析但仍然生效,就是因为内部继承了@Import并且声明了导入的的配置类,也就是说真正生效的是@Import,外部的@EnableXXX仅仅用于封装和明确功能。当然不是所有@EnableXXX都是这种模式,例如@EnableEurekaClient,就是@EnableAutoConfiguration去加载了相关依赖包的配置类后实例化,该实例去扫描类路径检测是否有@EnableEurekaClient声明来达到生效目的)
IOC——@ConfigurationProperties/@Value实现配置信息实例化或属性化
前面IOC——Spring注解章节已经描述了@ConfigurationProperties的使用方法,这里细化Spring Boot 和 Spring MVC 两种场景的应用。
一、Spring Boot 场景(最简方式)
Spring Boot 会自动加载 application.properties 或 application.yml,无需额外注解。
1. 使用 @ConfigurationProperties(类型安全,推荐)
// 1. 定义配置类
@ConfigurationProperties(prefix = "aliyun.oss")
public class OssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
// getters / setters
}
// 2. 启用配置(方式一:加 @Component)
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class OssProperties { ... }
// 或方式二:在启动类或配置类上用 @EnableConfigurationProperties
@SpringBootApplication
@EnableConfigurationProperties(OssProperties.class)
public class Application { ... }
# application.properties
aliyun.oss.endpoint=oss-cn-hangzhou.aliyuncs.com
aliyun.oss.access-key-id=xxx
aliyun.oss.access-key-secret=yyy
aliyun.oss.bucket-name=my-bucket
然后在 Service 中注入使用(如果构造器仅有一个构造方法则不需要声明@Autowired):
@Service
public class OssService {
private final OssProperties ossProperties;
public OssService(OssProperties ossProperties) {
this.ossProperties = ossProperties;
}
// 使用 ossProperties.getEndpoint() 等
}
2. 使用 @Value 注入单个属性
@Service
public class OssService {
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.access-key-id}")
private String accessKeyId;
// ...
}
【注:】@Value 不支持宽松绑定(如 access-key-id 无法自动转成 accessKeyId),推荐使用 @ConfigurationProperties。
二、传统 Spring MVC 场景(无 Spring Boot)
传统 Spring MVC 项目需要手动加载 properties 文件,才能使用 @ConfigurationProperties或者@Value。
1. 使用 XML 配置加载 properties 文件(或者配置类声明 @PropertySource)
<!-- applicationContext.xml 或 spring-mvc.xml -->
<context:property-placeholder location="classpath:config.properties" />
然后在 Java 代码中使用 @Value:
@Component
public class OssService {
@Value("${aliyun.oss.endpoint}")
private String endpoint;
}
2. 使用 @PropertySource + @ConfigurationProperties(Java Config 方式)
@Configuration
@PropertySource("classpath:config.properties") // 指定 properties 文件位置
@EnableConfigurationProperties(OssProperties.class)
public class AppConfig {
}
// OssProperties 类不需要 @Component,由 @EnableConfigurationProperties 注册
@ConfigurationProperties(prefix = "aliyun.oss")
public class OssProperties {
// getters/setters
}
注意:@PropertySource 默认不支持 YAML,且不能用于 Spring Boot 的 application.properties 覆盖场景(Spring Boot 有自己的处理逻辑)。
3. 使用 Environment 抽象(编程式获取)
首先通过 <context:property-placeholder> 或 @PropertySource 将文件加载到 Environment 中。
在Service中引入使用:
@Autowired
private Environment env;
public void show() {
String endpoint = env.getProperty("aliyun.oss.endpoint");
}
三、自定义 properties 文件(非默认名称)
Spring Boot 中加载自定义文件
@PropertySource("classpath:custom.properties")
@ConfigurationProperties(prefix = "custom")
@Component
public class CustomProperties {
private String name;
// getter/setter
}
或者通过 @PropertySource + @ConfigurationProperties 配合 @EnableConfigurationProperties。
Spring MVC 中加载自定义文件
同上(传统 Spring MVC 场景),不再重复。
四、远程配置的更新
给声明@ConfigurationProperties或者@Value的类补充声明@RefreshScope即可,例如:
# 依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Actuator提供了/refresh端点,用于手动触发热更新 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
------------------------------------------------------------------
# yaml
spring:
application:
name: your-service-name
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 # Nacos 服务地址
file-extension: yaml # 配置文件格式
# 暴露 /actuator/refresh 端点,用于手动刷新
management:
endpoints:
web:
exposure:
include: refresh
然后在相关代码应用:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@Component
@RefreshScope
@ConfigurationProperties(prefix = "app.storage")
public class StorageProperties {
private String endpoint;
private String bucketName;
// ... 标准的 getter 和 setter
}
原理:

IOC——bean实例操作applicationContext
如果希望bean实例可以操作applicationContext,可以尝试实现ApplicationContextAware
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
//获取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通过name获取 Bean.
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
//通过class获取Bean.
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
//通过name,以及Clazz返回指定的Bean
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}
也可以直接@Autowired 来修饰ApplicationContext属性
@SpringBootTest
class SpringSecurityDemoApplicationTests {
@Autowired
private ApplicationContext context;
@Test
void test() {
BeanFactory factory = context.getAutowireCapableBeanFactory();
System.out.println("applicationUtils:"+factory.getBean("applicationUtils").toString());
}
}
Spring MVC——介绍
以前基于Servlet做开发,一个web接口对应一个servlet实现,当接口量多时需要维护大量的Servlet实现,这些实现中有大量的内容是公有的,结果就会造成冗余和维护麻烦,SpringMVC就是解决这种问题的web框架,实质是一个Servlet的封装,强悍之处在于开发的web接口都被纳入到此Servlet的管理之中,客户端请求只能发给这个Servlet,然后根据url映射到各自的web接口(控制器)。
人话:前端控制器就是前台,当你去前台预约工作时,前台会让相应部门做好接待
优点:一个Servlet的架构让通用功能不再冗余,万佛归一

Spring MVC——运行流程
这个Servlet就是DispatchServlet(调度服务),运行原理如下
1、DispatcherServlet接收请求数据
2、DispatcherServlet转发请求到HandlerMapping处理映射器
3、处理映射器找到具体的处理器Handler(Controller),生成处理器对象及拦截链(Interceptor chain)一并返回给Dispatcher
4、DispatcherServlet调用HanlerAdapter处理适配器
5、HandlerAdapter经过适配Handler调用具体的处理器(Controller)
6、Controller执行完成返回ModelAndView
7、HandlerAdapter将controller执行结果ModelAndView返回给Dispatcher
8、DispatcherServlet将ModelAndView传给ViewResolver视图解析器
9、ViewResolver返回具体View
10、DispatcherServlet根据View进行视图渲染,最后响应出去
完整的 DispatcherServlet 处理流程(简版)
doDispatch(request, response) ├─ 1. 调用 HandlerMapping.getHandler(request) → 获得 HandlerExecutionChain ├─ 2. 调用 HandlerAdapter.supports(handler) → 判断和获得支持该 handler 的适配器 ├─ 3. 执行拦截器链的 preHandle(...) ├─ 4. 调用适配器的 handle(...) → 执行 handler,处理请求并返回 ModelAndView(或 null) ├─ 5. 执行拦截器链的 postHandle(...) ├─ 6. 视图渲染(可选)→ 适配器如果需要渲染才会调用视图解析器,比如返回json则不需要视图渲染 └─ 7. 执行拦截器链的 afterCompletion(...)
Spring MVC——搭建
maven导入坐标
我们只需要配置mvc,只需这一个坐标即可(内置了spring-web、spring-context)
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.11.RELEASE</version>
</dependency>
web.xml配置DispatcherServlet

创建Controller和视图, 使用注解配置映射地址

配置spring-mvc.xml,添加扫描注解类配置

Spring MVC——Controller是不是单例
控制器也是单例,之所以保证了线程安全是因为整个单例的操作都是无状态的,每个request处理过后都无法扰动单例对象本身数据
Spring MVC——注解


【注:】
- 如果有全局url,那么return "success.jsp"是指在/user下找寻jsp返回,因此要用绝对路径来写:return "/success.jsp"
- return "/success.jsp" 实质是转发,通return "forward:/success.jsp",如果用重定向是return "redirect:/success.jsp"
Spring MVC——配置文件
扫描@Component注解
<context:component-scan base-package="com.baizhiedu.basic.mapper"/>
指定扫描组件范围
集成了SpringContext和SpringWeb的SpringMVC框架拥有两个IOC,service层的IOC是父容器,而spring-mvc.xml搭建的是子容器,这也就解释了为什么Controller可以调用service层。通常子容器先自查bean的有无,没有再去父容器查找。
背景介绍完了,现在的问题是如何避免两个容器重复包扫描同一类型的实例
解决方法
<!--Spring容器和SpringMVC容器虽然是父容器与子容器的关系,但二者之间具有一定的独立性。具体来说,两个容器基于各自的配置文件分别进行初始化,只有在子容器找不到对应的Bean时,才去父容器中去找并加载-->
<context:component-scan base-package="com.baizhiedu.basic.controller" >
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
- <context:include-filter type="" expression="">
- include-filter: 保留符合条件的component,和exclude-filter相对
- exclude-filter: 去除符合条件的component
- type: 有多种类型,目前我只用过annotation
- expression: 指定type的具体功能接口
自定义配置(修改自带的请求映射器、请求处理器、视图处理器)
SpringMVC 默认有相关的配置,但是不一定符合需求,我们如何自定义相关配置

案例:自定义视图处理器
spring-mvc.xml默认会加载视图处理器实现类(InternalResourceViewResolver)

手动加载此类,以便传递自定义参数


总结

Spring MVC——返回数据
返回页面
- 直接返回字符串

- 通过ModelAndView对象返回

测试
【注:】上述两种返回模式有多种写法,总结如下
【注:】以上的model封装也是为了方便将属性注入到requst的attribute
返回json数据
加入@ResponseBody表明返回数据不是页面,spring-mvcweb内置的jackson依赖会自动处理为json格式

Spring MVC——请求和响应的数据格式
常用的请求数据content-type:
- application/x-www-form-urlencoded
- multipart/form-data
- application/json
- application/xml
Spring MVC——cors
Spring MVC——文件处理
maven
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
spring-mvc.xml
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 总文件大小-->
<property name="maxUploadSize" value="5242800"></property>
<!-- 单个文件大小-->
<property name="maxUploadSizePerFile" value="5242800"></property>
<!-- 字符编码格式-->
<property name="defaultEncoding" value="UTF-8"></property>
</bean>
Spring MVC——内置拦截器
流程图:servlet、过滤器、拦截器、控制器
拦截器配置方式:
1、spring-mvc.xml
<!-- 配置拦截器 -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<!-- 排除下面这些请求 -->
<mvc:exclude-mapping path="/user/login"/>
<bean class="com.baizhiedu.basic.interceptor.MyInterceptor"></bean>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/other/**"/>
<bean class="com.baizhiedu.basic.interceptor.MyInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
2、实现WebMvcConfigurer重写addInterceptors方法
@Configuration
@EnableWebMvc
public class MyMvcConfig implements WebMvcConfigurer {
@Bean
public MyInterceptor getMyInterceptor(){
System.out.println("注入了MyInterceptor");
return new MyInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// getMyInterceptor()执行过就会直接返回实例
registry.addInterceptor(getMyInterceptor()).addPathPatterns("/**");
}
Spring MVC——异常处理
1、servlet的全局异常处理,可以通过异常状态码和异常类型拦截
<!-- 500 内部服务器错误 -->
<error-page>
<error-code>500</error-code>
<location>/page/error/500.jsp</location>
</error-page>
<!-- 内部服务器错误 空指针异常 -->
<error-page>
<exception-type>java.lang.NullPointerException</exception-type>
<location>/page/error/nullPoint.jsp</location>
</error-page>
2、SpringMVC内置处理器
<!-- 异常处理器-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!--出现除exceptionMappings配置的异常外,显示error.jsp页面-->
<property name="defaultErrorView" value="error"></property>
<property name="exceptionMappings">
<map>
<!-- error即/error.jsp 是按照试图处理器前后缀格式写的 -->
<entry key="java.lang.ClassCastException" value="error"></entry>
<entry key="java.lang.NullPointerException" value="error"></entry>
</map>
</property>
</bean>
3、SpringMVC通过注解实现异常处理器(Controller的后置增强)
@ControllerAdvice
public class AppWideExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public MyResult exception(HttpServletRequest request, Exception ex){
String msg="异常信息>>>>>异常名:"+ex.getClass()+"||方法名:"+ex.getStackTrace()[0].getMethodName()+"||类名:"+ex.getStackTrace()[0].getClassName()+"||行数:"+ex.getStackTrace()[0].getLineNumber();
return MyResult.error(msg);
}
}
Spring MVC——url-pattern设置
1、url-pattern匹配方式:
精准匹配:对请求资源做全名匹配,如/user/login.jsp
路径匹配:对请求资源做表达式匹配,如/*
后缀名匹配:对请求资源做后缀名匹配,如/user/*.jsp
扩展名匹配:不以/ 开头,也不能以*结尾,只能以后缀名结尾,如:*.jsp,常见于JspServlet (即根据url匹配到对应jsp模板文件,通过jsp模板引擎生成html后返回,匹配策略是截取客户端url的contextPath之后的路径,在项目根路径下匹配路径一致的jsp资源)
<!-- 这是 Tomcat/conf/web.xml 中的JspServlet默认配置 -->
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
缺省匹配:用于任何匹配方式都匹配不到的兜底,格式固定/,常见于DefaultServlet(静态资源匹配,但不支持匹配web-inf路径的资源,匹配策略是截取客户端url的contextPath之后的路径,在项目根路径下匹配路径一致的静态文件资源)
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
优先级:
整合所有的url-pattern设置路由表,不免出现几个路径冲突的情况,需要通过优先级解决
精准匹配 > 路径、后缀名匹配(二者不可同时存在)> 扩展名匹配(如jsp匹配器) > 缺省匹配
2、dispatcher的url-pattern设置
2.1.必须配置/,因为该匹配器是最低优先级匹配,例如客户端发送.jsp后缀的url请求会先匹配tomcat的JspServlet,因为其配置的匹配器是*.jsp,如果匹配不到再交给DispatcherServlet。 2.2.servlet的url-pattern不允许冲突,因此tomcat的全局web.xml配置的DefaultServlet被服务的web.xml配置的DispatcherServlet覆盖而失效了,这意味着DispatcherServlet还要支持静态资源的响应工作,一般是配置ResourceHandler或者转发给DefaultServlet处理(建议配置ResourceHandler,可以通过自定义映射关系匹配到web-inf路径下的资源)
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
Spring MVC——Filter
1、过滤器是Java web三大组件(Servlet,Filter、listener)之一
过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
过滤器一般完成一些通用的操作,比如:权限控制,统一编码处理,敏感字符处理、cors等等
1.1、基本步骤
1.2、Filter执行流程
- 从filterconfig中拿到filterchain,filterchain开启链路,流式执行过滤器的doFilter方法
- 方法中对request数据进行处理;
- 然后调用chain.dofilter()放行;
- chain.dofilter()下面的逻辑用来对response数据进行处理;
1.3、过滤器和拦截器
区别
1、拦截器是基于java的反射机制的,而过滤器是基于函数的回调。
2、拦截器依赖ioc容器,而过滤器依赖于servlet容器。
3、拦截器只处理action请求,而过滤器可以处理所有请求。
4、拦截器可以用ioc资源,如数据源、事务、依赖注入,而过滤器则做不到。
5、拦截器每次都会执行preHandle()、postHandle() 和afterCompletion(),而过滤器init 方法只能在容器初始化时被调用一次,过滤则通过doFilter() 处理。
6、调用顺序不同
过滤器在servlet处理前过滤请求,在servlet 处理后过滤响应。
拦截器在servlet 和controller 之间处理请求。
【注:】
如果拦截器preHandle()为true则允许在视图渲染前执行postHandle(),也允许在视图渲染后执行afterCompletion()
执行顺序
Filter 请求过滤 -> Interceptor 前置 -> controller -> Interceptor 后置 -> 视图渲染 -> AfterCompletion -> Filter 响应过滤
控制执行顺序
过滤器:@Order注解控制执行顺序,值越小级别越高越先执行。
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class MyFilter2 implements Filter {
拦截器:默认顺序是注册顺序,也可以通过Order手动设置,值越小越先执行。
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor2()).addPathPatterns("/**").order(2);
registry.addInterceptor(new MyInterceptor1()).addPathPatterns("/**").order(1);
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**").order(3);
}
使用场景
过滤器:编码格式、敏感字符、权限认证;
拦截器:异常处理(统一处理所有controller 的异常),日志记录(记录请求信息以便监控、信息统计、计算PV(Page View)等);
Spring MVC——listener
listener可以绑定触发事件,自动执行指定代码
1、servlet提供application,session,request三个对象创建、销毁或往其中添加修改删除属性时自动执行代码的功能组件。
监听器的分类:
2、使用步骤:

Spring MVC——Java web三大组件装配方式
推荐第2种作为Spring MVC的装配方式
1、web.xml配置servlet、filter、listener
2、注解,@WebFilter、@WebListener、@WebServlet,还要在启动类添加@ServletComponentScan,将组件实例注入到spring容器并导入ServletContext;
3、也可通过@Bean注册实例,如FilterRegistrationBean
@Bean
public FilterRegistrationBean<OrderFilter> orderFilter() {
FilterRegistrationBean<OrderFilter> filter = new FilterRegistrationBean<>();
filter.setName("reqFilter");
filter.setFilter(new ReqFilter());
// 指定优先级
filter.setOrder(-1);
return filter;
}
init-param 与context-param的区别是什么?
context-param和init-param都是在webapp/WEB-INF/web.xml文件中配置的参数。它们之间的区别在于作用范围和获取方式。
context-param是全局的参数,它对整个web应用程序有效。可以在web.xml的<context-param>标签中进行配置,并通过ServletContext对象的getInitParameter方法获取。
init-param是针对某个特定的Servlet或Filter的参数,它只在该Servlet或Filter的init方法初始化过程中有效。在web.xml中的<init-param>标签中进行配置后可以通过ServletConfig或FilterConfig对象的getInitParameter方法获取,例如:
<servlet>
<servlet-name>demo01</servlet-name>
<servlet-class>com.lanou3g.Demo01</servlet-class>
<init-param>
<param-name>username</param-name>
<param-value>张飞</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>demo01</servlet-name>
<url-pattern>/demo01</url-pattern>
</servlet-mapping>
public class Demo01 extends HttpServlet{
private ServletConfig config;
@Override
//当创建实例后该方法会被执行用来初始化,如果是Filter则该方法的参数是FilterConfig
public void init(ServletConfig config) throws ServletException {
this.config = config;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
传入参数 配置时的 username(相当于key)
用key获取对应的value
String value = this.config.getInitParameter("username");
System.out.println(value);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
以上,context-param适用于整个应用程序共享的参数;init-param适用于Servlet或Filter单独使用的参数。
AOP
定义
面向切面编程,通过动态代理对OOP实现增强。利用AOP可以实现功能复用,解耦合,提高开发效率。
组成
- Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
- Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
- Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
- Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
- Target(目标对象):织入 Advice 的目标对象。
- Proxy (代理对象):最终暴露外部被调用。
- Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程
基于IoC配置文件的应用
1、beans添加aop命名空间
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
2、引入依赖,由于引入的springwebmvc内部的aspectjweaver版本是1.8.13且optional为true,因此需要再手动引入一遍
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
3、定义下图几个类
【注:】MyAspect的arround方法,需要配置上执行参数



4、配置。IoC是按照子、父IoC的顺序加载Bean,因此建议在spring-mvc.xml下配置target、aspect和aop:config 实例
<!-- aop-->
<!-- 1、目标对象-->
<bean id="target" class="com.baizhiedu.basic.aop.Target"></bean>
<!-- 2、切面对象-->
<bean id="myAspect" class="com.baizhiedu.basic.aop.MyAspect"></bean>
<!-- 3、为切面对象织入通知,创建代理 -->
<aop:config>
<!-- 引入的切面实例实际里面存储的是增强方法,这里要对切面对象织入通知 -->
<aop:aspect ref="myAspect">
<!-- 4.1、前置增强 -->
<aop:before method="before" pointcut="execution(public void com.baizhiedu.basic.aop.Target.save())"></aop:before>
<!-- 4.2、异常通知 -->
<aop:after-throwing method="before" pointcut="execution(public void com.baizhiedu.basic.controller.FighterController.quick22())"></aop:after-throwing>
<!-- 4.3、后置增强 -->
<aop:after-returning method="before" pointcut="execution(public void com.baizhiedu.basic.controller.FighterController.quick22())"></aop:after-returning>
<!-- 4.4、后置强制增强,即异常仍然执行,相当于finally -->
<aop:after method="before" pointcut="execution(public void com.baizhiedu.basic.controller.FighterController.quick22())"></aop:after>
<!-- 4.5、环绕通知 -->
<aop:around method="around" pointcut="execution(public void com.baizhiedu.basic.controller.FighterController.quick22())"></aop:around>
<!-- pointcut格式:execution([修饰符] 返回类型 包名 类名 方法(参数))-->
<!-- 1、访问权限修饰符可省略,如:execution(void com.baizhiedu.basic.aop.Target.*(..))-->
<!-- 2、访问值类型、报名、类名、方法名可以使用*代表任意,例如:execution(* *.*.*()) -->
<!-- 3、包名和类名之间的一个点,代表当前包下的类,两个点..表示当前包及其子包,例如:execution(* *..*.*())-->
<!-- 4、方法参数可以使用..表示任意个数、任意类型的参数列表,如:execution(* *.*(..)) -->
</aop:aspect>
</aop:config>
5、如图,单元测试:执行target对象的切入点
6、结果

基于AOP注解的应用
Spring @Aspect、@Before、@After 注解实现 AOP 切面功能_aspect before-CSDN博客
1、定义下图的aspect和target。当前包为com.baizhiedu.basic.aop_anno


2、包扫描、实例化代理对象
<context:component-scan base-package="com.baizhiedu.basic.aop_anno" >
</context:component-scan>
<!-- 自动为spring容器中那些配置@aspectJ切面的bean织入切面,创建代理。-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
如此便完成了,更便捷
事务(TransactionDefinition)
事务的特性
- 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用(强调写操作的原子性),且线程安全。
- 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此事务的数据读取和写入要与其他事务隔离开来(不同隔离级别读写隔离策略不同,但是写写都必须用互斥锁策略隔离)。
- 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
- 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。
Spring事务的配置方式
Spring支持编程式事务管理以及声明式事务管理两种方式。
1. 编程式事务管理
编程式事务管理是侵入性事务管理,使用TransactionTemplate或者直接使用PlatformTransactionManager,对于编程式事务管理,Spring推荐使用TransactionTemplate。
2. 声明式事务管理
声明式事务管理建立在AOP之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。声明式事务属于无侵入式,不会影响业务逻辑的实现,只需要在配置文件中做相关的事务规则声明或者通过注解的方式即可。
事务的传播机制
事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,事务传播机制可以通过ThreadLocal管理事务哪些运行,哪些挂起。
常用的事务传播机制如下:
- PROPAGATION_REQUIRED
- Spring默认的传播机制,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。
- 如果外层没有事务,新建一个事务执行。
- 如果内层方法抛异常或者回滚,则事务会被标记回滚,外层方法也会回滚,即便外层方法吃掉异常后继续执行也得回滚。
- PROPAGATION_REQUES_NEW
该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可 - PROPAGATION_SUPPORT
如果外层有事务,则加入外层事务,如果外层没有事务,则不开事务(但是innodb会自动设置autocommit=true,并自动执行隐式事务) - PROPAGATION_NOT_SUPPORT
该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码 - PROPAGATION_NEVER
该传播机制不支持外层事务,即如果外层有事务就抛出异常 - PROPAGATION_MANDATORY
与NEVER相反,如果外层没有事务,则抛出异常 - PROPAGATION_NESTED
- 和REQUIRED类似,外部有就融入事务,没有就创建新事务。
- 不同点是设置了保存点,即内层方法回滚会保存到保存点(而不是像REQUIRED一旦内部方法回滚就直接给事务标识回滚),从而避免外面方法也跟着回滚。
- 保存点仅支持内层方法回滚后不干扰外层方法的事务继续,抛异常则和REQUIRED机制一样对事务标记回滚。
【注:】
哪怕 JDBC URL 里显式配置了 autocommit=true,PROPAGATION_NESTED 也不会变成多条独立的事务,因为 Spring 管理事务时,会把连接的自动提交关掉(setAutoCommit(false))。
PROPAGATION_SUPPORT这种传播机制外部有事务就融入,没有也不创建新事务。这里要清楚地意识到 “innodb一定开启事务后才能执行sql”,所谓的不创建新事务是指新connect不会再开启显式事务,新connect会autocommit=true并执行隐式事务,即每一条sql都是一个独立事务。
不管是NESTED、MANDATORY或者REQUIRED,一旦内部事务方法抛异常,就直接给事务标识回滚,外面事务方法都必回滚。
要避免使用try catch,例如@Transactional(propagation="PROPAGATION_REQUIRED")修饰methodA、methodB,然后methodA内部调用methodB,且methodA 通过try catch消化掉methodB抛出的异常后继续往下执行,到methodA执行结束时会触发TransactionException。

异常分析:methodB共享了methodA的事务,当methodB异常时就已经为事务设置了rollback的标识,因此methodA无法成功提交。
异常导致原因:methodB的异常被methodA捕获后吃掉了,methodA 继续正常执行,导致methodA触发提交时报错。
来看一下网上大神的翻译
PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 "内部" 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行.
另一方面, PROPAGATION_NESTED 开始一个 "嵌套的" 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交.
以下绘制一个表格来表现他们的差异
| 定义serviceA.methodA()以PROPAGATION_REQUIRED修饰; 定义serviceB.methodB()以表格中三种方式修饰; methodA中调用methodB | |||
|---|---|---|---|
| 异常状态 | PROPAGATION_REQUIRES_NEW (两个独立事务) | PROPAGATION_NESTED (B的事务嵌套在A的事务中) | PROPAGATION_REQUIRED (同一个事务) |
| methodA抛异常 methodB正常 | A回滚,B正常提交 | A与B一起回滚 | A与B一起回滚 |
| methodA正常 methodB抛异常 | 1.如果A中捕获B的异常,并没有继续向上抛异常,则B先回滚,A再正常提交; 2.如果A未捕获B的异常,默认则会将B的异常向上抛,则B先回滚,A再回滚 | B先回滚,A再正常提交 | A与B一起回滚 |
| methodA抛异常 methodB抛异常 | B先回滚,A再回滚 | A与B一起回滚 | A与B一起回滚 |
| methodA正常 methodB正常 | B先提交,A再提交 | A与B一起提交 | A与B一起提交 |
事务的传播原理
Spring采用Threadlocal 保证单个线程中的数据库操作使用的是同一个数据库连接,Mybatis逻辑同理。另一种用法就是事务,Spring的业务层使用事务时不需要管理connection对象,通过ThreadLocal实现了传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
事务的隔离级别
事务的隔离级别定义一个事务可能受其他并发务活动活动影响的程度,可以把事务的隔离级别想象为这个事务对于事物处理数据的自私程度。
在一个典型的应用程序中,多个事务同时运行,经常会为了完成他们的工作而操作同一个数据。并发虽然是必需的,但是会导致以下问题:
- 脏读(Dirty read)
脏读发生在一个事务读取了被另一个事务改写但尚未提交的数据时。如果这些改变在稍后被回滚了,那么第一个事务读取的数据就会是无效的。 - 不可重复读(Nonrepeatable read)
不可重复读发生在一个事务执行相同的查询两次或两次以上,但每次查询结果都不相同时。这通常是由于另一个并发事务在两次查询之间更新了数据。 - 幻读(Phantom reads)
幻读和不可重复读相似。当一个事务(T1)读取几行记录后,另一个并发事务(T2)插入了一些记录时,幻读就发生了。在后来的查询中,第一个事务(T1)就会发现一些原来没有的额外记录。
不可重复读重点在修改和删除,幻读重点在新增。
在理想状态下,事务之间将完全隔离,从而可以防止这些问题发生。然而,完全隔离会影响性能,因为隔离经常涉及到锁定在数据库中的记录(甚至有时是锁表)。完全隔离要求事务相互等待来完成工作,会阻碍并发。因此,可以根据业务场景选择不同的隔离级别。
| 隔离级别(具体原理了解mvcc) | 含义 |
|---|---|
| ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 |
| ISOLATION_READ_UNCOMMITTED | 特性:允许读取其它事务尚未提交的更改 缺点:可能导致脏读、幻读或不可重复读。 |
| ISOLATION_READ_COMMITTED | 特性:允许从已经提交的并发事务读取。(Oracle 默认级别) 缺点:可防止脏读,但幻读和不可重复读仍可能会发生。 |
| ISOLATION_REPEATABLE_READ | 特性:对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。(MYSQL默认级别) 缺点:可防止脏读和不可重复读,但幻读仍可能发生。 |
| ISOLATION_SERIALIZABLE | 特性:完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。 缺点:这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。 |
【补:】
- 未提交读(read uncommitted):可读取其它事务未提交的数据,但是数据rollback的话数据就是脏数据,其它事务读取这种数据的操作称为脏读;
- 提交读(read committed):可读取其它事务已提交的数据和自身事务未提交的数据
- 但如果事务A提交了更新数据,事务B在数据提交前后的查询结果不一样,这种情况称为不可重复读,即记录被修改;
- 如果事务A提交了增删数据,事务B在数据提交前后的查询数据量不一样,这种情况称为幻读,即记录量被增删;
- 可重复读(repeatable read):在无当前读的情况下实现了可重复读和幻读
- 创建readview的事务执行了当前读使得readview更新可能出现幻读,即记录量被增删;
- 同样当前读也可能导致不可重读,即记录被修改;
- 序列化读(serializable):强制性对事务排序,通过锁来维护数据安全实现的,没办法并行,高并发下锁竞争和超时会导致低效,看业务场景选择吧。
【注:】
- 1-3均为写写互斥;4不仅写写互斥,还读写互斥;
- MySQL相关命令
- 查看当前会话的隔离级别:select @@tx_isolation;
- 查看系统的隔离级别:select @@global.tx_isolation;
- 设置会话的隔离级别,隔离级别由低到高设置依次为:
- set session transaction isolation level read uncommitted;
- set session transaction isolation level read committed;
- set session transaction isolation level repeatable read;
- set session transaction isolation level serializable;
- 设置当前系统的隔离级别,隔离级别由低到高设置依次为:
- set global transaction isolation level read uncommitted;
- set global transaction isolation level read committed;
- set global transaction isolation level repeatable read;
- set global transaction isolation level serializable;
- session隔离级别优先级高,如果没有再选用系统隔离;MySQL默认的事务处理级别是'REPEATABLE-READ',而Oracle和SQL Server是READ_COMMITED
只读 @Transactional(readOnly=true)
如果一个事务只对数据库执行读操作,那么该数据库就可能利用那个事务的只读特性,采取某些优化措施。通过把一个事务声明为只读,可以给后端数据库一个机会来应用那些它认为合适的优化措施。由于只读的优化措施是在一个事务启动时由后端数据库实施的, 因此,只有对于那些具有可能启动新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、 ROPAGATION_NESTED)的方法来说,将事务声明为只读才有意义。
事务超时
为了使一个应用程序很好地执行,它的事务不能运行太长时间。因此,声明式事务的下一个特性就是它的超时。
假设事务的运行时间变得格外的长,由于事务可能涉及对数据库的锁定,所以长时间运行的事务会不必要地占用数据库资源。这时就可以声明一个事务在特定秒数后自动回滚,不必等它自己结束。
由于超时时钟在一个事务启动的时候开始的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明事务超时才有意义。
回滚规则
在默认设置下,事务只在出现运行时异常(runtime exception)时回滚,而在出现受检查异常(checked exception)时不回滚(这一行为和EJB中的回滚行为是一致的)。
不过,可以声明在出现特定受检查异常时像运行时异常一样回滚。同样,也可以声明一个事务在出现特定的异常时不回滚,即使特定的异常是运行时异常。
Spring编程式事务配置参考
-
开启注解事务,xml配置 <tx:annotation-driven />
-
@EnableTransactionManagement
Spring声明式事务配置参考
- 事务管理注解总开关:@EnableTransactionManagement,等同于xml配置方式的 <tx:annotation-driven />
- 事务的传播性:@Transactional(propagation=Propagation.REQUIRED)
- 事务的隔离级别:@Transactional(isolation = Isolation.READ_UNCOMMITTED)
【注:】
如果是springboot直接用@Transactional即可,因为@SpringBootApplication内置@EnableAutoConfiguration,@EnableAutoConfiguration又内置了@EnableTransactionManagement
读取未提交数据(会出现脏读, 不可重复读) 基本不使用
- 只读:
@Transactional(readOnly=true)
该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。 - 事务的超时性:
@Transactional(timeout=30) - 回滚:
指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)
指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。
@Transactional原理
1.声明@EnableTransactionManagement,创建BeanPostProcessor实例;
2.创建bean实例,在initializeBean阶段被此BeanPostProcessor实例执行后置方法,生成代理bean。代理类的相关代理方法逻辑如下:
@Transactional 方法调用
↓
事务拦截器 (TransactionInterceptor)
↓
事务管理器 (DataSourceTransactionManager,被ThreadLocal缓存后供mybatis相关方法使用)
↓
从 DataSource(连接池)获取 Connection
↓
将 Connection 绑定到当前线程
↓
执行 SQL 操作(MyBatis、JdbcTemplate 等从线程中获取同一 Connection)
↓
提交/回滚事务
↓
释放 Connection 回连接池
Mybatis——介绍
介绍
Mybatis 是一个开源的基于动态代理实现的半ORM 轻量级持久框架,核心部分仍然是JDBC,通过配置数据源、连接池实现connection 管理,开发时只需要关注SQL 语句,即prepareStatement 的使用
传统jdbc开发问题:
数据库连接创建,释放频繁,影响性能;
sql语句硬编码到程式,维护不便;
查询操作:需要手动把结果集中数据手动封装到实体;
增删改操作时,需要手动将实体数据设置到sql语句的占位符;
解决方案:使用数据库连接池初始化连接资源;
将sql语句抽象到xml文件中;
使用反射,内省等技术,自动将实体与表进行属性和字段的映射;
如何配置
Mybatis可以通过xml文件添加配置,通过xxxmapper.xml添加sql标签,也可以为xxxmapper接口添加sql注解,配置文件最后通过包扫描或者mapper路径匹配收集sql数据
优点
1、基于sql语句编程,灵活;2、持久层对外提供接口,隐藏JDBC,代码简洁;3、兼容多数据库;4、集成Spring;5、支持对象与字段的映射配置,维护方便;6、插件扩展
缺点
SQL语句依赖数据库,无法随意切换数据库
使用场景
对性能有调优需求,功能迭代快的产品
Mybatis——预编译
2.防止SQL注入,对所有的传参校验数据类型。
Mybatis——#{} 和${}
${}直接字符串替换,缺少检查机制,但简单高效;#{}则是预编译,先用占位符描述sql,然后校验传参是否合法,最后参数替换占位符,防止SQL注入。
在 MyBatis 中,#{value} 会被替换成 ?,然后 MyBatis 使用 PreparedStatement 来执行 SQL:
PreparedStatement ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?");
ps.setInt(1, 100); // 参数值在这里传入
ResultSet rs = ps.executeQuery();
Mybatis——引入依赖
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
transactionManagerment通过datasource去获取connect,datasource先从druid连接池获取空闲connect,没有再自建新connect,配置如下:
spring:
datasource:
druid:
url: jdbc:mysql://localhost:3306/your_db
username: root
password: your_password
# 连接池大小配置
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
# 连接检测与回收
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
# 开启监控和防火墙等
filters: stat,wall
# 监控页面配置
stat-view-servlet:
enabled: true
url-pattern: /druid/*
login-username: admin
login-password: 123456
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
Mybatis——JDBC回顾
下面是PreparedStatement实例操作sql(Statement实例操作略)
@Test
public void testPreparedStatement() {
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
// 连接数据库
connection = JDBCTools.getConnection();
// 使用占位符的SQl语句
String sql = "insert into customers(name,email,birth)"
+ "values(?,?,?)";
// 使用preparedStatement的setXxx方法设置每一个位置上的值
preparedStatement = connection.prepareStatement(sql);
// 设置name字段
preparedStatement.setString(1, "ATGUIGU");
// 设置email字段
preparedStatement.setString(2, "simale@163.com");
// 设置birth字段
preparedStatement.setDate(3,
new Date(new java.util.Date().getTime()));
// 执行更新操作
preparedStatement.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放资源
JDBCTools.release(null, preparedStatement, connection);
}
}
Mybatis——Mybatis案例
1、创建user表,类型是 int、varchar、varchar

2、创建一个项目,然后布局如下,如果你上面的依赖加载成功的话,这里应该不会报啥问题

3、实体类
public class User {
private Integer id;
private String name;
private String pwd;
public User(Integer id, String name, String pwd) {
this.id = id;
this.name = name;
this.pwd = pwd;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", pwd='" + pwd + '\'' +
'}';
}
}
4、UserMapper.xml
<?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="userMapper">
<!-- User类需要实现序列化接口和添加long类型的serialVersionUID字段 -->
<cache>
<property name="eviction" value="LRU" />
<property name="flushInterval" value="6000000" />
<property name="size" value="1024" />
<property name="readOnly" value="false" />
</cache>
<!-- 查询所有网站信息 -->
<select id="findAll" resultType="com.baizhiedu.basic.pojo.User">
select * from user
</select>
<!--插入操作-->
<insert id="save" parameterType="com.baizhiedu.basic.pojo.User">
insert into user values (#{id},#{name},#{pwd});
</insert>
<!-- 更新-->
<update id="update" parameterType="com.baizhiedu.basic.pojo.User">
update user set name = #{name} where id = 10
</update>
<!-- 删除-->
<delete id="delete" parameterType="java.lang.String">
delete from user where pwd = #{pwd}
</delete>
</mapper>
5、jdbc.properties
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://192.168.0.2:3306/mybatis
username=root
password=root
6、sqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 文件加载-->
<properties resource="jdbc.properties"></properties>
<!-- 全局开启二级缓存-->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 环境配置-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!-- 映射文件加载-->
<mappers>
<!-- 如果使用xml编辑SQL则使用相对路径,如下-->
<mapper resource="com/baizhiedu/mapper/UserMapper.xml"/>
<!-- 如果使用注解编辑SQL则使用包下扫描,如下-->
<package name="com.baizhiedu.mapper"/>
</mappers>
</configuration>
7、单元测试
@Slf4j
public class Test {
@org.junit.Test
public void mybatisQuickStart1() throws IOException {
// 加载核心配置文件
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 获取sqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取sqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 查询操作
selectAll(sqlSession);
}
public void selectAll(SqlSession sqlSession){
List list = sqlSession.selectList("userMapper.findAll");
Iterator iterator = list.iterator();
while (iterator.hasNext()){
log.debug(iterator.next().toString());
}
}
}
Mybatis——Mapper接口绑定
<mappers>
<package name="com.xxx.xxxMapper" />
</mappers>
2、XML绑定:在mybatis-config文件配置
<mappers>
<mapper location="classpath:/.../mapper.xml" />
</mappers>
Sql语句比较简单用注解绑定,SQL比较复杂用xml绑定,一般用xml绑定,建议统一方式,别一会儿注解一会儿xml。
5、XML实现接口绑定的格式要求
Mybatis——如何获取自动生成的(主)键值
Mybatis——动态SQL编程
介绍
3、动态 SQL 含有很多扩展,可以根据传参拼接成合适的SQL,重用性高;
mybatis动态SQL - trim where set标签 - 简书 (jianshu.com)
https://www.jianshu.com/p/d27f60937da9
Mybatis——批量插入
mapper方法:int addEmpsBatch(@param("emps") List<Employee> employees);
<insert id = "addEmpsBatch">
insert into emp(ename,gender,email,did)
values
<foreach collection="emps" item="emp" separator="," open="(" close=")">
(#{emp.ename},#{emp.gender},#{emp.email},#{emp.dept})
</ foreach>
</insert>
Mybatis——参数传递方式
public UserselectUser(String name,String area);
通过参数排序下标设置xml的SQL参数,#{0}代表接收的是 dao 层中的第一个参数,#{1}代表 dao 层中第二参数,以此类推。
<select id="selectUser" resultMap="BaseResultMap">
select * fromuser_user_t whereuser_name = #{0} anduser_area=#{1}...
</select>
2、使用 @param 注解指定别名
public interface usermapper {
user selectuser(@param(“user”) string userName);
}
在 xml 中使用别名作为单个参数
<select id=”selectuser” resulttype=”user”>
select * from some_table where username = #{user}
3、多个参数封装成 map
List<User> findByConditionByMap(HashMap<String, Object> map);
在 xml 中使用key作为单个参数,自动加载value
<select id="findByConditionByMap" parameterType="map" resultType="user">
select * from user where username like "%"#{username}"%" and sex=#{sex}
limit #{startIndex},#{pageSize}
</select>
Mybatis——ResultMap
resultMap 是一种“查询结果集---Bean对象”属性名称映射关系,使用resultMap关系可将查询结果集中的列映射到bean对象的各个属性,用于一对一、一对多映射
mybatis:resultType、resultMap(级联,association,association分步,collection,collection分步,延迟,discriminator)_yubin1285570923的博客-CSDN博客
https://blog.csdn.net/yubin1285570923/article/details/97553278?ops_request_misc=&request_id=&biz_id=102&utm_term=association%20resulttype&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-97553278.142%5Ev92%5EchatgptT3_1&spm=1018.2226.3001.4187 1、连接查询(在resultMap里面可以配置连接条件,利用association标签)
<!-- 订单查询关联用户的resultMap将整个查询的结果映射到cn.itcast.mybatis.po.Orders中 -->
<resultMap type="cn.itcast.mybatis.po.Orders" id="OrdersUserResultMap">
<!-- 配置映射的订单信息 -->
<!-- id:指定查询列中的唯 一标识,订单信息的中的唯 一标识,如果有多个列组成唯一标识,
配置多个id ,column:订单信息的唯 一标识列 ,property:订单信息的唯一标识列所映射到
Orders中哪个属性 -->
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="number" property="number"/>
<result column="createtime" property="createtime"/>
<result column="note" property=note/>
<!-- 配置映射的关联的用户信息 -->
<!-- association:用于映射关联查询单个对象的信息property:要将关联查询的用户信息映射
到Orders中哪个属性 -->
<association property="user" javaType="cn.itcast.mybatis.po.User">
<id column="user_id" property="id"/>
<result column="username" property="username"/>
<result column="sex" property="sex"/>
<result column="address" property="address"/>
</association>
</resultMap>
2、适用于表的一对多连接查询,(如,订单对应多个订单明细时,需要根据连接条件订单id匹配订单明细,并且消除重复的订单信息(订单明细中的),如下程序);
<resultMap type="cn.itcast.mybatis.po.Orders"
id="OrdersAndOrderDetailResultMap"
extends="OrdersUserResultMap">
<!-- 使用extends继承,不用再配置订单信息和用户信息的映射 -->
<!-- 订单明细信息一个订单关联查询出了多条明细,要使用collection进行映射
collection:对关联查询到多条记录映射到集合对象中
property:将关联查询到多条记录映射到cn.itcast.mybatis.po.Orders哪个属性
ofType:指定映射到list集合属性中pojo的类型
-->
<collection property="orderdetails" ofType="cn.itcast.mybatis.po.Orderdetail">
<id column="orderdetail_id" property="id"/>
<result column="items_id" property="itemsId"/>
<result column="items_num" property="itemsNum"/>
<result column="orders_id" property="ordersId"/>
</collection>
</resultMap>
Mybatis——懒/延迟加载
Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。
在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。
不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的
Mybatis——缓存
MyBatis 内置了一个强大的事务性查询缓存机制,包括一级缓存,二级缓存,它可以非常方便地配置和定制。一级缓存是sqlSession级别的缓存,二级缓存是Mapper 命名空间级别的缓存。
MyBatis 默认是开启一级缓存的,即同一个 sqlSession 每次查询都会先去缓存中查询,没有数据的话,再去数据库获取数据。它的默认配置就是启用二级缓存的,所以这个不用我们操心。因为Mybatis二级缓存是基于mapper级别的,所以还需要在各个Mapper 命名空间中进行进一步设置。
1、Spring Boot 开启二级缓存:首先yml中配置mybatis-plus.configuration.cache-enabled=true,然后sql语句存在于mapper.xml上时,在<mapper>中添加开启标签:<cache />
加上这个标签,二级缓存就会启用,它的默认属性如下:
- 映射语句文件中的所有 select 语句将会被缓存。
- 映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
- 缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
- 根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序来刷新。
- 缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
- 缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
2、可用的清除策略有:
- LRU – 最近最少使用:移除最长时间不被使用的对象。
- FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
- WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
- 默认的清除策略是 LRU。
3、如果sql语句用注解,则需要给接口添加@CacheNamespace
使用二级缓存时,sql语句的映射要么全xml格式表示,要么注解表示,二选一,不然可能会出现无法利用二级缓存或二级缓存没有及时清空的bug问题
如果命中缓存后会提示 Cache hit ratio ...
4、使用Redis管理二级缓存
4.1、配置插件如下:
@Slf4j
public class MybatisRedisCache implements Cache {
// 读写锁
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
//这里使用了redis缓存,使用springboot自动注入
private RedisTemplate<String, Object> redisTemplate;
private String id;
public MybatisRedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
if (redisTemplate == null) {
//由于启动期间注入失败,只能运行期间注入,这段代码可以删除
redisTemplate = (RedisTemplate<String, Object>) SpringUtil.getBean("redisTemplate");
}
if (value != null) {
redisTemplate.opsForValue().set(key.toString(), value);
redisTemplate.expire(key.toString(),1, TimeUnit.HOURS);
}
}
@Override
public Object getObject(Object key) {
if (redisTemplate == null) {
//由于启动期间注入失败,只能运行期间注入,这段代码可以删除
redisTemplate = (RedisTemplate<String, Object>) SpringUtil.getBean("redisTemplate");
}
try {
if (key != null) {
return redisTemplate.opsForValue().get(key.toString());
}
} catch (Exception e) {
e.printStackTrace();
log.error("缓存出错 ");
}
return null;
}
@Override
public Object removeObject(Object key) {
if (redisTemplate == null) {
//由于启动期间注入失败,只能运行期间注入,这段代码可以删除
redisTemplate = (RedisTemplate<String, Object>) SpringUtil.getBean("redisTemplate");
}
if (key != null) {
redisTemplate.delete(key.toString());
}
return null;
}
@Override
public void clear() {
log.debug("清空缓存");
if (redisTemplate == null) {
redisTemplate = (RedisTemplate<String, Object>) SpringUtil.getBean("redisTemplate");
}
Set<String> keys = redisTemplate.keys("*:" + this.id + "*");
if (!CollectionUtils.isEmpty(keys)) {
redisTemplate.delete(keys);
}
}
@Override
public int getSize() {
if (redisTemplate == null) {
//由于启动期间注入失败,只能运行期间注入,这段代码可以删除
redisTemplate = (RedisTemplate<String, Object>) SpringUtil.getBean("redisTemplate");
}
Long size = redisTemplate.execute((RedisCallback<Long>) RedisServerCommands::dbSize);
return size.intValue();
}
@Override
public ReadWriteLock getReadWriteLock() {
return this.readWriteLock;
}
}
4.2、添加属性给cache:
4.2.1、注解cache配置
@CacheNamespace(implementation = MybatisRedisCache.class)
public interface UserMapper(
@Select("select * from t_user where user_id = #{userId}")
@Options(useCache = true)
List<User> getUser(User u);
}
4.2.2、xml文件cache配置
<?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="cn.mybatis.UserMapper">
<cache type="cn.mybatis.MybatisRedisCache">
<property name="eviction" value="LRU" />
<property name="flushInterval" value="6000000" />
<property name="size" value="1024" />
<property name="readOnly" value="false" />
</cache>
<select id="selectById">
select * from test where id = #{id}
</select >
</mapper>
【注:】
- Mapper中的数据表类也需要实现序列化;
- Mybatis-Plus的BaseMapper方法使用@CacheNamespace,如果是自定义sql则按照上述3的要求配置
Mybatis——拦截器
介绍
Interceptor 接口提供三个方法,intercept、plugin和setProperties。
setProperties:自定义properties,SqlSessionFactoryBuilder.build("xxx.xml") 将文件配置加载后由XmlConfigBuilder 创建Configuration 实例,并属性填充,其中interceptorChain 属性的填充就是迭代所有interceptors 完成实例化并执行setProperties(Properties ps) 方法;
plugin:在创建Executor、ParameterHandler、ResultSetHandler和StatementHandler时会通过InterceptorChain做迭代,即判断拦截器对当前实例有无拦截点设置,如果有则生成JDK动态代理对象并返回。这个功能封装就是plugin方法,官方为plugin提供了通用处理方法,直接重写到自定义的plugin 方法即可:return Plugin.wrap(target, this);
intercept:用于拦截指定方法并通过传参增强功能,如分页查询,日期填充,id策略;
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
四大拦截对象
只能拦截 Executor、ParameterHandler、ResultSetHandler和StatementHandler,基本上都是拦截Executor

- Executor 是 Mybatis的内部执行器,它负责调用StatementHandler 操作数据库;
- StatementHandler 是Mybatis 封装JDBC 的对象,整合调用ParameterHandler、ResultSetHandler、boundsql、mappedstatement等资源;
- ParameterHandler 是Mybatis 实现Sql 入参设置的对象,插件可以改变Sql的参数设置;
- ResultSetHandler 是Mybatis 把ResultSet 集合映射成POJO的对象;
@Signature
用于指定拦截点的接口、方法和方法参数
<-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
---------------------------------------------------------------
// ExamplePlugin.java
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed();
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public void setProperties(Properties properties) {
}
}
Mybatis——Mybatis分页原理
Mybatis——执行原理
1、加载配置:配置来源于两个地方,一处是配置文件,一处是Java代码的注解,将SQL的配置信息加载成为一个个MappedStatement对象(包括了传入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。
2、SQL解析:当API接口层接收到请求时,会接收到传入SQL的ID和传入对象(可以是Map、JavaBean或者基本数据类型),Mybatis会根据ID从Configuration.mappedStatements找到对应的MappedStatement,然后根据传入参数对象对MappedStatement进行解析,解析后可以得到最终要执行的SQL。
3、SQL执行:将最终得到的SQL拿到数据库进行执行,得到操作数据库的结果。
4、结果映射:将操作数据库的结果按照映射的配置进行转换,可以转换成HashMap、JavaBean或者基本数据类型,并将最终结果返回

Mybatis——Executor-type介绍
mybatis提供三种sql执行器,分别是SIMPLE、REUSE、BATCH。
1、SIMPLE是默认执行器,根据对应的sql直接执行,不会做一些额外的操作。
2、REUSE是可重用执行器,重用对象是Statement(即该执行器会缓存同一个sql的Statement,省去Statement的重新创建,优化性能)(即会重用预处理语句)
3、BATCH执行器会重用预处理语句,并执行批量更新。
Mybatis——Executor-type执行效果
1、SIMPLE、REUSE:可通过insert、update、delete方法的返回值判断sql是否执行成功,返回非0表示执行sql成功的条数,返回0表示sql执行失败
2、BATCH:insert、update、delete方法返回值一直会是负数-2147482646,在该模式下insert、update、delete返回值将无任何意义,不能作为判断sql执行成功的判断依据
Mybatis——全流程描述
加载xml配置文件到内存,执行XmlConfigBuilder创建Configuration实例,然后解析配置信息填充Configuration实例,比如常见的interceptorChain,mapperedStatements,mapperRegistry都在这一时期完成配置
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build("...xml");
创建Executor,无参构造默认是SimpleExecutor,拦截器在这一时期基于责任链模式和JDK动态代理创建Executor代理对象并返回
sqlSession sq = sqlSessionFactory.openSession();
基于JDK动态代理生成mapper实例
xxxMapper mapper = sq.getMapper(xxx.class);
当调用接口方法时,获取sqlSession对象,sqlSession 根据接口全限名+方法名拼接字符串作为key值,从mappedStatements定位mappedStatement。然后executor以传参和mappedStatement为参数执行自身方法,将结果返回。
mapper.query(...);
SpringBoot
1. SpringBoot基础
1.1 什么是SpringBoot?
- 简化Spring应用开发,实现了约定大于配置
- 创建独立的Spring引用程序main方法运行
- 嵌入的tomcat无需部署war文件
- 简化maven配置
- 自动配置
1.2 SpringBoot有哪些优点?
- 独立运行
Spring Boot 而且内嵌了各种 servlet 容器,Tomcat、Jetty 等,现在不再需要打成war 包部署到容器中,Spring Boot 只要打成一个可执行的 jar 包就能独立运行,所有的依赖包都在一个 jar 包内。 - 简化配置
spring-boot-starter-web 启动器自动依赖其他组件,简少了 maven 的配置。 - 自动配置
Spring Boot 能根据当前类路径下的类、jar 包来自动配置 bean,如添加一个 spring-boot-starter-web 启动器就能拥有 web 的功能,无需其他配置。 - 无代码生成和XML配置
Spring Boot 配置过程中无代码生成,也无需 XML 配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是 Spring4.x 的核心功能之一。 - 避免大量的Maven导入和各种版本冲突
- 应用监控
Spring Boot 提供一系列端点可以监控服务及应用,做健康检测。
1.3 SpringBoot的核心注解是什么?由那些注解组成?
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
主要组合包含了以下 3 个注解:
- @SpringBootConfiguration:
组合了 @Configuration 注解,表示启动类是一个配置类。 - @EnableAutoConfiguration:
向Spring容器导入了一个Selector,用来加载classpath下SpringFactories中所定义的自动配置类,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 - @ComponentScan:
Spring组件扫描,可以指定多个扫描路径,如@ComponentScan({"com.sx.srb"}),默认是扫描启动类所在路径。
1.4 什么是JavaConfig?
Spring JavaConfig 是 Spring 社区的产品,它提供了配置 Spring IoC 容器的纯Java 方法。因此它有助于避免使用 XML 配置。使用 JavaConfig 的优点在于:
- 面向对象的配置。
由于配置被定义为 JavaConfig 中的类,因此用户可以充分利用 Java 中的面向对象功能。一个配置类可以继承另一个,重写它的@Bean 方法等。 - 减少或消除 XML 配置。
基于依赖注入原则的外化配置的好处已被证明。但是,许多开发人员不希望在 XML 和 Java 之间来回切换。JavaConfig 为开发人员提供了一种纯 Java 方法来配置与 XML 配置概念相似的 Spring 容器。从技术角度来讲,只使用 JavaConfig 配置类来配置容器是可行的,但实际上很多人认为将JavaConfig 与 XML 混合匹配是理想的。 - 类型安全和重构友好。
JavaConfig 提供了一种类型安全的方法来配置 Spring容器。由于 Java 5.0 对泛型的支持,现在可以按类型而不是按名称检索 bean,不需要任何强制转换或基于字符串的查找。
1.5 SpringBoot自动配置原理是什么?
- SpringBoot启动会加载大量的自动配置类
- 看需要的功能有没有在SpringBoot默认写好的自动配置类当中;
- 再看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)
- @EnableAutoConfiguration选择路径是各个jar包的resource/meta-inf/spring.factories,只需在该文件指定配置类全名,开发项目就会基于反射和yml配置实例化。
// 路径为resource/meta-inf/spring.factories org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.sg.config.SwaggerConfig
【注:】自动配置是Spring Boot将依赖包的类型bean实例化并加载到IOC的一种SPI(Service Provider Interface,即框架开发者根据自身目的约定一种合适的协议,服务商遵从该协议达到功能实现和应用),协议形式任意、目的任意:
例如类接口实现,com.mysql.cj.jdbc.Driver实现了java.sql.Driver,目的是通过接口管理和操作具体的数据库;
例如Spring Boot的自动配置,依赖包在resource/meta-inf/spring.factories中声明了配置类的全限定名,目的是框架启动时将依赖包中类型bean实例化并加载到IOC。
1.6 你如何理解SpringBoot配置加载顺序?
1. 开发者工具 `Devtools` 全局配置参数;
2. 单元测试上的 `@TestPropertySource` 注解指定的参数;
3. 单元测试上的 `@SpringBootTest` 注解指定的参数;
4. 命令行指定的参数,如 `java -jar springboot.jar --name="码霸霸"`;
5. 命令行中的 `SPRING_APPLICATION_JSONJSON` 指定参数, 如 `java -Dspring.application.json='{"name":"码霸霸"}' -jar springboot.jar`;
6. `ServletConfig` 初始化参数;
7. `ServletContext` 初始化参数;
8. JNDI参数(如 `java:comp/env/spring.application.json`);
9. Java系统参数(来源:`System.getProperties()`);
10、操作系统环境变量参数;
11、`RandomValuePropertySource` 随机数,仅匹配:`ramdom.*`;
12、JAR包外面的配置文件参数(`application-{profile}.properties(YAML)`);
13、JAR包里面的配置文件参数(`application-{profile}.properties(YAML)`);
14、JAR包外面的配置文件参数(`application.properties(YAML)`);
15、JAR包里面的配置文件参数(`application.properties(YAML)`);
16、`@Configuration`配置文件上 `@PropertySource` 注解加载的参数;
17、默认参数(通过 `SpringApplication.setDefaultProperties` 指定);
1.7 运行 SpringBoot 有哪几种方式?
- 打包用命令或者放到容器中运行
- 用 Maven/ Gradle 插件运行
- 直接执行 main 方法运行
1.8 SpringBoot 需要独立的容器运行吗?
不需要,内置了 Tomcat/ Jetty 等容器。
1.9 开启SpringBoot 特性有哪几种方式?
- 继承spring-boot-starter-parent项目
- 导入spring-boot-dependencies项目依赖
1.10 SpringBoot、Spring MVC和Spring有什么区别?
- Spring
Spring最重要的特征是依赖注入。所有Spring Modules不是依赖注入就是IOC控制反转。
当我们恰当的使用DI或者是IOC的时候,可以开发松耦合应用。 - Spring MVC
Spring MVC提供了一种分离式的方法来开发Web应用。通过运用像DispatcherServelet,MoudlAndView 和 ViewResolver 等一些简单的概念,开发 Web 应用将会变的非常简单。 - SpringBoot
Spring和Spring MVC的问题在于需要配置大量的参数。
SpringBoot通过一个自动配置和启动的项来解决这个问题。
1.11 SpringBoot启动时都做了什么?
- SpringBoot在启动的时候从各个依赖jar包的类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值
- 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
- 整个J2EE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中;
- 它会给容器中导入非常多的自动配置类(xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并配置好这些组件;
- 有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;
2. SpringBoot配置
2.1 什么是YAML?
YAML 是一种配置文件格式。在配置文件中添加复杂的配置可以展示出非常结构化的效果。可以直观看出 YAML 的各层配置数据。
2.2 YAML 配置的优势在哪里 ?
-
- 配置有序
- 支持数组,数组中的元素可以是基本数据类型也可以是对象
- 格式间接,层次分明
相比 properties 配置文件,YAML 还有一个缺点,就是不支持 @PropertySource 注解导入自定义的 YAML 配置。
2.3 SpringBoot 是否可以使用 XML 配置 ?
Spring Boot 推荐使用 Java 配置而非 XML 配置,但是 Spring Boot 中也可以使用 XML 配置,通过 @ImportResource 注解可以引入一个 XML 配置。
2.4 SpringBoot核心配置文件是什么?
bootstrap.properties和application.properties
2.5 bootstrap.properties和application.properties 有何区别 ?
SpringBoot两个核心的配置文件:
- bootstrap(.yml 或者 .properties):boostrap 由父 ApplicationContext 加载的,比applicaton优先加载,配置在应用程序上下文的引导阶段生效。一般来说我们在 SpringCloud Config 或者Nacos中会用到它。且boostrap里面的属性不能被覆盖;
- application (.yml或者.properties):由ApplicatonContext 加载,用于 SpringBoot项目的自动化配置。
2.6 什么是Spring Profiles?
主要用来区分环境;
Spring Profiles 允许用户根据配置文件(dev,test,prod 等)来注册 bean。因此,当应用程序在开发中运行时,只有某些 bean 可以加载,而在 PRODUCTION中,某些其他 bean 可以加载。假设我们的要求是 Swagger 文档仅适用于 QA 环境,并且禁用所有其他文档。这可以使用配置文件来完成。Spring Boot 使得使用配置文件非常简单。
2.7 如何在自定义端口上运行SpringBoot应用程序?
SpringBoot默认监听的是8080端口;为了在自定义端口上运行 SpringBoot 应用程序,您可以在application.properties 中通过
server.port = 8888
指定端口;这样就可以将监听的端口修改为8888。
3. SpringBoot 安全性
3.1 如何实现SpringBoot应用程序的安全性?
为了实现SpringBoot的安全性,使用spring-boot-starter-security依赖项,必须添加安全配置。配置类需要扩展WebSecurityConfigurerAdapter类并覆盖其方法。
3.2 Spring Security 和Shiro各自的优缺点 ?
Shiro和Spring Security相比,主要有如下一些特点:
- Spring Security 是一个重量级的安全管理框架;Shiro 则是一个轻量级的安全管理框架
- Spring Security 概念复杂,配置繁琐;Shiro 概念简单、配置简单
- Spring Security 功能强大,支持单机、分布式、oauth;Shiro 相对简单,可以做认证、授权和session管理
3.3 SpringBoot 如何解决跨域问题 ?
跨域可以在前端通过 JSONP 来解决,但是 JSONP 只可以发送 GET 请求,无法发送其他请求,因此我们推荐在后端通过 (CORS,Cross-origin resource sharing) 来解决跨域问题。在传统的 SSM 框架中,都是通过 CORS 来解决跨域问题,只不过之前我们是在 XML 文件中配置 CORS ,现在可以通过实现WebMvcConfigurer接口然后重写addCorsMappings方法解决跨域问题。
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3600);
}
}
项目中前后端分离部署,所以需要解决跨域的问题。
我们使用cookie存放用户登录的信息,在spring拦截器进行权限控制,当权限不符合时,直接返回给用户固定的json结果。
当用户登录以后,正常使用;当用户退出登录状态时或者token过期时,由于拦截器和跨域的顺序有问题,出现了跨域的现象。
我们知道一个http请求,先走filter,到达servlet后才进行拦截器的处理,如果我们把cors放在filter里,就可以优先于权限拦截器执行。
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}
3.4 什么是 CSRF 攻击?
CSRF 代表跨站请求伪造。这是一种攻击,迫使最终用户在当前通过身份验证的Web 应用程序上执行不需要的操作。CSRF 攻击专门针对状态改变请求,而不是数据窃取,因为攻击者无法查看对伪造请求的响应。
3.5 SpringBoot 中的监视器是什么
Spring boot actuator 可访问生产环境中正在运行的应用状态;
比如有几个指标必须在生产环境中进行检查和监控,监视器可以指定管理端口,并公开监控指标,管理者可直接访问端点检查状态;
默认公开的只有health和info,其余均需配置公开。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
server.tomcat.uri-encoding=UTF-8
# 程序运行端口
server.port=8888
# 监视程序运行端口
management.server.port=8090
# 激活所有的内置Endpoints
management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.exclude=env,beans
# 开启shutdown这个endpoint
management.endpoint.shutdown.enabled=true
3.6 如何在SpringBoot中禁用Actuator端点安全性?
默认情况下,所有敏感的HTTP端点都是安全的,只有具有ACTUATOR角色的用户才能访问它们。可以用 management.security.enabled=false 来禁用安全性。当执行端点在防火墙后时,才建议禁用安全性。
3.7 如何监视所有SpringBoot微服务?
SpringBoot提供监视器端点以监控各个微服务的度量。这些端点对于查看应用的状态是否正常运行很有帮助。但是,使用监视器的一个主要缺点或困难是,我们必须单独打开应用端点以了解其状态或健康状况。想象一下涉及 50 个应用程序的微服务,管理员将不得不击中所有 50 个应用程序的执行终端。为了处理这种情况,引入了spring boot admin。 它建立在 Spring Boot Actuator 之上,提供了一个 Web UI,使我们能够可视化多个应用程序的度量。
4. SpringBoot进阶
4.1 如何让 Spring Boot 自动将Controller的日期字符串入参转成 Date 或
LocalDateTime?
-
@DateTimeFormat:由 Spring 框架提供,专门用于处理来自 Query String、表单数据、路径变量的字符串参数。 -
@JsonFormat:由 Jackson 库提供,用于处理 JSON 请求体 (@RequestBody) 的序列化和反序列化
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
public class User {
// 处理表单或URL参数:birthday=2024-05-20 14:30:00
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
// 处理JSON请求体:{"birthday":"2024-05-20 14:30:00"}
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date birthday;
// ... 省略 getter/setter
}
spring mvc 则需要手动配置
package com.baizhiedu.basic.converter;
import org.springframework.core.convert.converter.Converter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @Author: robert.song
* @Date: Created in 2021/9/6
*/
public class DateConverter implements Converter<String, Date> {
@Override
public Date convert(String s) {
// 转换日期格式
System.out.println("convertor");
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
Date date = null;
try {
date = format.parse(s);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}
<bean id="dateConverter" class="com.baizhiedu.basic.converter.DateConverter"></bean>
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<ref bean="dateConverter"/>
</set>
</property>
</bean>
<mvc:annotation-driven conversion-service="conversionService"/>
↓
影响 RequestMappingHandlerAdapter 的初始化
↓
创建 WebDataBinder 时设置其 conversionService
↓
参数绑定时通过 WebDataBinder 使用 ConversionService 进行类型转换,所有参数解析器(HandlerMethodArgumentResolver)都通过 WebDataBinder 获取所需转换方案
4.1 spring boot 需要配置jsp引擎吗
需要,虽然其自带tomcat,但是属于简单版本,需要额外配置。
<!-- Spring Boot Web 启动器,包含 spring-boot-starter-tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JSP 引擎,用于解析 .jsp 文件 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!-- JSTL 标签库支持 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
这和spring mvc不同,spring mvc需要单独放到tomcat中部署,这种场景下tomcat的功能未削减因而不需要配置,仅需要一些api依赖配置供开发场景使用。
<!-- Spring MVC 核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.25</version>
</dependency>
<!-- Servlet API,由Servlet容器提供,打包时无需包含 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- JSP API,由Servlet容器提供,打包时无需包含 -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
<!-- JSTL 支持,在JSP页面中编写Java代码时需要使用 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
4.1 什么是 WebSockets?
WebSocket是一种计算机通信协议,通过单个TCP连接提供全双工通信信道。
- WebSocket是双向的 -使用 WebSocket 客户端或服务器可以发起消息发送。
- WebSocket是全双工的 -客户端和服务器通信是相互独立的。
- 单个TCP连接 -初始连接使用 HTTP,然后将此连接升级到基于套接字的连接。然后这个单一连接用于所有未来的通信
- 与http相比,WebSocket消息数据交换要轻得多。
4.2 什么是 FreeMarker 模板?
FreeMarker 是 Java WEB模板引擎,主要优点是表示层和业务层的完全分离。程序员可以处理应用程序代码,而设计人员可以处理 html 页面设计。最后使用freemarker 可以将这些结合起来,给出最终的输出页面。
4.3 Swagger用过麽?他用来做什么?
Swagger广泛用于可视化API,使用SwaggerUl为前端开发人员提供在线沙箱。Swagger 是用于生成RESTful Web服务的可视化表示的工具,规范和完整框架实现。它使文档能够以与服务器相同的速度更新。当通过Swagger 正确定义时,消费者可以使用最少量的实现逻辑来理解远程服务并与其进行交互。
4.4 前后端分离,如何维护接口文档 ?
大部分情况下,我们都是通过 Spring Boot 做前后端分离开发,前后端分离一定会有接口文档。传统的方法就是使用 word 或者 md 来维护接口文档,但是效率太低,接口一变,所有人手上的文档都得变。在 Spring Boot 中,这个问题常见的解决方案是 Swagger ,使用 Swagger 我们可以快速生成一个接口文档网站,接口一旦发生变化,文档就会自动更新,所有开发工程师访问这一个在线网站就可以获取到最新的接口文档,非常方便。
4.5 SpringBoot项目如何热部署?
这可以使用 DEV 工具来实现。通过这种依赖关系,您可以节省任何更改,嵌入式tomcat 将重新启动。Spring Boot 有一个开发工具(DevTools)模块,它有助于提高开发人员的生产力。Java 开发人员面临的一个主要挑战是将文件更改自动部署到服务器并自动重启服务器。开发人员可以重新加载 Spring Boot 上的更改,而无需重新启动服务器。这将消除每次手动部署更改的需要。Spring Boot 在发布它的第一个版本时没有这个功能。这是开发人员最需要的功能。DevTools 模块完全满足开发人员的需求。该模块将在生产环境中被禁用。它还提供 H2 数据库控制台以更好地测试应用程序。
org.springframework.boot
spring-boot-devtools
4.6 SpringBoot 中的starter到底是什么 ?
SpringBootStarter是基于 Spring 已有功能来实现的。SpringBootStarter的核心注解是自动配置注解(@EnableAutoConfiguration,属SpringBoot注解),在这个配置类中通过条件注解来决定一个配置是否生效(@Conditional系列,均属Spring注解),允许开发者自定义Starter,新注入的属性会代替掉默认属性。开发者只需要引入依赖就可以完成依赖配置。
4.7 自定义一个Starter
(102条消息) 如何自定义starter_田维常的博客-CSDN博客
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.0.0.RELEASE</version>
<optional>true</optional>
</dependency>
</dependencies>
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.tian")
public class TianProperties {
// 姓名
private String name;
// 年龄
private int age;
// 性别
private String sex = "M";
//get and set
}
public class TianService {
private TianProperties properties;
public TianService() {
}
public TianService(TianProperties userProperties) {
this.properties = userProperties;
}
public void sayHello(){
System.out.println("hi, 我叫: " + properties.getName() +
", 今年" + properties.getAge() + "岁"
+ ", 性别: " + properties.getSex());
}
}
@Configuration
@EnableConfigurationProperties(TianProperties.class)
@ConditionalOnClass(TianService.class)
@ConditionalOnProperty(prefix = "spring.tian", value = "enabled", matchIfMissing = true)
public class TianServiceAutoConfiguration {
@Autowired
private TianProperties properties;
@Bean
@ConditionalOnMissingBean(TianService.class)
public TianService tianService() {
return new TianService(properties);
}
}
resource/meta-inf/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tian.TianServiceAutoConfiguration
然后做成jar包,名称就可以叫做 spring-boot-tian-starter.jar
然后创建一个Spring Boot项目test
在项目中把自定义starter添加pom依赖
<dependency>
<groupId>com.tian</groupId>
<artifactId>spring-boot-tian-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
TestApplication启动类
@SpringBootApplication
@EnableEurekaServer
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
application.properties中配置
spring.tian.name=tian
spring.tian.age=22
spring.tian.sex=M
写一个TestController.java类
RestController
@RequestMapping("/my")
public class TestController {
@Resource
private TianService tianService;
@PostMapping("/starter")
public Object starter() {
tianService.sayHello();
return "ok";
}
}
最后启动项目,输入http://localhost:9091/my/starter,后台打印
hi, 我叫: tian, 今年22岁, 性别: M
这就成功的现实了自定义的starter。
4.8 spring-boot-starter-parent 有什么用?
新创建一个 SpringBoot 项目,默认都是有 parent 的,这个 parent 就是 spring-boot-starter-parent ,spring-boot-starter-parent 主要有如下作用:
- 定义了 Java 编译版本为 1.8 。
- 使用 UTF-8 格式编码。
- 继承自 spring-boot-dependencies,这个里边定义了依赖的版本,也正是因为继承了这个依赖,所以我们在写依赖时才不需要写版本号。
- 执行打包操作的配置。
- 自动化的资源过滤。
- 自动化的插件配置。
- 针对 application.properties 和 application.yml 的资源过滤,包括通过 profile 定义的不同环境的配置文件,例如 application-dev.properties 和 application-dev.yml。
4.9 SpringBoot 打成的jar和普通的jar有什么区别 ?
SpringBoot 项目最终打包成的 jar 是可执行 jar ,这种 jar 可以直接通过 java -jar xxx.jar 命令运行,不可以作为普通的jar 被其他项目依赖,即使依赖了也无法使用。
SpringBoot 的 jar 无法被其他项目依赖,主要还是他和普通 jar 的结构不同。普通的 jar 包,解压后直接就是包名,包里就是我们的代码,而 Spring Boot 打包成的可执行 jar 解压后,在 \BOOT-INF\classes 目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加build配置,将 Spring Boot 项目打成两个 jar,一个可执行,一个可引用。
4.10 如何使用SpringBoot实现异常处理?
Spring 提供了@RestControllerAdvice 处理异常的非常有用的方法。 我们通过实现一个 ControlerAdvice 类,通过@ExceptionHandler注解来拦截控制器类抛出的所有异常。
4.11 微服务中如何实现 session 共享?
在微服务中,一个完整的项目被拆分成多个不相同的独立的服务,各个服务独立部署在不同的服务器上,各自的 session 被从物理空间上隔离开了,但是经常,我们需要在不同微服务之间共享 session ,常见的方案就是 Spring Session + Redis 来实现 session 共享。将所有微服务的session 统一保存在 Redis 上,当各个微服务对 session 有相关的读写操作时,都去操作 Redis 上的 session 。这样就实现了 session 共享,Spring Session 基于 Spring 中的代理过滤器实现,使得 session 的同步操作对开发人员而言是透明的,非常简便。
4.12 SpringBoot 中如何实现定时任务?
SpringBoot 中对于定时任务的支持主要还是来自 Spring 框架。
在 SpringBoot 中使用定时任务主要有两种不同的方式,一个就是使用 Spring 中的 @Scheduled 注解,另一个则是使用第三方分布式框架 XXL-JOB、Quartz等。
- 使用Spring中的 @Scheduled的方式主要通过@Scheduled注解来实现。
- 使用Quartz,则按照Quartz的方式,定义Job和Trigger即可。
4.13 异步使用
@Async来声明调用的线程池
方式一:
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import java.util.concurrent.Future;
@Service
public class MyAsyncService {
@Async("customExecutor")
public Future<String> asyncTaskWithResult() {
// 模拟耗时操作
try { Thread.sleep(2000); } catch (InterruptedException e) {}
String result = "任务执行结果:" + System.currentTimeMillis();
// 通过 AsyncResult 包装结果
return new AsyncResult<>(result);
}
}
调用
@RestController
public class MyController {
@Autowired
private MyAsyncService myAsyncService;
@GetMapping("/test-future")
public String testFuture() throws Exception {
Future<String> future = myAsyncService.asyncTaskWithResult();
// 可以在此做其他事情...
String result = future.get(); // 阻塞直到异步任务完成
return "异步结果:" + result;
}
}
提示:
future.get()会阻塞当前线程,如果不想阻塞,可以使用CompletableFuture并添加回调(如thenApply、whenComplete)。
方式二:
@Autowired
private Executor executor;
@Async("customExecutor")
public CompletableFuture<String> asyncWithCompletableFuture() {
return CompletableFuture.supplyAsync(() -> {
// 模拟耗时计算
try { Thread.sleep(2000); } catch (InterruptedException e) {}
return "异步计算结果:" + System.currentTimeMillis();
}, executor); // 使用同一个线程池,或由@Async自动提供
}
调用
@RestController
public class MyController {
@Autowired
private MyAsyncService myAsyncService;
@GetMapping("/async-nonblocking")
public String nonBlockingDemo() {
CompletableFuture<String> future = myAsyncService.asyncWithCompletableFuture();
// 注册回调:任务完成后会异步执行这段代码,不会阻塞当前线程
future.thenAccept(result -> {
System.out.println("收到异步结果(非阻塞): " + result);
// 这里可以继续处理结果,比如保存到数据库、发送WebSocket推送等
});
// 主线程立即返回,不会等待异步结果
return "异步任务已提交,结果会在后台处理";
}
}
@Async使用注意事项
| 注意点 | 说明 | 解决方案 |
|---|---|---|
| 1. 必须自定义线程池 | 默认 SimpleAsyncTaskExecutor 每次调用创建新线程,高并发下极易 OOM。 | 按业务配置 ThreadPoolTaskExecutor,并为每个异步方法指定线程池名称(如 @Async("customExecutor"))。 |
2. 别忘了 @EnableAsync | 不添加该注解,@Async 注解会被忽略,方法同步执行。 | 在 @Configuration 类或启动类上加上 @EnableAsync。 |
| 3. 自调用问题 | 同一个类内的方法调用(this.method())不会走代理,异步失效。 | 将异步方法移到独立的 @Service/@Component 中;或通过 AopContext.currentProxy() 获取代理对象再调用。 |
| 4. 异常处理 | 异步方法中抛出未捕获异常不会抛给调用方(除非用 Future)。 | - 对于 void 方法:实现 AsyncUnconfigurer 并重写 getAsyncUncaughtExceptionHandler()。- 对于 Future 方法:调用 future.get() 时捕获 ExecutionException。 |
| 5. 事务上下文失效 | @Async 与 @Transactional 一起使用时,事务不起作用(因为执行线程不同,ThreadLocal 不共享)。 | 异步方法内手动管理事务,或拆分同步事务部分 + 异步非事务部分。 |
| 6. 请求/安全上下文丢失 | @Async 方法默认运行在另一个线程,无法获取原请求的 RequestContextHolder 或 SecurityContext。 | 需要显式传递上下文(例如通过方法参数传递 HttpServletRequest 或认证信息)。 |
| 7. 优雅关闭 | 应用关闭时可能丢失正在执行的任务。 | 配置线程池时设置:executor.setWaitForTasksToCompleteOnShutdown(true);executor.setAwaitTerminationSeconds(60); |
| 8. 默认线程池名称混乱 | 如果不指定线程池名称,默认名为 simpleAsyncTaskExecutor,日志不便排查。 | 始终用 @Async("yourBeanName") 明确指定自定义线程池。 |
更多推荐



所有评论(0)