Spring-基础

笔记参考于柏码知识库

loC容器基础

简单使用

新建Maven项目, 导入Spring框架依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.13</version>
</dependency>

在在resource中创建一个Spring配置文件,命名为test.xml:

1
2
3
4
5
6
7
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">

</beans>

主方法获取loC容器:

1
2
3
4
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("text");

}

想loC容器中注册JavaBean:

1
2
3
4
5
//注意,这里还用不到值注入,只需要包含成员属性即可,不用Getter/Setter。
public class Student {
String name;
int age;
}

配置文件中添加(name或id不可重复):

1
<bean name="student" class="com.test.bean.Student"/>

通过loC容器获取:

1
2
3
4
5
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
Student student = (Student) context.getBean("student");
System.out.println(student);
}

默认情况下,loC容器进行管理的JavaBean是单例模式,在一开始Bean就会被创建。

可以添加scope属性修改作用域。prototype(原型模式)来使得其每次都会创建一个新的对象,在获取时才被创建。

1
<bean name="student" class="com.test.bean.Student" scope="prototype"/>

生命周期

还可以配置初始化方法以及销毁方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Student {
String name;
int age;

private void init(){
System.out.println("我是初始化方法!");
}

private void destroy(){
System.out.println("我是销毁方法!");
}

public Student(){
System.out.println("我被构造了!");
}
}
1
<bean name="student" class="com.test.bean.Student" init-method="init" destroy-method="destroy"/>
1
2
3
4
5
6
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
Student student = (Student) context.getBean("student");
System.out.println(student);
context.close(); //手动销毁容器
}

还可以使用depend-on属性指定Bean的加载顺序。

依赖注入

可以使用property标签预定属性,对应属性必须存在set方法:

1
2
3
<bean name="student" class="com.test.bean.Student">
<property name="name" value="小明"/>
</bean>

对于对象,也将其对应的类型注册为bean即可,使用ref属性引用。

1
2
3
4
5
<bean name="card" class="com.test.bean.Card"/>
<bean name="student" class="com.test.bean.Student">
<property name="name" value="小明"/>
<property name="card" ref="card"/>
</bean>

集合使用list-value标签编写:

1
2
3
4
5
6
7
8
9
<bean name="student" class="com.test.bean.Student">
<property name="list">
<list>
<value type="double">100.0</value>
<value type="double">95.0</value>
<value type="double">92.5</value>
</list>
</property>
</bean>

Map类型使用map-entry标签:

1
2
3
4
5
6
7
8
9
<bean name="student" class="com.test.bean.Student">
<property name="map">
<map>
<entry key="语文" value="100.0"/>
<entry key="数学" value="80.0"/>
<entry key="英语" value="92.5"/>
</map>
</property>
</bean>

自动装配

还可以自动装配:

1
2
<bean name="card" class="com.test.bean.Card"/>
<bean name="student" class="com.test.bean.Student" autowire="byType"/>

使用有参构造方法:

1
2
3
4
<bean name="student" class="com.test.bean.Student">
<constructor-arg name="name" value="小明"/>
<constructor-arg index="1" value="18"/>
</bean>

使用注解

主要配置文件使用@Configuration注解

1
2
3
4
@Configuration
public class MainConfiguration {
//没有配置任何Bean
}

配置Bean使用@Bean注解。

1
2
3
4
5
6
7
8
@Configuration
public class MainConfiguration {

@Bean("student")
public Student student(){
return new Student();
}
}

可以在@Bean中配置初始化方法、摧毁方法、自动装配等:

1
2
3
4
@Bean(name = "", initMethod = "", destroyMethod = "", autowireCandidate = false)
public Student student(){
return new Student();
}

对于依赖注入的Bean:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class MainConfiguration {
@Bean
public Teacher teacher(){
return new Teacher();
}

@Bean
public Student student(Teacher teacher){
return new Student(teacher);
}
}

也可以到Bean中对应类使用自动装配:

1
2
3
4
public class Student {
@Autowired //使用此注解来进行自动装配,由IoC容器自动为其赋值
private Teacher teacher;
}

