Spring

Bean

IoC

控制反转(Inversion of Control),是 Spring 框架的核心之一

作用

  • 管理对象的创建和生命周期:对象的创建和管理交由 Spring 容器 来完成,开发者只需要配置好依赖关系
  • 解耦:不需要主动去依赖某个实现类,而是依赖于接口。具体的实现由容器在运行时依赖注入(Dependency Injection)

Bean的加载方式

1.<bean/>标签

通过xml配置来注入bean

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--xml方式声明自己开发的bean-->
<bean id="cat" class="com.xw.bean.Cat"/>
<bean class="com.xw.bean.Dog"/>

<!--xml方式声明第三方开发的bean-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"/>
<bean class="com.alibaba.druid.pool.DruidDataSource"/>
<bean class="com.alibaba.druid.pool.DruidDataSource"/>
</beans>

获取bean

1
2
3
4
5
6
7
8
9
public class App {
public static void main(String[] args) {
ApplicationContext app = new ClassPathXmlApplicationContext("spring-config.xml");
String[] names = app.getBeanDefinitionNames(); // 获取容器中所有bean的名称
for (String name : names) {
System.out.println(name);
}
}
}

结果

1
2
3
4
5
cat
com.xw.bean.Dog#0
dataSource
com.alibaba.druid.pool.DruidDataSource#0
com.alibaba.druid.pool.DruidDataSource#1

2.注解定义bean

可以使用的注解有@Component以及三个衍生注解@Service、@Controller、@Repository。

1
2
3
@Component("jerry")	//可以给bean起个名字
public class Mouse {
}

加载第三方bean时可通过@bean来注入

1
2
3
4
5
6
7
8
@Component
public class DbConfig {
@Bean
public DruidDataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}
}

最后记得开启注解扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">
<!--指定扫描加载bean的位置-->
<context:component-scan base-package="com.xw.bean,com.xw.config"/>
</beans>

3.@ComponentScan扫描bean

使用FactoryBean接口

造出来的bean并不是DogFactoryBean,而是Dog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DogFactoryBean implements FactoryBean<Dog> {
@Override
public Dog getObject() throws Exception {
Dog d = new Dog();
//可以对对象进行一系列操作后再注入
return d;
}
@Override
public Class<?> getObjectType() {
return Dog.class;
}
//是否单例
@Override
public boolean isSingleton() {
return true;
}
}

定义一个类并使用**@ComponentScan**替代原始xml配置中的包扫描这个动作

1
2
3
4
5
6
7
8
@ComponentScan({"com.xw.bean","com.xw.config"})
public class SpringConfig3 {
@Bean
public DogFactoryBean dog(){
//通过FactoryBean来注入,可以在对象初始化前做一些事情
return new DogFactoryBean();
}
}

通过AnnotationConfigApplicationContext获取bean

1
2
3
4
5
6
7
8
9
public class App3 {
public static void main(String[] args) {
ApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig3.class);
String[] names = app.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
}

补充

可用**@ImportResource**将xml配置文件中的bean和配置类中的bean融合在一起,在配置类上直接写上要被融合的xml配置文件名即可,算的上一种兼容性解决方案

1
2
3
4
@Configuration
@ImportResource("applicationContext1.xml")
public class SpringConfig3 {
}

@Configuration注解的proxyBeanMethods属性,此属性默认值为true,若设为false,则表示注入到容器中的对象不再是唯一的

1
2
3
4
5
6
7
@Configuration(proxyBeanMethods = true)
public class SpringConfig3 {
@Bean
public Cat cat(){
return new Cat();
}
}

4.@Import指定bean

通过@Import可指定要注入的bean,不需要在bean上加注解

1
2
3
@Import(Dog.class)
public class SpringConfig4 {
}

获取bean

1
2
3
4
5
6
7
8
9
public class App4 {
public static void main(String[] args) {
ApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig4.class);
String[] names = app.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
}

结果

1
2
springConfig4
com.xw.bean.Dog

5.容器初始化完成后手动加载bean

通过AnnotationConfigApplicationContext的register方法,可以在容器初始化完成后手动加载bean

1
2
3
4
5
6
7
public class App5 {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig5.class);
//上下文容器对象已经初始化完毕后,手工加载bean
ctx.register(Cat.class);
}
}