若有多个相同类型的Bean,可以配合@Qualifier进行匹配:

1
2
3
4
5
public class Student {
@Autowired
@Qualifier("a") //匹配名称为a的Teacher类型的Bean
private Teacher teacher;
}

官方更推荐使用@Resource注解进行注入,区别为:

  • @Resource默认ByName如果找不到则ByType,可以添加到set方法、字段上。
  • @Autowired默认是byType,只会根据类型寻找,可以添加在构造方法、set方法、字段、方法参数上。

使用@Resource需要额外导入包:

1
2
3
4
5
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>

还有@PostConstruct和@PreDestroy,它们效果和init-method和destroy-method是一样的:

1
2
3
4
5
6
7
8
9
@PostConstruct
public void init(){
System.out.println("我是初始化方法");
}

@PreDestroy
public void destroy(){
System.out.println("我是销毁方法");
}

对于自己编写的类,可以使用@Component注解自动扫描并注册:

1
2
3
4
@Component("lbwnb")   //同样可以自己起名字
public class Student {

}
1
2
3
4
5
@Configuration
@ComponentScan("com.test.bean") //包扫描,这样Spring就会去扫描对应包下所有的类
public class MainConfiguration {

}

对于这种使用@Component注册的Bean,如果其构造方法不是默认无参构造,那么默认会对其每一个参数都进行自动注入。

面向切面AOP

SpringAOP

可以使用AOP在方法执行前后进行一些额外操作,而不修改方法源码。

首先导入依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.13</version>
</dependency>

定义切入方法:

1
2
3
4
5
6
7
8
9
10
11
12
public class AopTest {

//执行之后的方法
public void after(){
System.out.println("我是执行之后");
}

//执行之前的方法
public void before(){
System.out.println("我是执行之前");
}
}

注册bean:

1
2
<bean name="student" class="com.test.bean.Student"/>
<bean name="aopTest" class="com.test.aop.AopTest"/>

引入aop标签:

1
2
3
4
5
6
<?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: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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

添加AOP配置:

1
2
3
4
5
6
7
<aop:config>
<aop:pointcut id="test" expression="execution(* com.test.bean.Student.test(String))"/>
<aop:aspect ref="aopTest">
<aop:before method="before" pointcut-ref="test"/>
<aop:after-returning method="after" pointcut-ref="test"/>
</aop:aspect>
</aop:config>

接口AOP

通过Advice实现AOP。

方法前操作继承MethodBeforeAdvice实现:

1
2
3
4
5
6
public class AopTest implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("通过Advice实现AOP");
}
}

其中args代表的是方法执行前得到的实参列表,还有target表示执行此方法的实例对象。

AOP配置文件:

1
2
3
4
<aop:config>
<aop:pointcut id="stu" expression="execution(* com.test.bean.Student.say(String))"/>
<aop:advisor advice-ref="before" pointcut-ref="stu"/>
</aop:config>

添加方法执行后:

1
2
3
4
5
6
7
8
9
10
11
public class AopTest implements MethodBeforeAdvice, AfterReturningAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("我是方法执行之前!");
}

@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("我是方法执行之后!");
}
}

使用MethodInterceptor(同样也是Advice的子接口)进行更加环绕那样的自定义的增强:

1
2
3
4
5
6
7
public class StudentAOP implements MethodInterceptor {   //实现MethodInterceptor接口
@Override
public Object invoke(MethodInvocation invocation) throws Throwable { //invoke方法就是代理方法
Object value = invocation.proceed(); //跟之前一样,需要手动proceed()才能调用原方法
return value+"增强";
}
}

注解AOP

添加@EnableAspectJAutoProxy注解,开启AOP注解支持:

1
2
3
4
5
@EnableAspectJAutoProxy
@ComponentScan("org.example.entity")
@Configuration
public class MainConfiguration {
}

注册Bean:

1
2
3
4
5
6
@Component
public class Student {
public void study(){
System.out.println("我是学习方法!");
}
}