也可以用registerBean方法指定bean的名称和参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class App5 {
public static void main(String[] args) {
AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig5.class);

//容器中有同名称的bean时会覆盖之前的
app.registerBean("tom", Cat.class, 0);
app.registerBean("tom", Cat.class, 1);
app.registerBean("tom", Cat.class, 2);

System.out.println(app.getBean("tom"));
/*
结果
Cat{id='2'}
*/
}
}

6.在容器初始化过程中加载bean

(1)ImportSelector接口

实现ImportSelector接口,selectImports方法的返回值会被注入到容器中

1
2
3
4
5
6
7
8
9
10
11
public class MySelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
boolean flag = importingClassMetadata.hasAnnotation("org.springframework.context.annotation.ComponentScan");
if (flag) {
return new String[]{"com.xw.bean.Dog"};
} else {
return new String[0];
}
}
}

导入Selector配置类

1
2
3
4
@ComponentScan("com.xw.bean")
@Import(MySelector.class)
public class SpringConfig6 {
}

获取bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class App6 {
public static void main(String[] args) {
AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig6.class);

String[] names = app.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
/*
结果
com.xw.bean.Dog
*/
}
}

(2)ImportBeanDefinitionRegistrar接口

bean的加载不是一个简简单单的对象,spring中定义了一个叫做BeanDefinition的东西,它才是控制bean初始化加载的核心

ImportBeanDefinitionRegistrar相比于ImportSelector,对注入的bean有更大的操作空间

实现ImportBeanDefinitionRegistrar接口

1
2
3
4
5
6
7
8
9
public class MyRegister implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//获取bean的beanDefinition
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class).getBeanDefinition();
//注入到容器中
registry.registerBeanDefinition("dog", beanDefinition);
}
}

导入Registrar配置类

1
2
3
4
5
@ComponentScan("com.xw.bean")
@Import(MyRegister.class)
public class SpringConfig6 {

}

获取bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class App6 {
public static void main(String[] args) {
AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig6.class);

String[] names = app.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
/*
结果
dog
*/
}
}

7.对容器中bean的最终裁定

postProcessor为最后执行的对容器中bean的注册

实现BeanDefinitionRegistryPostProcessor接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
//获取bean的beanDefinition
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Cat.class).getBeanDefinition();
//将之前的dog删掉换成cat
registry.removeBeanDefinition("dog");
registry.registerBeanDefinition("cat", beanDefinition);
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

}
}

导入PostProcessor配置类

1
2
3
4
@ComponentScan("com.xw.bean")
@Import({MyRegister.class, MyPostProcessor.class})
public class SpringConfig6 {
}

获取bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class App6 {
public static void main(String[] args) {
AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig6.class);

String[] names = app.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
/*
结果
cat
*/
}
}

FactoryBean

BeanFactory的作用是完成了Bean的初始化、自动装配、存储单例Bean等功能

FactoryBean本身是一个Bean,的作用是会将自身和getObject()方法的返回值作为Bean注入到容器中

读原文FactoryBean——Spring的扩展点之一 - 掘金 (juejin.cn)做的笔记

基本使用

  1. 实现FactoryBean接口,将自定义的FactoryBean注入容器
  2. 从容器中获取
1
2
3
4
5
6
7
8
9
10
// service类,将通过FactoryBean注入容器

package com.xw.service;

public class UserService {

public UserService() {
System.out.println("userService construct");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 自定义的FactoryBean

package com.xw.component;

@Component
public class CustomFactoryBean implements FactoryBean<UserService> {

@Override
public UserService getObject() throws Exception {
return new UserService();
}

@Override
public Class<?> getObjectType() {
return UserService.class;
}
}

1
2
3
4
5
6
7
8
9
// 配置类,用于扫描组件

package com.xw.config;

@Configuration
@ComponentScan("com.xw.component")
public class AppConfig {

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 容器,获取Bean

package com.xw;

public class MainApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println("容器初始化完成");
UserService userService = applicationContext.getBean(UserService.class);
Object customFactoryBean = applicationContext.getBean("customFactoryBean");

// 通过Class类和BeanName获取的Bean竟然是同一个Bean
System.out.println(userService);
System.out.println(customFactoryBean);
}
}

/*
容器初始化完成
userService construct
com.xw.service.UserService@59d016c9
com.xw.service.UserService@59d016c9
*/

注意:

  1. 自定义的FactoryBean会向容器中注入两个Bean,一个是自身,一个是getObject()方法的返回值;

  2. 要想获取自定义的FactoryBean本身,需要在BeanName前面加上&符号,applicationContext.getBean("&customFactoryBean")

源码

  1. refresh()

    创建容器的finishBeanFactoryInitialization步骤,会调用到beanFactory.preInstantiateSingletons()来实例化单例Bean

  2. preInstantiateSingletons()

    如果是FactoryBean的实例,就通过getBean()来获取或创建Bean,且在BeanName前加了个&

    之后会判断isEagerInit来创建getObject()里的Bean

  3. getObjectForBeanInstance()

    getBean()中会调用doGetBean(),然后调用getObjectForBeanInstance()

    在这个方法中,会判断Bean是否是FactoryBean的实例,如果是则判断缓存中是否有对应的实例,如果没有才会调用getObjectFromFactoryBean()

  4. getObjectFromFactoryBean()

    通过doGetObjectFromFactoryBean()来调用FactoryBean的getObject()方法,并且判断是否为单例来决定要不要放到缓存中

  5. doGetObjectFromFactoryBean()

    最终在这个方法中调用了getObject()

  6. 总结

    通过判断FactoryBeangetObject()里返回的对象是否被创建过,只有都没有创建过才会最终创建Bean,保证单例性

在Mybatis里的使用场景

Spring中使用Mybatis

  1. mybatis中,通过sqlSessionFactoryBean来创建sqlSessionFactory
  2. MapperScannerRegistrar会将Mapper文件扫描出来生成BeanDefinition,然后会生成一个MapperFactoryBean,通过MapperFactoryBean最终会生成一个Mapper文件对应的代理对象

SpringBoot整合Mybatis

  1. 在SpringBoot中Mybatis的自动装配由MybatisAutoConfiguration这个类完成,里面调用了SqlSessionFactoryBean的getObject()方法来注入了sqlSessionFactory

Bean的加载控制

@ConditionalOnClass / @ConditionalOnMissingClass

当虚拟机中有指定类时加载bean

1
2
3
4
5
@Bean
@ConditionalOnClass(name = "com.xw.bean.Dog")
public Cat cat() {
return new Cat();
}

当虚拟机中有没有指定类时加载bean

1
2
3
4
5
@Bean
@ConditionalOnMissingClass("com.xw.bean.Dog")
public Cat cat() {
return new Cat();
}

@ConditionalOnBean / @ConditionalOnMissingBean

当加载了指定名称的bean时加载bean

1
2
3
4
5
@Bean
@ConditionalOnBean(name = "dog")
public Cat cat() {
return new Cat();
}

当没有加载了指定名称的bean时加载bean

1
2
3
4
5
@Bean
@ConditionalOnMissingBean(name = "dog")
public Cat cat() {
return new Cat();
}

还有一系列的条件控制注解

Bean默认是单例还是多例

在Spring框架中,Bean默认是单例的。也就是说,当应用程序中的多个地方都需要调用某个Bean时,Spring容器只会创建一个Bean的实例,并在需要时共享使用这个实例。

如果需要将Bean设置成多例,可以通过在Bean类上添加@Scope("prototype")注解来实现

1
2
3
4
5
@Component
@Scope("prototype")
public class DemoBean {
// ...
}

当容器需要创建这个Bean时,每次都会创建一个新的实例

Bean的生命周期

BV1L14y1S7cf的笔记

步骤

五步骤版本