定义AOP增强操作的类,添加@Aspect注解和@Component将其注册为Bean, 定义方法并在方法前添加注解:

1
2
3
4
5
6
7
8
@Aspect
@Component
public class StudentAOP {
@Before("execution(* org.example.entity.Student.study())") //execution写法跟之前一样
public void before(){
System.out.println("我是之前执行的内容!");
}
}

添加JoinPoint参数获取切入点信息:

1
2
3
4
5
@Before("execution(* org.example.entity.Student.study())")
public void before(JoinPoint point){
System.out.println("参数列表:"+ Arrays.toString(point.getArgs()));
System.out.println("我是之前执行的内容!");
}

使用命名绑定模式,可以快速得到原方法的参数:

1
2
3
4
5
6
7
@Before(value = "execution(* org.example.entity.Student.study(..)) && args(str)", argNames = "str")
//命名绑定模式就是根据下面的方法参数列表进行匹配
//这里args指明参数,注意需要跟原方法保持一致,然后在argNames中指明
public void before(String str){
System.out.println(str); //可以快速得到传入的参数
System.out.println("我是之前执行的内容!");
}

还有@AfterReturning、@AfterThrowing等注解。

环绕注解:

1
2
3
4
5
6
7
@Around("execution(* com.test.bean.Student.test(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("方法执行之前!");
Object val = point.proceed();
System.out.println("方法执行之后!");
return val;
}

Spring高级特性

Bean Aware

在Bean加载前获取相关信息。

任务调度

异步任务

首先开启异步任务支持,添加@EnableAsync注解。

1
2
3
4
5
@EnableAsync
@Configuration
@ComponentScan("com.test.bean")
public class MainConfiguration {
}

在需要异步执行的方法上添加@Async注解,当此方法被调用时,会异步执行,也就是新开一个线程执行,而不是在当前线程执行。添加此注解要求方法的返回值只能是void或是Future类型才可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class Student {
public void syncTest() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"我是同步执行的方法,开始...");
Thread.sleep(3000);
System.out.println("我是同步执行的方法,结束!");
}

@Async
public void asyncTest() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"我是异步执行的方法,开始...");
Thread.sleep(3000);
System.out.println("我是异步执行的方法,结束!");
}
}

定时任务

在配置类上添加@EnableScheduling注解:

1
2
3
4
5
@EnableScheduling
@Configuration
@ComponentScan("com.test.bean")
public class MainConfiguration {
}

添加@Scheduled注解:

1
2
3
4
@Scheduled(fixedRate = 2000)   //单位依然是毫秒,这里是每两秒钟打印一次
public void task(){
System.out.println("我是定时任务!"+new Date());
}

@Scheduled中有很多参数,需要指定’cron’, ‘fixedDelay(String)’, or ‘fixedRate(String)’的其中一个:

  • ixedDelay:在上一次定时任务执行完之后,间隔多久继续执行。
  • fixedRate:无论上一次定时任务有没有执行完成,两次任务之间的时间间隔。
  • cron:还可以使用cron表达式来指定任务计划。

监听器

创建监听器:

1
2
3
4
5
6
7
@Component
public class TestListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println(event.getApplicationContext()); //可以直接通过事件获取到事件相关的东西
}
}

自定义监听事件:

1
2
3
4
5
public class TestEvent extends ApplicationEvent {   //自定义事件需要继承ApplicationEvent
public TestEvent(Object source) {
super(source);
}
}

SpringEL表达式

SpEL 是一种强大,简洁的装配 Bean 的方式,它可以通过运行期间执行的表达式将值装配到我们的属性或构造函数当中,更可以调用 JDK 中提供的静态常量,获取外部 Properties 文件中的的配置。

外部属性注入

在resource目录下创建以test.properties结尾的配置文件:

1
test.name=只因

使用@PropertySource引入:

1
2
3
4
5
6
@Configuration
@ComponentScan("com.test.bean")
@PropertySource("classpath:test.properties") //注意,类路径下的文件名称需要在前面加上classpath:
public class MainConfiguration{

}

使用@Value注解注入:

1
2
3
4
5
6
7
8
9
@Component
public class Student {
@Value("${test.name}") //这里需要在外层套上 ${ }
private String name; //String会被自动赋值为配置文件中对应属性的值

public void hello(){
System.out.println("我的名字是:"+name);
}
}

也可以注入方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class Student {
private final String name;

//构造方法中的参数除了被自动注入外,我们也可以选择使用@Value进行注入
public Student(@Value("${test.name}") String name){
this.name = name;
}

public void hello(){
System.out.println("我的名字是:"+name);
}
}

也可以注入常量值:

1
2
3
4
private final String name;
public Student(@Value("10") String name){ //只不过,这里都是常量值了,我干嘛不直接写到代码里呢
this.name = name;
}

SpEL简单使用

创建SpEL表达式,此处得到一个简单的字符串:

1
2
3
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); //使用parseExpression方法来创建一个表达式
System.out.println(exp.getValue()); //表达式最终的运算结果可以通过getValue()获取

还可以调用字符串方法:

1
2
Expression exp = parser.parseExpression("'Hello World'.toUpperCase()");   //调用String的toUpperCase方法
System.out.println(exp.getValue());

可以像访问属性一样使用Getter方法:

1
2
3
//比如 String.getBytes() 方法,就是一个Getter,那么可以写成 bytes
Expression exp = parser.parseExpression("'Hello World'.bytes");
System.out.println(exp.getValue());

还可以多级调用:

1
2
Expression exp = parser.parseExpression("'Hello World'.bytes.length");   //继续访问数组的length属性
System.out.println(exp.getValue());

可以支持根据特定表达式,从给定对象中获取属性出来:

1
2
3
4
5
6
7
8
9
10
11
@Component
public class Student {
private final String name;
public Student(@Value("${test.name}") String name){
this.name = name;
}

public String getName() { //比如下面要访问name属性,那么这个属性得可以访问才行,访问权限不够是不行的
return name;
}
}
1
2
3
4
Student student = context.getBean(Student.class);
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");
System.out.println(exp.getValue(student)); //直接读取对象的name属性

还可以进一步处理对象属性,或设置属性值:

1
Expression exp = parser.parseExpression("name.bytes.length");   //拿到name之后继续getBytes然后length
1
2
Expression exp = parser.parseExpression("name");
exp.setValue(student, "刻师傅"); //同样的,这个属性得有访问权限且能set才可以,否则会报错

还可以进行一些计算。还有一种特殊语法来使用需要导入才能使用的类:

1
2
3
Expression exp = parser.parseExpression("T(java.lang.Math).random()");   //由T()囊括,包含完整包名+类名
//Expression exp = parser.parseExpression("T(System).nanoTime()"); //默认导入的类可以不加包名
System.out.println(exp.getValue());

集合操作

对于类中的集合类:

1
2
3
4
5
@Component
public class Student {
public Map<String, String> map = Map.of("test", "你干嘛");
public List<String> list = List.of("AAA", "BBB", "CCC");
}

使用SpEL快速取出集合中的元素:

1
2
3
4
Expression exp = parser.parseExpression("map['test']");  //对于Map这里映射型,可以直接使用map[key]来取出value
System.out.println(exp.getValue(student));
Expression exp = parser.parseExpression("list[2]"); //对于List、数组这类,可以直接使用[index]
System.out.println(exp.getValue(student));

快速创建集合:

1
2
3
4
5
6
7
8
9
10
Expression exp = parser.parseExpression("{5, 2, 1, 4, 6, 7, 0, 3, 9, 8}"); //使用{}来快速创建List集合
List value = (List) exp.getValue();
value.forEach(System.out::println);

Expression exp = parser.parseExpression("{{1, 2}, {3, 4}}"); //它是支持嵌套使用的

//创建Map也很简单,只需要key:value就可以了,怎么有股JSON味
Expression exp = parser.parseExpression("{name: '小明', info: {address: '北京市朝阳区', tel: 10086}}");
System.out.println(exp.getValue());

根据条件获取集合元素:

1
2
3
4
5
6
@Component
public class Student {
public List<Clazz> list = List.of(new Clazz("高等数学", 4));

public record Clazz(String name, int score){ }
}
1
2
3
4
5
6
//现在我们希望从list中获取那些满足我们条件的元素,并组成一个新的集合,我们可以使用.?运算符
Expression exp = parser.parseExpression("list.?[name == '高等数学']");
System.out.println(exp.getValue(student));

Expression exp = parser.parseExpression("list.?[score > 3]"); //选择学分大于3分的科目
System.out.println(exp.getValue(student));

针对某个属性创建对应的投影集合:

1
2
Expression exp = parser.parseExpression("list.![name]");   //使用.!创建投影集合,这里创建的时课程名称组成的新集合
System.out.println(exp.getValue(student));

安全导航运算符

针对空指针

1
2
Expression exp = parser.parseExpression("name?.toUpperCase()");
System.out.println(exp.getValue(student));

与注解和xml文件配合

1
2
3
<bean id="numberGuess" class="org.spring.samples.NumberGuess">
<property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
</bean>
1
2
3
4
public class FieldValueTestBean {
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;
}

数据库框架整合

关于数据源

采用池化技术来避免反复与数据库新建连接降低性能。也是Mybatis默认配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${驱动类(含包名)}"/>
<property name="url" value="${数据库连接URL}"/>
<property name="username" value="${用户名}"/>
<property name="password" value="${密码}"/>
</dataSource>
</environment>
</environments>
</configuration>

其中dataSource标签属性有三个选项:

  • UNPOOLED 不使用连接池的数据源
  • POOLED 使用连接池的数据源
  • JNDI 使用JNDI实现的数据源

整合Mybatis框架

使用@MapperScan来去除Mapper配置文件。

在配置文件中创建数据源Bean去除Mybatis配置文件。

在配置文件中创建SqlSessionFactoryBean类来代替手写的SqlSession分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
@ComponentScan("org.example.entity")
@MapperScan("org.example.mapper")
public class MainConfiguration {
@Bean //单独创建一个Bean,方便之后更换
public DataSource dataSource(){
return new PooledDataSource("com.mysql.cj.jdbc.Driver",
"jdbc:mysql://localhost:3306/study", "root", "123456");
}

@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){ //直接参数得到Bean对象
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean;
}
}

使用:

1
2
3
4
5
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class);
TestMapper mapper = context.getBean(TestMapper.class);
System.out.println(mapper.getStudent());
}

HikariCP连接池

导入依赖:

1
2
3
4
5
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>

数据源的实现:

1
2
3
4
5
6
7
8
9
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/study");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}

Mybatis事务管理

隔离级别

  • 读未提交:可能脏读、不可重复读(虚读)、幻读
  • 读已提交:不可重复读、幻读
  • 可重复读:幻读
  • 串行化:数据没问题,但效率低。

其中脏读是读取到旧数据,虚读是两次读取前后不一致,幻读是表记录的数量读取不一致。

Spring声明式事务管理

在配置类添加@EnableTransactionManagement注解:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@ComponentScan("org.example")
@MapperScan("org.example.mapper")
@EnableTransactionManagement
public class MainConfiguration {

@Bean
public TransactionManager transactionManager(DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}

在方法上添加@Transactional注解,即可表示此方法执行的是一个事务操作,在调用此方法时,Spring会通过AOP机制为其进行增强,一旦发现异常,事务会自动回滚。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class TestServiceImpl implements TestService{

@Resource
TestMapper mapper;

@Transactional //此注解表示事务,之后执行的所有方法都会在同一个事务中执行
public void test() {
mapper.insertStudent();
if(true) throw new RuntimeException("我是测试异常!");
mapper.insertStudent();
}
}

集成JUnit测试

Spring提供了一个Test模块,会自动集成Junit进行测试,导入以下依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.10</version>
</dependency>

在测试类上添加注解:

1
2
3
4
5
6
7
8
9
10
11
12
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestConfiguration.class)
public class TestMain {

@Autowired
TestService service;

@Test
public void test(){
service.test();
}
}