  1. 实例化:创建对象
  2. 依赖注入:给成员变量赋值
  3. 初始化:执行自定义的初始化方法
  4. 使用
  5. 销毁

例子

Dog类,有成员变量name,自定义的初始化和销毁方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Dog {

private String name;

public Dog() {
System.out.println("1. 实例化");
}

public Dog(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
System.out.println("2. 依赖注入");
this.name = name;
}

public void myInit() {
System.out.println("3. 初始化");
}

public void myDestroy() {
System.out.println("5. 销毁bean");
}
}

通过配置文件将Dog类交给Spring管理,并指定初始化和销毁方法,还有给成员变量赋值

1
2
3
4
5
6
7
8
9
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="dog" class="com.xw.bean.Dog" init-method="myInit" destroy-method="myDestroy">
<property name="name" value="旺财"></property>
</bean>

</beans>

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MainApplication {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Dog dog = applicationContext.getBean(Dog.class);
System.out.println("4.使用bean");
applicationContext.close();
}
}
// 打印
/*
1. 实例化
2. 依赖注入
3. 初始化
4.使用bean
5. 销毁bean
旺财
*/

七步骤版本

  1. 实例化:创建对象

  2. 依赖注入:给成员变量赋值

    初始化前执行 BeanPostProcessor before方法

  3. 初始化:执行自定义的初始化方法

    初始化前执行 BeanPostProcessor after方法

  4. 使用

  5. 销毁

例子

创建自定义的Bean后置处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Dog && "dog".equals(beanName))
System.out.println(" 执行后置处理器的before方法");
return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Dog && "dog".equals(beanName))
System.out.println(" 执行后置处理器的after方法");
return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}
}

交给Spring管理

1
2
3
4
5
6
7
8
9
10
11
12
13
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="dog" class="com.xw.bean.Dog" init-method="myInit" destroy-method="myDestroy">
<property name="name" value="旺财"></property>
</bean>

<bean id="myBeanPostProcessor" class="com.xw.bean.MyBeanPostProcessor">

</bean>

</beans>

使用

1
2
3
4
5
6
7
8
9
10
// 打印
/*
1. 实例化
2. 依赖注入
执行后置处理器的before方法
3. 初始化
执行后置处理器的after方法
4.使用bean
5. 销毁bean
*/

十步骤版本

  1. 实例化:创建对象

  2. 依赖注入:给成员变量赋值

    执行回调函数…Aware

    初始化前执行 BeanPostProcessor before方法

    执行正在初始化bean函数

  3. 初始化:执行自定义的初始化方法

    初始化前执行 BeanPostProcessor after方法

  4. 使用

    执行销毁bean方法

  5. 销毁

例子

实现BeanNameAware, InitializingBean, DisposableBean接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Dog implements BeanNameAware, InitializingBean, DisposableBean {

private String name;

public Dog() {
System.out.println("1. 实例化");
}

public Dog(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
System.out.println("2. 依赖注入");
this.name = name;
}

public void myInit() {
System.out.println("3. 初始化");
}

public void myDestroy() {
System.out.println("5. 销毁bean");
}

@Override
public void setBeanName(String name) {
System.out.println(" 执行回调函数");
}

@Override
public void afterPropertiesSet() throws Exception {
System.out.println(" 执行正在初始化bean函数");
}

@Override
public void destroy() throws Exception {
System.out.println(" 执行销毁bean方法");
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 打印
/*
1. 实例化
2. 依赖注入
执行回调函数
执行后置处理器的before方法
执行正在初始化bean函数
3. 初始化
执行后置处理器的after方法
4.使用bean
执行销毁bean方法
5. 销毁bean

进程已结束,退出代码为 0

*/

InitializingBean作用

InitializingBean的afterPropertiesSet()是在依赖注入完、执行完回调、执行完后处理器的before后调用的。这个时候成员变量已经实例化完,可以通过afterPropertiesSet()方法来给变量进行一些初始化操作

如:

  1. 数据库连接池的初始化
    在连接池的初始化过程中,可以通过实现InitializingBean接口,在Bean初始化完成后自动调用afterPropertiesSet()方法,进行连接池的初始化工作。
  2. 配置文件加载
    在Spring中,可以通过实现InitializingBean接口,使用afterPropertiesSet()方法,在Bean属性设置完成后,进行配置文件的加载工作,从而减少了代码量和配置文件的加载时间。
  3. 线程池初始化
    在Java中创建线程池时,可以通过实现InitializingBean接口,使用afterPropertiesSet()方法,在Bean属性设置完成后,进行线程池的初始化工作。
  4. 数据初始化
    在某些场景下,需要在Bean初始化完成后,进行数据初始化的操作。可以通过实现InitializingBean接口,在Bean属性设置完成后,使用afterPropertiesSet()方法,进行相关数据的初始化工作。

BeanPostProcessor作用

BeanPostProcessor是Spring IoC容器提供的一个扩展接口,主要作用是在Spring IoC容器实例化Bean对象后,对其进行加工和装饰,添加功能

如:

  1. AOP代理
    通过before或after方法,在Bean对象装配之后,对Bean对象进行AOP代理,从而在不修改原有代码的情况下,实现对Bean对象的增强或切面操作,如事务管理、权限控制、日志记录、性能监控等。

定制Bean

单例多例

默认单例Bean

通过@Scope("prototype")声明为多例Bean,每次都返回新的实例

注入所有同类型的Bean

1
2
3
4
5
// 通过List接收
@Autowired
List<Validator> validators;

// 在实现类Bean上添加@Order(),可指定顺序

注入指定的Bean

1
2
3
4
5
6
7
8
9
// 通过@Bean取名
@Bean("Bean name")

// 也可通过@Qualifier取名
@Bean()
@Qualifier("name")

// 没指定名字时,指定优先注入哪个
@Primary

在注入时通过@Qualifier(beanName)指定

Bean的依赖属性注入

在配置文件中设置值

1
2
3
4
5
6
7
zoo:
cat:
id: 1
name: "zs"
dog:
id: 2
name: "ls"

定义一个类,通过ConfigurationProperties注解来读取和封装配置中的属性

1
2
3
4
5
6
7
@Component
@ConfigurationProperties(prefix = "zoo")
@Data
public class ZooProperties {
private Cat cat;
private Dog dog;
}

在需要使用配置的地方使用EnableConfigurationProperties注解来注入配置类

1
2
3
4
5
6
@EnableConfigurationProperties(ZooProperties.class)
public class TestSpringboot23BeanPropApplication {

@Autowired
private ZooProperties zooProperties;
}

在需要使用属性类的位置通过注解EnableConfigurationProperties注解加载bean,而不要直接在属性配置类上定义bean,减少资源加载的数量,因需加载而不要饱和式加载。

AOP

OOP:Object Oriented Programming,面向对象变成

AOP:Aspect Orient Programming,面向切面编程

作用:实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术,减少对业务代码的侵入

使用场景:日志、事务、权限校验、性能监控等

概念

名词

  • Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
  • Joinpoint:连接点,指可以被动态代理拦截目标类的方法
  • Pointcut:切入点,被拦截的连接点
  • Advice:通知,对切入点增强的内容
  • Target Object:代理的目标对象
  • Weaving:织入,把增强代码应用到目标上,生成代理对象的过程
  • AOP Proxy:AOP代理,生成的代理对象

通知类别

注解 含义 类比
@Before 方法执行前 “准备阶段”
@After 方法执行后(无论成功失败) “清理阶段”
@AfterReturning 正常返回后 “成功收尾”
@AfterThrowing 抛出异常后 “出错应对”
@Around 包裹整个方法,可控制执行与否 “万能钩子”

切点表达式

表达式 说明
execution(* com.example.service..*(..)) 匹配 service 包及子包下的所有方法
execution(public * com.example.service.UserService.*(..)) 匹配 UserService 类的所有 public 方法
@annotation(org.springframework.transaction.annotation.Transactional) 匹配被某个注解标注的方法
within(com.example.controller..*) 匹配某个包下的所有类中的方法

使用步骤

Spring中使用

使用方式

SpringBoot中使用

例:记录方法的执行时间、参数、方法名等信息

  1. 导入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    </dependency>

    <!-- 序列化、反序列化 -->
    <dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    </dependency>
  2. 定义日志实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package com.xw.springboottest.bo;

    import lombok.Data;

    /**
    * 系统日志bo(Business Object)
    */
    @Data
    public class SysLogBO {

    private String className;

    private String methodName;

    private String params;

    private Long exeuTime;

    private String remark;

    private String createDate;
    }
  3. 定义日志注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.xw.springboottest.anno;

    import java.lang.annotation.*;

    /**
    * 定义系统日志注解
    */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface SysLog {
    String value() default "";
    }
  4. (重点)定义切面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    package com.xw.springboottest.aspect;

    import com.google.gson.Gson;
    import com.xw.springboottest.anno.SysLog;
    import com.xw.springboottest.bo.SysLogBO;
    import com.xw.springboottest.service.SysLogService;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;

    import java.lang.reflect.Method;
    import java.text.SimpleDateFormat;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;

    @Aspect
    @Component
    public class SysLogAspect {
    @Autowired
    private SysLogService sysLogService;

    // 定义切点
    @Pointcut("@annotation(com.xw.springboottest.anno.SysLog)")
    public void logPointCut() {}

    /**
    * 环绕通知
    * @param point
    * @return
    * @throws Throwable
    */
    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
    // 运行开始时间
    long beginTime = System.currentTimeMillis();
    // 执行目标方法
    Object result = point.proceed();
    // 运行结束时间
    long endTime = System.currentTimeMillis();
    try {
    saveLog(point, endTime - beginTime);
    } catch (Exception e) {
    }
    return result;
    }

    /**
    * 保存日志
    */
    private void saveLog(ProceedingJoinPoint joinPoint, long time) {
    // 拿到当前执行方法的签名信息
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();

    // 创建日志对象
    SysLogBO sysLogBO = new SysLogBO();
    sysLogBO.setExeuTime(time);
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    sysLogBO.setCreateDate(dateFormat.format(new Date()));

    // 判断目标方法是否有 @SysLog 注解
    SysLog sysLog = method.getAnnotation(SysLog.class);
    if(sysLog != null){
    // 保存注解上的描述
    sysLogBO.setRemark(sysLog.value());
    }

    //请求的 类名、方法名
    String className = joinPoint.getTarget().getClass().getName();
    String methodName = signature.getName();
    sysLogBO.setClassName(className);
    sysLogBO.setMethodName(methodName);

    // 请求的参数
    Object[] args = joinPoint.getArgs();
    try{
    List<String> list = new ArrayList<>();
    for (Object o : args) {
    // 将参数转化为json字符串
    list.add(new Gson().toJson(o));
    }
    sysLogBO.setParams(list.toString());
    }catch (Exception e){ }

    // 保存日志
    sysLogService.save(sysLogBO);
    }
    }
  5. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RestController
    public class TestController {

    @SysLog("测试")
    @GetMapping("/test")
    public String test(@RequestParam("name") String name){
    return name;
    }
    }

实现原理

采用JDK动态代理CGLib动态代理实现

  • 如果目标对象有实现接口,则使用JDK动态代理
  • 如果没有实现接口,则使用CGLib动态代理

JDK动态代理

通过反射实现代理

步骤

  1. 定义实现InvocationHandler接口的处理器
  2. 通过**Proxy.newInstance(类加载器,所有代理目标实现的接口,处理器)**来获取代理对象
  3. 使用代理对象的方法

优点:

  1. 原生,不需要其他依赖
  2. 生成代理类的速度快

缺点:

  1. 通过接口的方法来织入增强方法,因此目标必须需要实现接口
  2. 执行方法的速度相对较慢
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class JdkProxySubject implements InvocationHandler {

private Subject subject;

public JdkProxySubject(Subject subject) {
this.subject = subject;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

System.out.println("before 前置通知");
Object result = null;

try {
result = method.invoke(subject, args);
}catch (Exception ex) {
System.out.println("ex: " + ex.getMessage());
throw ex;
}finally {
System.out.println("after 后置通知");
}
return result;
}
}
1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
//获取InvocationHandler对象 在构造方法中注入目标对象
InvocationHandler handler = new JdkProxySubject(new RealSubject());

//获取代理类对象
Subject proxySubject = (Subject)Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{Subject.class}, handler);

//调用目标方法
proxySubject.request();
}

CGLib动态代理

通过操作字节码实现代理

Cglib的实现是在字节码的基础上的,并且使用了开源的ASM读取字节码,对类实现增强功能的。

SpringMVC

MVC(Model-View-Controller),模型-视图-控制器

作用

SpringMVC底层就是Servlet,SpringMVC就是对Servlet进行深层次的封装

MVC模式

  • Model表示数据和应用逻辑,可能包括数据库操作等。View是用户所看到的界面,它展示Model中的数据并让用户与其进行交互。Controller负责处理用户请求,从View中接收输入并在Model里面读取或修改数据,负责把模块和视图结合起来

    SpringMVC执行流程

执行流程

SpringMVC执行流程

  1. 用户发送请求到前端调度器(DispatcherServlet)
  2. 调度器收到请求后调用处理器映射器(HandlerMapping)
  3. 处理器映射器根据url找到具体的处理器和拦截器,生成执行链,返回给调度器
  4. 调度器通过处理器适配器(HandlerAdapter)调用具体的处理器(Handler),返回ModelAndView
  5. 调度器将ModelAndView传给视图解析器(ViewReslover)解析,返回视图对象(View)
  6. 调度器渲染视图,把视图和数据结合,响应给用户

组件

  • 前端调度器(DispatcherServlet):接收请求,响应数据,需要程序员配置
  • 处理器映射器(HandlerMapping):通过url查找Handler
  • 处理器适配器(Handler Adapter):执行Handler中的方法
  • 处理器(Handler):业务方法,需要程序员编写
  • 视图解析器(ViewResolver):把ModelAndView解析出给用户看的视图
  • 视图(View):展示给用户看的,需要程序员编写

问题

  1. 调度器(DispatcherServlet)如何获取处理器(Handler)?

    调度器里有一个doDispatch()方法,里面调用了getHandler()方法;在getHandler()里面调用了处理器映射器(HandlerMapping)的getHandler()方法来获取处理器

  2. 调度器如何执行处理器?

    调度器里的doDispatch()方法里,首先通过getHandlerAdapter()方法获取当前处理器的处理器适配器(HandlerAdapter)来做适配,然后执行适配器的handler()方法

  3. 调度器如何解析视图对象(ModelAndView)的?

    调度器里的doDispatch()方法里,调用processDispatcherResult()方法,方法里调用render()方法,render()方法调用resolveViewName(),通过遍历的方式来找和viewName相同的View

  4. 三大组件(HandlerMapping、HandlerAdapter、ViewResolver)是谁初始化的?

    SpringMVC中通过DispatcherServlet中的initStrategies()方法,通过默认配置来找到类所在位置,来初始化组件

Spring Cache

通过注解来实现缓存,底层可以切换不同的cache实现,通过CacheManager接口来统一不同的缓存技术

对不同的缓存技术需要实现不同的CacheManager

CacheManager 描述
EhCacheCacheManager 使用EhCache缓存技术
GuavaCacheManager 使用Guava缓存技术
RedisCacheManager 使用Redis缓存技术

常用注解

注解 说明
@EnableCaching 开启缓存注解功能
@Cacheable 优先从缓存中取数据,没有去数据库取
@CachePut 将方法返回值放到内存中
@CacheEvict 将一条或多条数据从缓存中删除

实例1

导入坐标

1
2
3
4
5
6
<!-- 不使用默认缓存技术,使用到其他缓存技术时要导入这个坐标 -->
<!-- 及相应缓存技术的坐标 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

配置相应技术

1
2
3
4
5
6
7
8
9
spring: 
redis:
host: localhost
port: 6379
database: 0
cache:
redis:
time-to-live: 1800000 #缓存过期时间
type: redis

启动类加@EnableCaching注解

在要缓存的方法上加注解, 返回值会被存到redis中, 返回值必须是可序列化的

1
2
3
@GetMapping("/list")
@Cacheable(value = "setmealCache", key = "#setmeal.categoryId + '_' + #setmeal.status")
public R<List<Setmeal>> list(Setmeal setmeal) {...}

实例2

在微服务中使用spring cache

例子来自尚医通

  • 在common组件中添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- redis -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- spring2.X集成redis所需common-pool2-->
    <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.7.0</version>
    </dependency>
  • 在common组件中添加redis配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    package com.xw.config;

    @Configuration
    @EnableCaching
    public class RedisConfig {

    /**
    * 自定义key规则
    * @return
    */
    @Bean
    public KeyGenerator keyGenerator() {
    return new KeyGenerator() {
    @Override
    public Object generate(Object target, Method method, Object... params) {
    StringBuilder sb = new StringBuilder();
    sb.append(target.getClass().getName());
    sb.append(method.getName());
    for (Object obj : params) {
    sb.append(obj.toString());
    }
    return sb.toString();
    }
    };
    }

    /**
    * 设置RedisTemplate规则
    * @param redisConnectionFactory
    * @return
    */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

    //解决查询缓存转换异常的问题
    ObjectMapper om = new ObjectMapper();
    // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
    om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(om);

    //序列号key value
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
    redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

    redisTemplate.afterPropertiesSet();
    return redisTemplate;
    }

    /**
    * 设置CacheManager缓存规则
    * @param factory
    * @return
    */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
    RedisSerializer<String> redisSerializer = new StringRedisSerializer();
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

    //解决查询缓存转换异常的问题
    ObjectMapper om = new ObjectMapper();
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(om);

    // 配置序列化(解决乱码的问题),过期时间600秒
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
    .entryTtl(Duration.ofSeconds(600))
    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
    .disableCachingNullValues();

    RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
    .cacheDefaults(config)
    .build();
    return cacheManager;
    }
    }

  • 在使用到缓存的模块中添加配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    spring.redis.database= 0
    spring.redis.timeout=1800000

    spring.redis.lettuce.pool.max-active=20
    spring.redis.lettuce.pool.max-wait=-1
    #最大阻塞等待时间(负数表示没限制)
    spring.redis.lettuce.pool.max-idle=5
    spring.redis.lettuce.pool.min-idle=0
  • 在需要缓存的方法上添加注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 根据id查询子数据列表
    @Cacheable(value = "dict",keyGenerator = "keyGenerator")
    @Override
    public List<Dict> findChildData(Long id) {
    LambdaQueryWrapper<Dict> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(Dict::getParentId, id);
    List<Dict> dictList = baseMapper.selectList(wrapper);
    for (Dict dict : dictList) {
    boolean hasChildren = this.isChildren(dict.getId());
    dict.setHasChildren(hasChildren);
    }
    return dictList;
    }

SpringTask

SpringTask是Spring自主研发的轻量级定时任务工具

Cron表达式

格式:秒 分 时 日 月 星期

时间元素 可出现的字符 有效数值范围
Seconds , - * / 0-59
Minutes , - * / 0-59
Hours , - * / 0-23
DayofMonth , - * / ? L W 0-31
Month , - * / 1-12
DayofWeek , - * / ? L # 1-7或SUN-SAT

说明

字符 作用 举例
, 列出枚举值 在Minutes域使用5,10,表示在5分和10分各触发一次
- 表示触发范围 在Minutes域使用5-10,表示从5分到10分钟每分钟触发一次
* 匹配任意值 在Minutes域使用*, 表示每分钟都会触发一次
/ 起始时间开始触发,每隔固定时间触发一次 在Minutes域使用5/10,表示5分时触发一次,每10分钟再触发一次
? 在DayofMonth和DayofWeek中,用于匹配任意值 在DayofMonth域使用?,表示每天都触发一次
# 在DayofMonth中,确定第几个星期几 1#3表示第三个星期日
L 表示最后 在DayofWeek中使用5L,表示在最后一个星期四触发
W 表示有效工作日(周一到周五) 在DayofMonth使用5W,如果5日是星期六,则将在最近的工作日4日触发一次

示例

  1. 每两秒执行一次

    1
    0/2 * * * * *
  2. 每个二、四、六星期的下午两点执行一次

    1
    * * 14 * * 2,4,6

使用

  1. SpringTask已经存在于Spring框架中,无需添加新的依赖

  2. @EnableScheduling开启定时任务功能

    1
    2
    3
    4
    @Configuration
    @EnableScheduling
    public class SpringTaskConfig {
    }
  3. 使用示例

    每十分钟执行一次

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package com.xw.mallLearning.component;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;

    @Slf4j
    @Component
    public class OrderTimeOutCancelTask {

    @Scheduled(cron = "0 0/10 * * * *")
    private void cancelTimeOutOrder() {
    // 定时任务
    log.info("task");
    }
    }


Spring
http://xwww12.github.io/2025/10/02/Spring&SpringBoot/Spring/
作者
xw
发布于
2025年10月2日
许可协议