一、入门
1、需求
基于spring创建对象
2、开发
1.创建模块、添加依赖
<!--spring context依赖-->
<!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.2</version>
</dependency>
<!--junit5测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.1</version>
</dependency>
<!-- lombok相关依赖 -->
<!--
@Data //getter、setter、equals、hashCode、toString
@AllArgsConstructor //有参构造
@NoArgsConstructor //无参构造
-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</dependency>
2.创建java类
package com.it.spring6;
public class User {
public void add(){
System.out.println("add....");
}
}
3.创建配置文件
<?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">
<!--完成user对象创建
id属性:唯一标识
class属性:类的全路径
-->
<bean id="user" class="com.it.spring6.User"></bean>
</beans>
4.创建测试类
package com.it.spring6;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestUser {
@Test
public void TestUserObject(){
//1.加载spring配置文件,对象创建
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
//2.获取创建的对象
User user = (User) context.getBean("user");
System.out.println(user);
//3.使用对象调用方法进行测试
user.add();
}
}
3、Log4j2
1.Log4j2主要由几个重要的组件构成:
日志信息的优先级,日志信息的优先级从高到低有TRACE < DEBUG < INFO < WARN < ERROR < FATAL
TRACE:追踪,是最低的日志级别,相当于追踪程序的执行
DEBUG:调试,一般在开发中,都将其设置为最低的日志级别
INFO:信息,输出重要的信息,使用较多
WARN:警告,输出警告的信息
ERROR:错误,输出错误信息
FATAL:严重错误
2.Log4j2依赖
<!--log4j2的依赖-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.19.0</version>
</dependency>
3.配置文件(log4j2.xml)
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<loggers>
<!--
level指定日志级别,从低到高的优先级:
TRACE < DEBUG < INFO < WARN < ERROR < FATAL
trace:追踪,是最低的日志级别,相当于追踪程序的执行
debug:调试,一般在开发中,都将其设置为最低的日志级别
info:信息,输出重要的信息,使用较多
warn:警告,输出警告的信息
error:错误,输出错误信息
fatal:严重错误
-->
<root level="DEBUG">
<appender-ref ref="spring6log"/>
<appender-ref ref="RollingFile"/>
<appender-ref ref="log"/>
</root>
</loggers>
<appenders>
<!--输出日志信息到控制台-->
<console name="spring6log" target="SYSTEM_OUT">
<!--控制日志输出的格式-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/>
</console>
<!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用-->
<File name="log" fileName="f:/spring6_log/test.log" append="false">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
</File>
<!-- 这个会打印出所有的信息,
每次大小超过size,
则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,
作为存档-->
<RollingFile name="RollingFile" fileName="f:/spring6_log/app.log"
filePattern="log/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">
<PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
<SizeBasedTriggeringPolicy size="50MB"/>
<!-- DefaultRolloverStrategy属性如不设置,
则默认为最多同一文件夹下7个文件,这里设置了20 -->
<DefaultRolloverStrategy max="20"/>
</RollingFile>
</appenders>
</configuration>
4.使用日志
private Logger logger = LoggerFactory.getLogger(TestUser.class);
//这里直接调就行
logger.info("###执行成功....");
二、IOC
1、IOC容器
1)控制反转(IOC)
控制反转是一种思想。
控制反转是为了降低程序耦合度,提高程序扩展力。
控制反转,反转的是什么?
将对象的创建权利交出去,交给第三方容器负责。
将对象和对象之间关系的维护权交出去,交给第三方容器负责。
控制反转这种思想如何实现呢?
DI(Dependency Injection):依赖注入
2)依赖注入
依赖注入实现了控制反转的思想。
指Spring创建对象的过程中,将对象依赖属性通过配置进行注入
总结:IOC 就是一种控制反转的思想, 而 DI 是对IOC的一种具体实现。
3)IOC容器在Spring的实现
Spring 的 IOC 容器就是 IOC思想的一个落地的产品实现。IOC容器中管理的组件也叫做 bean。在创建 bean 之前,首先需要创建IOC 容器。Spring 提供了IOC 容器的两种实现方式:
①BeanFactory
这是 IoC 容器的基本实现,是 Spring 内部使用的接口。面向 Spring 本身,不提供给开发人员使用。
②ApplicationContext
BeanFactory 的子接口,提供了更多高级特性。面向 Spring 的使用者,几乎所有场合都使用 ApplicationContext 而不是底层的 BeanFactory。
类型名 | 简介 |
---|---|
ClassPathXmlApplicationContext | 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象 |
FileSystemXmlApplicationContext | 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象 |
ConfigurableApplicationContext | ApplicationContext 的子接口,包含一些扩展方法 refresh() 和 close() ,让 ApplicationContext 具有启动、关闭和刷新上下文的能力。 |
WebApplicationContext | 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。 |
2、基于XML管理Bean
1.获取bean
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
//1.基于id创建对象
User user = (User) context.getBean("user");
System.out.println("1.基于id创建对象:"+user);
//2.基于类型创建对象
User bean = context.getBean(User.class);
System.out.println("2.基于类型创建对象:"+user);
//3.基于id和类型一起创建对象
context.getBean("user",User.class);
System.out.println("3.基于id和类型一起创建对象:"+user);
注意:
获取对象时如果出现二义性则会抛异常
NoUniqueBeanDefinitionException
2.依赖注入
创建测试类Book
package com.it.spring6.iocxml.di;
//自己重写setter方法,无参构造,有参构造,否则注入不了,toString也重写方便测试
public class Book {
private String bname;
private String author;
}
1)基于setter注入
<!--1.set方法注入-->
<bean id="book" class="com.it.spring6.iocxml.di.Book">
<!-- property底层会先调无参构造,在直接调用setter方法 -->
<property name="bname" value="java"></property>
<property name="author" value="尚硅谷"></property>
</bean>
2)基于构造器注入
<!--2.构造器注入-->
<bean id="bookCon" class="com.it.spring6.iocxml.di.Book">
<!-- constructor-arg底层会调有参构造 -->
<constructor-arg name="bname" value="前端"></constructor-arg>
<constructor-arg name="author" value="黑马"></constructor-arg>
</bean>
让后正常获取就可以了
3.特殊值处理
1)null值
<property name="bname">
<!-- 这样就可以注入null了 -->
<null />
</property>
2)xml实体
<!-- 比如:<>,必须用<代替 -->
<property name="expression" value="a < b"/>
3)CDATA节
<property name="expression">
<!-- 可以输入<>,但必须要这么写 -->
<value><![CDATA[a < b]]></value>
</property>
4.对象类型属性注入
创建部门和员工类
//部门类(自己重写方法)
public class Dept {
private String dname;
public void info(){
System.out.println("部门名称:"+dname);
}
}
// 员工类
public class Emp {
//员工属于某个部门
private Dept dept;
private String dname;
private Integer age;
public void work(){
System.out.println("员工正在工作...");
dept.info();
}
}
1)引用外部bean
<!-- 注入部门类 -->
<bean id="dept" class="com.it.spring6.iocxml.ditest.Dept">
<property name="dname" value="产品部"></property>
</bean>
<!-- 注入员工类 -->
<bean id="emp" class="com.it.spring6.iocxml.ditest.Emp">
<!-- 这里在注入对象时,用ref属性,引用上面写的部门id,这种就叫引用外部bean -->
<property name="dept" ref="dept"></property>
</bean>
2)内部bean
<bean id="emp2" class="com.it.spring6.iocxml.ditest.Emp">
<property name="dname" value="李四"></property>
<property name="age" value="20"></property>
<property name="dept">
<!-- 内部嵌入一个bean -->
<bean id="dept2" class="com.it.spring6.iocxml.ditest.Dept">
<property name="dname" value="销售部"></property>
</bean>
</property>
</bean>
3)级联属性赋值(不常用)
<bean id="dept3" class="com.it.spring6.iocxml.ditest.Dept">
<property name="dname" value="技术研发部"></property>
</bean>
<bean id="emp3" class="com.it.spring6.iocxml.ditest.Emp">
<!-- 必须要先用外部引用,否则级联赋值失效 -->
<property name="dept" ref="dept3"></property>
<!-- 级联属性赋值,这个属性必须有getter方法 -->
<property name="dept.dname" value="测试部"></property>
</bean>
5.数组类型属性注入
1)修改员工类
// 添加爱好属性
private String[] loves;
2)配置bean
<bean id="emp" class="com.it.spring6.iocxml.ditest.Emp">
<!-- 数组类型注入 -->
<property name="loves">
<array>
<!-- 这里可以填多个value标签 -->
<value>吃饭</value>
<value>睡觉</value>
<value>敲代码</value>
</array>
</property>
</bean>
6.集合类型属性注入
1)List集合注入
修改部门类
//一个部门有多个员工
private List<Emp> empList;
<bean id="emp1" class="com.it.spring6.iocxml.ditest.Emp">
<property name="dname" value="张三"></property>
<property name="age" value="18"></property>
</bean>
<bean id="emp2" class="com.it.spring6.iocxml.ditest.Emp">
<property name="dname" value="李四"></property>
<property name="age" value="20"></property>
</bean>
<bean id="dept" class="com.it.spring6.iocxml.ditest.Dept">
<property name="dname" value="技术部"></property>
<property name="empList">
<!-- list类型注入(跟数组类似) -->
<list>
<!-- 这里注入的是对象,所以不能用value标签 -->
<ref bean="emp1"></ref>
<ref bean="emp2"></ref>
</list>
</property>
</bean>
2)Map集合注入
创建学生和老师类
//自己重写setter、toString等方法
public class Student {
//一个学生有多个老师
private Map<String,Teacher> teacherMap;
private String sid;
private String sname;
}
public class Teacher {
private String tId;
private String tName;
}
<bean id="teacher" class="com.it.spring6.iocxml.dimap.Teacher">
<property name="tId" value="100"></property>
<property name="tName" value="陈老师"></property>
</bean>
<bean id="student" class="com.it.spring6.iocxml.dimap.Student">
<property name="teacherMap">
<!-- map类型注入(跟list类似) -->
<map>
<!-- 一个entry就是一对key,value -->
<entry>
<!-- 因为key是String所以里面要用value接受 -->
<key>
<value>10010</value>
</key>
<!-- 因为value是对象所以要用ref接受 -->
<ref bean="teacher"></ref>
</entry>
</map>
</property>
</bean>
3)引用集合类型的bean(另一种方式)
添加课程类和学生属性
//课程类
public class Lesson {
private String lessonName;
}
//在学生类里添加list属性
//一个学生可以上多门课
private List<Lesson> lessonList;
修改xml的约束添加,否则定义不了外部集合标签
<?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:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>
配置bean
<bean id="student" class="com.it.spring6.iocxml.dimap.Student">
<property name="sid" value="10001"></property>
<property name="sname" value="张三"></property>
<!-- 这里直接通过外部定义的list标签的id引用该list -->
<property name="lessonList" ref="lessonList"></property>
<!-- 这里直接通过外部定义的map标签的id引用该map -->
<property name="teacherMap" ref="teacherMap"></property>
</bean>
<!-- 定义list标签,里面的内容一致 -->
<util:list id="lessonList">
<ref bean="lesson1"></ref>
<ref bean="lesson2"></ref>
</util:list>
<!-- 定义map标签,里面的内容一致 -->
<util:map id="teacherMap">
<entry>
<key>
<value>10010</value>
</key>
<ref bean="teacher1"></ref>
</entry>
<entry>
<key>
<value>20020</value>
</key>
<ref bean="teacher2"></ref>
</entry>
</util:map>
<bean id="lesson1" class="com.it.spring6.iocxml.dimap.Lesson">
<property name="lessonName" value="java开发"></property>
</bean>
<bean id="lesson2" class="com.it.spring6.iocxml.dimap.Lesson">
<property name="lessonName" value="前端开发"></property>
</bean>
<bean id="teacher1" class="com.it.spring6.iocxml.dimap.Teacher">
<property name="teacherName" value="陈老师"></property>
<property name="teacherId" value="100"></property>
</bean>
<bean id="teacher2" class="com.it.spring6.iocxml.dimap.Teacher">
<property name="teacherName" value="王老师"></property>
<property name="teacherId" value="200"></property>
</bean>
7.p命名空间注入
自定义命名空间p
bean注入
<bean id="studentp" class="com.it.spring6.iocxml.dimap.Student"
p:sid="20001" p:sname="李四" p:lessonList-ref="lessonList" p:teacherMap-ref="teacherMap"></bean>
8.引入外部属性文件注入
1)加入依赖
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
2)创建jdbc.properties配置文件
jdbc.user=root
jdbc.password=root
jdbc.url=jdbc:mysql://localhost:3306/spring?serverTimezone=UTC
jdbc.driver=com.mysql.cj.jdbc.Driver
3)修改命名空间
4)配置bean
<!-- 引入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!-- 完成数据库信息注入 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<!-- 使用${}引用数据库配置文件里的内容 -->
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="driverClassName" value="${jdbc.driver}"></property>
</bean>
5)一样的获取bean
9.bean的作用域
1)概念
在Spring中可以通过配置bean标签的scope属性来指定bean的作用域范围,如:
取值 | 含义 | 创建对象的时机 |
---|---|---|
singleton(默认) | 在IOC容器中,这个bean的对象始终为单实例 | IOC容器初始化时 |
prototype | 这个bean在IOC容器中有多个实例 | 获取bean时 |
2)创建一个Orders类,做测试
3)配置bean
<!-- 通过scope属性配置单实例 多实例 -->
<bean id="orders" class="com.it.spring6.iocxml.scope.Orders"
scope="prototype"></bean>
10.bean的生命周期
1)具体的生命周期过程
1、bean对象创建(调用无参构造)
2、给bean对象设置相关属性
3、bean后置处理器(初始化之前)
4、bean对象初始化(调用指定初始化方法)
5、bean后置处理器(初始化之后)
6、bean对象创建完成了,可以使用了
7、bean对象销毁(配置指定销毁的方法)
2)创建User类
1、无参构造
2、setter
<!-- 4、init-method引用User里面的初始化方法 -->
<!-- 7、destroy-method引用User里面的销毁方法 -->
<bean id="user" class="com.it.spring6.iocxml.life.User"
init-method="initMethod" destroy-method="destroyMethod">
<property name="name" value="张三"></property>
</bean>
注:7、必须要调.close()方法才能销毁
3)bean的后置处理器
创建MyBeanPost类,实现BeanPostProcessor接口,并重写全部方法
public class MyBeanPost implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("3 bean后置处理器,初始化之前执行");
System.out.println(beanName+"::"+bean);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("5 bean后置处理器,初始化之后执行");
System.out.println(beanName+"::"+bean);
return bean;
}
}
配置bean
<bean id="myBeanPost" class="com.it.spring6.iocxml.life.MyBeanPost"></bean>
11.FactoryBean
1)简介
FactoryBean是Spring提供的一种整合第三方框架的常用机制。和普通的bean不同,在获取bean时,得到是getObject()方法返回的对象。通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。
一般用来整合Mybatis时,Spring就是通过FactoryBean机制来帮我们创建SqlSessionFactory对象的。
2)创建MyFactoryBean
public class MyFactoryBean implements FactoryBean<User> {
@Override
public User getObject() throws Exception {
return new User();
}
@Override
public Class<?> getObjectType() {
return User.class;
}
}
3)配置bean
<bean id="myFactoryBean" class="com.it.spring6.iocxml.factorybean.MyFactoryBean"></bean>
4)这时获取的bean就是User,而不是MyFactoryBean
12.自动装配
1)定义:
根据指定的策略,在IOC容器中匹配某一个bean,自动为指定的bean中所依赖的类类型或接口类型属性赋值
2)场景模拟,创建三层架构,并创建add添加接口
public class UserController {
//自己重写setter,和调用UserService
private UserService userService;
}
//自己创建UserService
public class UserServiceImpl implements UserService{
//自己重写setter,和调用UserService
private UserDao userDao;
}
public class UserDaoImpl implements UserDao {
}
3)自动装配bean
<!-- autowire表示自动装配,引用byType表示根据类型自动装配 -->
<bean id="userController" class="com.it.spring6.iocxml.auto.controller.UserController"
autowire="byType"></bean>
<!-- 这里尽量用实现类,因为根据类型进行注入要保证类型唯一 -->
<bean id="userServiceImpl" class="com.it.spring6.iocxml.auto.service.UserServiceImpl"
autowire="byType"></bean>
<bean id="UserDaoImpl" class= "com.it.spring6.iocxml.auto.dao.UserDaoImpl"
autowire="byType"></bean>
注:如果是根据名称进行注入,必须保证注入的属性和id名保持一致
4)自己测试
3、基于注解管理Bean
从 Java 5 开始,Java 增加了对注解(Annotation)的支持,它是代码中的一种特殊标记,可以在编译、类加载和运行时被读取,执行相应的处理。开发人员可以通过注解在不改变原有代码和逻辑的情况下,在源代码中嵌入补充信息。
Spring 从 2.5 版本开始提供了对注解技术的全面支持,我们可以使用注解来实现自动装配,简化 Spring 的 XML 配置。
Spring 通过注解实现自动装配的步骤如下:
1.引入依赖
2.开启组件扫描
3.使用注解定义 Bean
4.依赖注入
1.开启组件扫描
Spring 默认不使用注解装配 Bean,因此我们需要在 Spring 的 XML 配置中,通过 context:component-scan 元素开启 Spring Beans的自动扫描功能。开启此功能后,Spring 会自动从扫描指定的包(base-package 属性设置)及其子包下的所有类。
1)定义contenxt命名空间
<!--开启组件扫描功能-->
<context:component-scan base-package="com.it"></context:component-scan>
2.使用注解定义Bean
注解 | 说明 |
---|---|
@Component | 该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。 |
@Repository | 该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
@Service | 该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
@Controller | 该注解通常作用在控制层(如SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
//value属性可以不写,默认就是类名首字母小写
@Component(value = "user") //value属性相当于在bean标签中设置id="user"
public class User {
}
3.@Autowired注入
单独使用@Autowired注解,默认根据类型装配。【默认是byType】
创建三层架构,并交给IOC容器管理
//自己创建add添加的测试接口
@Controller
public class UserController {
private UserService userService;
}
//自己创建UserService
@Service
public class UserServiceImpl implements UserService{
private UserDao userDao;
}
@Repository
public class UserDaoImpl implements UserDao {
}
1)属性注入
//第一种方式 属性注入
@Autowired
private UserService userService; //根据类型找到对应对象,完成注入
2)set注入
//第二种方式 setter方法注入
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
3)构造方法注入
//第三种方式 构造方法注入
private UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
4)形参上注入
//第四种方式 形参上注入
private UserService userService;
public UserController(@Autowired UserService userService) {
this.userService = userService;
}
5)只有一个构造方法,无注解
//第五种方式 只有一个有参构造,才能实现无注解注入
private UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
6)@Autowired注解和@Qualifier注解联合
//第六种方式 当UserService有多个实现类是,就必须根据名称进行注入
@Autowired
@Qualifier(value = "userRedisServiceImpl")
private UserService userService;
4.@Resource注入
@Resource注解是JDK扩展包中的,也就是说属于JDK的一部分。所以该注解是标准注解,更加具有通用性。(JSR-250标准中制定的注解类型。JSR是Java规范提案。)
@Resource注解默认根据名称byName进行注入,未指定name属性时会默认根据属性名称进行注入。如果name找不到的话会自动根据类型byType注入。
@Resource注解用在属性上、setter方法上。
@Resource注解属于JDK扩展包,所以不在JDK当中,需要额外引入以下依赖:【高于JDK11或低于JDK8需要引入以下依赖。】
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
5.Spring全注解开发
全注解开发就是不再使用spring配置文件了,写一个配置类来代替配置文件。
@Configuration //配置类
@ComponentScan("com.it") //开启组件扫描(如果不指定路径,则会扫描当前目录的平级目录)
public class SpringConfig {
}
测试
//通过AnnotationConfigApplicationContext创建
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
//其他保持一致
UserController userController = context.getBean("myUserController", UserController.class);
userController.addUser();
三、手写IOC
1、创建@Bean和@Di注解
//实现控制反转
@Target(ElementType.TYPE) //在类上能用
@Retention(RetentionPolicy.RUNTIME) //在运行时生效
public @interface Bean {
}
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Di {
}
2、创建UserDaoImpl和UserServicelmpl
//UserDao接口自己创建
@Bean
public class UserDaoImpl implements UserDao {
@Override
public void add() {
System.out.println("dao......");
}
}
//UserService接口自己创建
@Bean
public class UserServiceImpl implements UserService {
@Di
private UserDao userDao;
@Override
public void add() {
System.out.println("service.......");
userDao.add();
}
}
3、模拟创建ApplicationContext接口
public interface ApplicationContext {
Object getBean(Class clazz);
}
4、实现ApplicationContext接口
package com.it.bean;
import com.it.anno.Bean;
import com.it.anno.Di;
import java.io.File;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class AnnotationApplicationContext implements ApplicationContext{
//创建map集合,放bean对象
private Map<Class,Object> beanFactory = new HashMap<>();
private static String rootPath;
//返回对象
@Override
public Object getBean(Class clazz) {
return beanFactory.get(clazz);
}
//创建有参构造,传递包路径,设置包扫描规则
//当前包及其子包,哪个类有@Bean注解,把这个类通过反射实例化
public AnnotationApplicationContext(String basePackage) {
try {
//com.it
//1 把.替换成\
String packagePath = basePackage.replaceAll("\\.", "\\\\");
//2 获取包绝对路径
Enumeration<URL> urls
= Thread.currentThread().getContextClassLoader().getResources(packagePath);
while (urls.hasMoreElements()){
URL url = urls.nextElement();
String filePath = URLDecoder.decode(url.getFile(), "utf-8");
//获取包前面路径部分,字符串截取
rootPath = filePath.substring(0,filePath.length()-packagePath.length());
//包扫描
loadBean(new File(filePath));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
//属性注入
loadDi();
}
//包扫描过程,实例化
private void loadBean(File file) throws Exception {
//1 判断当前是否为文件夹
if (file.isDirectory()){
//2 获取文件夹里面所以内容
File[] childrenFiles = file.listFiles();
//3 判断文件夹里面为空,直接返回
if (childrenFiles == null || childrenFiles.length == 0){
return;
}
//4 如果文件夹里面不为空,遍历文件夹所以内容
for (File child : childrenFiles) {
//4.1 遍历得到每个File对象,继续判断,如果还是文件,递归
if (child.isDirectory()){
// 递归
loadBean(child);
} else {
//4.2 遍历得到File对象不是文件夹,是文件,
//4.3 得到包路径+类名称部分-字符串截取
String pathWithClass = child.getAbsolutePath().substring(rootPath.length() - 1);
//4.4 判断当前文件类型是否.class
if (pathWithClass.contains(".class")){
//4.5 如果是.class类型,把路径\替换成. 把.class去掉
// com.it.service.UserServiceImpl
String allName = pathWithClass.replaceAll("\\\\", ".").replace(".class", "");
//4.6 判断类上面是否有注解@Bean,如果有实例化过程
//4.6.1 获取类的Class
Class<?> clazz = Class.forName(allName);
//4.6.2 判断不是接口
if (!clazz.isInterface()){
//4.6.3 判断类上面是否有注解@Bean
Bean annotation = clazz.getAnnotation(Bean.class);
if (annotation != null){
//4.6.4 实例化
Object instance = clazz.getConstructor().newInstance();
//4.7 把对象实例化之后,放到map集合beanFactory
//4.7.1 判断当前类如果有接口,让接口class作为map的key
if (clazz.getInterfaces().length > 0){
beanFactory.put(clazz.getInterfaces()[0],instance);
} else {
beanFactory.put(clazz,instance);
}
}
}
}
}
}
}
}
//属性注入
private void loadDi() {
//实例化对象在beanFactory的map集合里面
//1 遍历beanFactory的map集合
Set<Map.Entry<Class, Object>> entries = beanFactory.entrySet();
for (Map.Entry<Class, Object> entry : entries) {
//2 获取map集合每个对象(value),每个对象属性获取到
Object obj = entry.getValue();
//获取对象Class
Class<?> clazz = obj.getClass();
//获取每个对象属性
Field[] declaredFields = clazz.getDeclaredFields();
//3 遍历得到每个对象属性数组,得到每个属性
for (Field field : declaredFields) {
//4 判断属性上面是否有@Di注解
Di annotation = field.getAnnotation(Di.class);
if (annotation != null){
//如果私有属性,设置可以设置值
field.setAccessible(true);
//5 如果有@Di注解,把对象进行设置(注入)
try {
field.set(obj,beanFactory.get(field.getType()));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
5、测试
ApplicationContext context = new AnnotationApplicationContext("com.it");
UserService userService = (UserService) context.getBean(UserService.class);
System.out.println(userService);
userService.add();
四、AOP
1、动态代理
1)创建业务类
public interface Calculator {
//加法
int add(int i, int j);
//减法
int sub(int i, int j);
//乘法
int mul(int i, int j);
//除法
int div(int i, int j);
}
public class CalculatorImpl implements Calculator{
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
2)创建代理类ProxyFactory
public class ProxyFactory {
//目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
//返回代理对象
public Object getProxy() {
/**
* Proxy.newProxyInstance()方法
* 有三个参数
*/
//第一个参数:ClassLoader: 加载动态生成代理类的来加载器
ClassLoader classLoader = target.getClass().getClassLoader();
//第二个参数: Class[] interfaces:目录对象实现的所有接口的class类型数组
Class<?>[] interfaces = target.getClass().getInterfaces();
//第三个参数:InvocationHandler:设置代理对象实现目标对象方法的过程
InvocationHandler invocationHandler =new InvocationHandler() {
//第一个参数:代理对象
//第二个参数:需要重写目标对象的方法
//第三个参数:method方法里面参数
@Override
public Object invoke(Object proxy,
Method method,
Object[] args) throws Throwable {
//方法调用之前输出
System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args));
//调用目标的方法
Object result = method.invoke(target, args);
//方法调用之后输出
System.out.println("[动态代理][日志] "+method.getName()+",结果:"+ result);
return result;
}
};
return Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
}
}
3)测试
//创建代理对象(动态)
ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
Calculator proxy = (Calculator)proxyFactory.getProxy();
//proxy.add(1,2);
proxy.mul(2,4);
2、AOP定义
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
①横切关注点
分散在每个各个模块中解决同一样的问题,如用户验证、日志管理、事务处理、数据缓存都属于横切关注点。
②通知(增强)
增强,通俗说,就是你想要增强的功能,比如 安全,事务,日志等。
1)前置通知:在被代理的目标方法前执行
2)返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
3)异常通知:在被代理的目标方法异常结束后执行(死于非命)
4)后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
5)环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置(常用)
③切面
封装通知方法的切面类。
④目标
被代理的目标对象。
⑤代理
向目标对象应用通知之后创建的代理对象。
⑥连接点
这也是一个纯逻辑概念,不是语法定义的。
把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。通俗说,就是spring允许你使用通知的地方
⑦切入点
定位连接点的方式。
Spring 的 AOP 技术可以通过切入点定位到特定的连接点。通俗说,要实际去增强的方法
3、基于注解实现AOP
1.技术说明
动态代理分为JDK动态代理和cglib动态代理
AspectJ:是AOP思想的一种实现。本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
2.准备工作
1)添加依赖
<!--spring aop依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.2</version>
</dependency>
<!--spring aspects依赖(实现aop的相关注解)-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.2</version>
</dependency>
2)把上面动态代理用到的业务类拷贝一份(加入IOC,不然测试会有问题)
3)配置bean
<?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: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/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">
<!-- 开启组件扫描 -->
<context:component-scan base-package="com.it.spring6.aop.annoaop"></context:component-scan>
<!-- 开启aspectj自动代理,为目标对象生成代理,这样才会认识@Aspect注解 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
3.创建切面类并配置
@Aspect //切面类
@Component //ioc容器
public class LogAspect {
// 设置切入点和通知类型
// 切入点表达式:execution(访问修饰符 增强方法返回类型 增强方法所在类全路径.方法名称(方法参数))
// 通知类型:
//1.前置 @Before(value = "切入点表达式配置切入点")
@Before(value = "execution(* com.it.spring6.aop.annoaop.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("Logger-->前置通知,方法名称:"+methodName+",参数:"+ Arrays.toString(args));
}
//2.返回 @AfterReturning
@AfterReturning(value = "execution(* com.it.spring6.aop.annoaop.CalculatorImpl.*(..))"
//returning这里定义的名称和形参的名称保持一致,表示原类的返回值
,returning = "result")
public void afterReturningMethod(JoinPoint joinPoint,Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名称:"+methodName+",返回结果:"+result);
}
//3.后置 @After()
@After(value = "execution(* com.it.spring6.aop.annoaop.CalculatorImpl.*(..))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->后置通知,方法名称:"+methodName);
}
//4.异常 @AfterThrowing 获取到目标方法异常信息
//目标方法出现异常,这个通知执行
@AfterThrowing(value = "execution(* com.it.spring6.aop.annoaop.CalculatorImpl.*(..))"
,throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint,Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名称:"+methodName+",异常信息:"+ex);
}
//5.环绕 @Around()
@Around(value = "execution(* com.it.spring6.aop.annoaop.CalculatorImpl.*(..))")
public Object afterThrowingMethod(ProceedingJoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
Object result = null;
try {
System.out.println("环绕通知==目标方法之前执行");
//调用目标方法
result = joinPoint.proceed();
System.out.println("环绕通知==目标方法返回值之后");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知==目标方法出现异常执行");
} finally {
System.out.println("环绕通知==目标方法执行完毕执行");
}
return result;
}
}
4.切入点表达式
5.重用切入点表达式
//这样可以直接用这个方法引用改切入点表达式,解决冗余问题
@Pointcut(value = "execution(* com.it.spring6.aop.annoaop.CalculatorImpl.*(..))")
public void pointCut(){}
6.切面的优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
1)优先级高的切面:外面
2)优先级低的切面:里面
使用@Order注解可以控制切面的优先级:
1)@Order(较小的数):优先级高
2)@Order(较大的数):优先级低
4、基于XML实现AOP
1.准备工作
参考基于注解的AOP环境,去掉切面类上AOP相关的注解
2.实现
<?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: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/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">
<!-- 开启组件扫描 -->
<context:component-scan base-package="com.it.spring6.aop.annoaop"></context:component-scan>
<!--配置aop五种通知类型-->
<aop:config>
<!--配置切面类-->
<aop:aspect ref="logAspect">
<!--配置切入点-->
<aop:pointcut id="pointcut" expression="execution(* com.it.spring6.aop.xmlaop.CalculatorImpl.*(..))"/>
<!--配置五种通知类型-->
<!--前置通知-->
<aop:before method="beforeMethod" pointcut-ref="pointcut"></aop:before>
<!--后置通知-->
<aop:after method="afterMethod" pointcut-ref="pointcut"></aop:after>
<!--返回通知-->
<aop:after-returning method="afterReturningMethod" returning="result" pointcut-ref="pointcut"></aop:after-returning>
<!--异常通知-->
<aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointcut"></aop:after-throwing>
<!--环绕通知-->
<aop:around method="aroundMethod" pointcut-ref="pointcut"></aop:around>
</aop:aspect>
</aop:config>
</beans>
五、单元测试JUnit
在之前的测试方法中,几乎都能看到以下的两行代码:
ApplicationContext context = new ClassPathXmlApplicationContext("xxx.xml");
Xxxx xxx = context.getBean(Xxxx.class);
这两行代码的作用是创建Spring容器,最终获取到对象,但是每次测试都需要重复编写。针对上述问题,我们需要的是程序能自动帮我们创建容器。我们都知道JUnit无法知晓我们是否使用了 Spring 框架,更不用说帮我们创建 Spring 容器了。Spring提供了一个运行器,可以读取配置文件(或注解)来创建容器。我们只需要告诉它配置文件位置就可以了。这样一来,我们通过Spring整合JUnit可以使程序创建spring容器了
1、整合JUnit5
1)引入依赖
<!--spring对junit的支持相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.2</version>
</dependency>
<!--junit5测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.1</version>
</dependency>
2)创建业务类
@Component
public class User {
public void run(){
System.out.println("run....");
}
}
3)配置bean
<?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"
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:component-scan base-package="com.it.spring6.junit"></context:component-scan>
</beans>
4)编写测试类
import org.junit.jupiter.api.Test; //这是JUnit5的包名,如果引入的是4则会报错
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
//如果想加载配置类,用value属性传入Class即可
//这样可以直接加载配置文件,直接注入就可以了
@SpringJUnitConfig(locations = "classpath:bean.xml")
public class SpringTestJunit5 {
@Autowired
private User user;
@Test
public void testUser(){
System.out.println(user);
user.run();
}
}
2、整合JUnit4
JUnit4在公司也会经常用到,在此也学习一下
1)添加依赖
<!-- junit4测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
2)测试
import com.atguigu.spring6.bean.User;
import org.junit.Test; //这是JUnit4的包名,如果引入的是5则会报错
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:beans.xml")
public class SpringJUnit4Test {
@Autowired
private User user;
@Test
public void testUser(){
System.out.println(user);
}
}
六、事务
1、JdbcTemplate
Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作
1.准备工作
1)引入依赖
<dependencies>
<!--spring jdbc Spring 持久化层支持jar包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.2</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
</dependencies>
2)创建jdbc.properties
jdbc.user=root
jdbc.password=root
jdbc.url=jdbc:mysql://192.168.6.100:3306/spring?characterEncoding=utf8&useSSL=false
jdbc.driver=com.mysql.cj.jdbc.Driver
3)配置Spring配置文件beans.xml
<?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"
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="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"></property>
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="username" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 创建jdbcTemplate对象,注入数据源 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
</beans>s
4)准备数据库和测试表
CREATE DATABASE `spring`;
use `spring`;
CREATE TABLE `t_emp` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL COMMENT '姓名',
`age` int(11) DEFAULT NULL COMMENT '年龄',
`sex` varchar(2) DEFAULT NULL COMMENT '性别',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
5)创建实体类
public class Emp {
private Integer id;
private String name;
private Integer age;
private String sex;
}
2.实现CURD
1)添加
String sql = "INSERT INTO t_emp VALUES (null,?,?,?)";
int i = jdbcTemplate.update(sql, "李四",20, "女");
System.out.println(i);
2)修改
String sql = "UPDATE t_emp SET `name` = ?";
int i = jdbcTemplate.update(sql, "李四");
System.out.println(i);
3)删除
String sql = "DELETE FROM t_emp WHERE id = ?";
int i = jdbcTemplate.update(sql, 2);
System.out.println(i);
4)查询:返回对象
//法一:自己重写函数接口
String sql = "SELECT * FROM t_emp WHERE id = ?";
Emp emp = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
int id = rs.getInt("id");
String name = rs.getString("name");
int age = rs.getInt("age");
String sex = rs.getString("sex");
return new Emp(id, name, age, sex);
}, 3);
System.out.println(emp);
//法二:使用函数接口的实现类
String sql = "SELECT * FROM t_emp WHERE id = ?";
Emp emp = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Emp.class), 3);
System.out.println(emp);
5)查询:返回list集合
String sql = "SELECT * FROM t_emp";
List<Emp> query = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Emp.class));
System.out.println(query);
6)查询:返回单个值
String sql = "SELECT count(*) FROM t_emp";
//Integer.class 这是返回类型的class
Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println(count);
2、声明式事物概念
1.事务基本概念
数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
A:原子性(Atomicity)
一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
C:一致性(Consistency)
事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。
如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。
如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。
I:隔离性(Isolation)
指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
D:持久性(Durability)
指的是只要事务成功结束,它对数据库所做的更新就必须保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。
2.编程式事务
事务功能的相关操作全部通过自己编写代码来实现:
Connection conn = ...;
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 提交事务
conn.commit();
}catch(Exception e){
// 回滚事务
conn.rollBack();
}finally{
// 释放数据库连接
conn.close();
}
编程式的实现方式存在缺陷:
- 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
3.声明式事务
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
- 好处1:提高开发效率
- 好处2:消除了冗余的代码
- 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化
所以,我们可以总结下面两个概念:
- 编程式:自己写代码实现功能
- 声明式:通过配置让框架实现功能
3、基于注解实现声明式事物
1.搭建案例环境
1)添加配置beans.xml
<!--扫描组件-->
<context:component-scan base-package="com.atguigu.spring6"></context:component-scan>
2)创建表
CREATE TABLE `t_book` (
`book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
`price` int(11) DEFAULT NULL COMMENT '价格',
`stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
PRIMARY KEY (`book_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
CREATE TABLE `t_user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(20) DEFAULT NULL COMMENT '用户名',
`balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
insert into `t_book`(`book_id`,`book_name`,`price`,`stock`)
values (1,'java编程思想',80,100),(2,'算法导论',50,100);
insert into `t_user`(`user_id`,`username`,`balance`)
values (1,'admin',500);
3)创建三层架构
@Controller
public class BookController {
@Autowired
private BookService bookService;
//买书的方法:图书id和用户id
public void buyBook(Integer bookId,Integer userId){
//调用service方法
bookService.buyBook(bookId,userId);
}
}
public interface BookService {
void buyBook(Integer bookId,Integer userId);
}
@Service
public class BookServiceImpl implements BookService{
@Autowired
private BookDao bookDao;
//买书的方法:图书id和用户id
@Transactional
@Override
public void buyBook(Integer bookId, Integer userId) {
//根据图书id查询图书价格
Integer price = bookDao.getBookPriceByBookId(bookId);
//更新图书表库存量 -1
bookDao.updateStock(bookId);
//更新用户表用户余额 -图书价格
bookDao.updateUserBalance(userId,price);
}
}
public interface BookDao {
//根据图书id查询图书价格
Integer getBookPriceByBookId(Integer bookId);
//更新图书表库存量 -1
void updateStock(Integer bookId);
//更新用户表用户余额 -图书价格
void updateUserBalance(Integer userId, Integer price);
}
@Repository
public class BookDaoImpl implements BookDao{
@Autowired
private JdbcTemplate jdbcTemplate;
//根据图书id查询图书价格
@Override
public Integer getBookPriceByBookId(Integer bookId) {
String sql = "SELECT price FROM t_book WHERE book_id = ?";
Integer price = jdbcTemplate.queryForObject(sql,Integer.class, bookId);
return price;
}
//更新图书表库存量 -1
@Override
public void updateStock(Integer bookId) {
String sql = "update t_book set stock = stock-1 WHERE book_id = ?";
jdbcTemplate.update(sql,bookId);
}
//更新用户表用户余额 -图书价格
@Override
public void updateUserBalance(Integer userId, Integer price) {
String sql = "update t_user set balance = balance-? WHERE user_id = ?";
jdbcTemplate.update(sql,price,userId);
}
}
4)测试无事务情况
@SpringJUnitConfig(locations = "classpath:beans.xml")
public class TestBookTx {
@Autowired
private BookController bookController;
@Test
public void testBuyBook(){
bookController.buyBook(1,1);
}
}
2.案例功能实现
1)模拟场景
用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额
假设用户id为1的用户,购买id为1的图书
用户余额为50,而图书价格为80
购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段
此时执行sql语句会抛出SQLException
2)观察结果
因为没有添加事务,图书的库存更新了,但是用户的余额没有更新
显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败
3.案例添加事务
1)在spring配置文件中引入tx命名空间
<?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:tx="http://www.springframework.org/schema/tx"
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
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
</beans>
2)在Spring的配置文件中添加配置:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<!--
开启事务的注解驱动
通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
-->
<!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
<tx:annotation-driven transaction-manager="transactionManager" />
3)在service层添加事务注解@Transactional
4)观察结果
由于使用了Spring的声明式事务,更新库存和更新余额都没有执行
5)@Transactional注解标识的位置
@Transactional标识在方法上,则只会影响该方法
@Transactional标识的类上,则会影响类中所有的方法
4.事务相关属性
1)只读
//如果修改,则会抛SQLException异常
@Transactional(readOnly = true)
2)超时(单位:秒)
//超时则会抛TransactionTimedOutException异常
@Transactional(timeout = 3)
3)回滚策略
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
可以通过@Transactional中相关属性设置回滚策略
-
rollbackFor属性:需要设置一个Class类型的对象
-
rollbackForClassName属性:需要设置一个字符串类型的全类名
-
noRollbackFor属性:需要设置一个Class类型的对象
-
rollbackFor属性:需要设置一个字符串类型的全类名
@Transactional(noRollbackFor = ArithmeticException.class)
//@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
System.out.println(1/0);
}
虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,因此购买图书的操作正常执行
4)事务的隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | 有 | 有 | 有 |
READ COMMITTED | 无 | 有 | 有 |
REPEATABLE READ | 无 | 无 | 有 |
SERIALIZABLE | 无 | 无 | 无 |
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化
5)传播行为
在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。
一共有七种传播行为:
- REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
- SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行**【有就加入,没有就不管了】**
- MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常**【有就加入,没有就抛异常】**
- REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起**【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】**
- NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务**【不支持事务,存在就挂起】**
- NEVER:以非事务方式运行,如果有事务存在,抛出异常**【不支持事务,存在就抛异常】**
- NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】
可以通过@Transactional中的propagation属性设置事务传播行为
@Transactional(propagation = Propagation.REQUIRED),默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。
@Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启的事务,都要开启新事务。
5.全注解配置事务
1)添加配置类
@Configuration //声明配置类
@ComponentScan("com.it.spring6") //扫包
@EnableTransactionManagement //开启事务,这样事务注解才会生效
public class SpringConfig {
@Bean //相当于bean标签,跟配置文件差不多,如果要引入外部属性直接加在形参上
public DruidDataSource getDruidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl("jdbc:mysql://192.168.6.100:3306/spring?characterEncoding=utf8&useSSL=false");
druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
druidDataSource.setUsername("root");
druidDataSource.setPassword("root");
return druidDataSource;
}
@Bean
public JdbcTemplate getJdbcTemplate(DruidDataSource druidDataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(druidDataSource);
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DruidDataSource druidDataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(druidDataSource);
return dataSourceTransactionManager;
}
}
2)测试
@SpringJUnitConfig(value = SpringConfig.class)
public class TxByAllAnnotationTest {
@Autowired
private BookController bookController;
@Test
public void testTxAllAnnotation(){
bookController.buyBook(1,1);
}
}
七、Resources资源操作
Java的标准java.net.URL类和各种URL前缀的标准处理程序无法满足所有对low-level资源的访问,比如:没有标准化的 URL 实现可用于访问需要从类路径或相对于 ServletContext 获取的资源。并且缺少某些Spring所需要的功能,例如检测某资源是否存在等。而Spring的Resource声明了访问low-level资源的能力。
1、Resource接口
Spring 的 Resource 接口位于 org.springframework.core.io 中。 旨在成为一个更强大的接口,用于抽象对低级资源的访问。以下显示了Resource接口定义的方法
public interface Resource extends InputStreamSource {
boolean exists();
boolean isReadable();
boolean isOpen();
boolean isFile();
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFile() throws IOException;
ReadableByteChannel readableChannel() throws IOException;
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename();
String getDescription();
}
2、Resource的实现类
1.UrlResource
Resource的一个实现类,用来访问网络资源,它支持URL的绝对路径。
http:------该前缀用于访问基于HTTP协议的网络资源。
ftp:------该前缀用于访问基于FTP协议的网络资源
file: ------该前缀用于从文件系统中读取资源
//访问网络资源
public class UrlResourceDemo {
public static void main(String[] args) throws Exception {
//访问网络资源
loadAndReadUrlResource("http://www.baidu.com");
}
public static void loadAndReadUrlResource(String path) throws Exception {
Resource url = new UrlResource(path);
// 获取资源名
System.out.println(url.getFilename());
System.out.println(url.getURI());
// 获取资源描述
System.out.println(url.getDescription());
//获取资源内容
System.out.println(url.getInputStream().read());
}
}
2.ClassPathResource
ClassPathResource 用来访问类加载路径下的资源,相对于其他的 Resource 实现类,其主要优势是方便访问类加载路径里的资源,尤其对于 Web 应用,ClassPathResource 可自动搜索位于 classes 下的资源文件,无须使用绝对路径访问。
//访问类路径下资源
public class ClassPathResourceDemo {
public static void main(String[] args) throws Exception {
loadAndReadUrlResource("it.txt");
}
public static void loadAndReadUrlResource(String path) throws Exception{
// 创建一个 Resource 对象
ClassPathResource resource = new ClassPathResource(path);
// 获取文件名
System.out.println("resource.getFileName = " + resource.getFilename());
// 获取文件描述
System.out.println("resource.getDescription = "+ resource.getDescription());
//获取文件内容
InputStream in = resource.getInputStream();
byte[] b = new byte[1024];
int len = 0;
while((len = in.read(b))!=-1) {
System.out.println(new String(b,0,len));
}
}
}
3.FileSystemResource
Spring 提供的 FileSystemResource 类用于访问文件系统资源,使用 FileSystemResource 来访问文件系统资源并没有太大的优势,因为 Java 提供的 File 类也可用于访问文件系统资源。
//访问文件系统资源
public class FileSystemResourceDemo {
public static void main(String[] args) throws Exception {
loadAndReadUrlResource("it.txt");
}
public static void loadAndReadUrlResource(String path) throws Exception{
//相对路径(相对于总工程路径下)
FileSystemResource resource = new FileSystemResource("it.txt");
//绝对路径
// FileSystemResource resource = new FileSystemResource("F:\\it.txt");
// 获取文件名
System.out.println("resource.getFileName = " + resource.getFilename());
// 获取文件描述
System.out.println("resource.getDescription = "+ resource.getDescription());
//获取文件内容
InputStream in = resource.getInputStream();
byte[] b = new byte[1024];
int len = 0;
while((len = in.read(b))!=-1) {
System.out.println(new String(b,0,len));
}
}
}
4.ServletContextResource
这是ServletContext资源的Resource实现,它解释相关Web应用程序根目录中的相对路径。它始终支持流(stream)访问和URL访问,但只有在扩展Web应用程序存档且资源实际位于文件系统上时才允许java.io.File访问。无论它是在文件系统上扩展还是直接从JAR或其他地方(如数据库)访问,实际上都依赖于Servlet容器。
5.InputStreamResource
InputStreamResource 是给定的输入流(InputStream)的Resource实现。它的使用场景在没有特定的资源实现的时候使用(感觉和@Component 的适用场景很相似)。与其他Resource实现相比,这是已打开资源的描述符。 因此,它的isOpen()方法返回true。如果需要将资源描述符保留在某处或者需要多次读取流,请不要使用它。
6.ByteArrayResource
字节数组的Resource实现类。通过给定的数组创建了一个ByteArrayInputStream。它对于从任何给定的字节数组加载内容非常有用,而无需求助于单次使用的InputStreamResource。
3、ResourceLoader 接口
该接口实现类的实例可以获得一个Resource实例。
Resource getResource(String location) : 该接口仅有这个方法,用于返回一个Resource实例。ApplicationContext实现类都实现ResourceLoader接口,因此ApplicationContext可直接获取Resource实例。
public class ResourceLoaderDemo {
@Test
public void demo1(){
ApplicationContext context = new ClassPathXmlApplicationContext();
Resource resource = context.getResource("it.txt");
System.out.println(resource.getFilename());
}
@Test
public void demo2(){
ApplicationContext context = new FileSystemXmlApplicationContext();
Resource resource = context.getResource("it.txt");
System.out.println(resource.getFilename());
}
}
Spring将采用和ApplicationContext相同的策略来访问资源。也就是说,如果ApplicationContext是FileSystemXmlApplicationContext,res就是FileSystemResource实例;如果ApplicationContext是ClassPathXmlApplicationContext,res就是ClassPathResource实例
当Spring应用需要进行资源访问时,实际上并不需要直接使用Resource实现类,而是调用ResourceLoader实例的getResource()方法来获得资源,ReosurceLoader将会负责选择Reosurce实现类,也就是确定具体的资源访问策略,从而将应用程序和具体的资源访问策略分离开来
4、ResourceLoaderAware 接口
ResourceLoaderAware接口实现类的实例将获得一个ResourceLoader的引用,ResourceLoaderAware接口也提供了一个setResourceLoader()方法,该方法将由Spring容器负责调用,Spring容器会将一个ResourceLoader对象作为该方法的参数传入。
如果把实现ResourceLoaderAware接口的Bean类部署在Spring容器中,Spring容器会将自身当成ResourceLoader作为setResourceLoader()方法的参数传入。由于ApplicationContext的实现类都实现了ResourceLoader接口,Spring容器自身完全可作为ResorceLoader使用。
@Component
public class TestBean implements ResourceLoaderAware {
private ResourceLoader resourceLoader;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public ResourceLoader getResourceLoader() {
return resourceLoader;
}
}
测试
//自己去写SpringConfig配置类
public class TestDemo {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
TestBean bean = context.getBean(TestBean.class);
ResourceLoader resourceLoader = bean.getResourceLoader();
System.out.println(context == resourceLoader);
}
}
5、使用Resource 作为属性
如果 Bean 实例需要访问资源,有如下两种解决方案:
- 代码中获取 Resource 实例。
- 使用依赖注入。(推荐)
对于第一种方式,当程序获取 Resource 实例时,总需要提供 Resource 所在的位置,不管通过 FileSystemResource 创建实例,还是通过 ClassPathResource 创建实例,或者通过 ApplicationContext 的 getResource() 方法获取实例,都需要提供资源位置。这意味着:资源所在的物理位置将被耦合到代码中,如果资源位置发生改变,则必须改写程序。因此,通常建议采用第二种方法,让 Spring 为 Bean 实例依赖注入资源。
//第一步 创建依赖注入类,定义属性和方法
public class ResourceBean {
private Resource res;
public void setRes(Resource res) {
this.res = res;
}
public Resource getRes() {
return res;
}
public void parse(){
System.out.println(res.getFilename());
System.out.println(res.getDescription());
}
}
//第二步 创建spring配置文件,配置依赖注入
<bean id="resourceBean" class="com.it.spring6.di.ResourceBean" >
<!-- 可以使用file:、http:、ftp:等前缀强制Spring采用对应的资源访问策略 -->
<!-- 如果不采用任何前缀,则Spring将采用与该ApplicationContext相同的资源访问策略来访问资源 -->
<!-- 这样就可以实现不修改代码,修改路径,完成解耦-->
<property name="res" value="classpath:it.txt"/>
</bean>
//第三步 测试
public class TestBean {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
ResourceBean bean = context.getBean(ResourceBean.class);
bean.parse();
}
}
6、指定访问策略
ApplicationContext确定资源访问策略通常有两种方法:
(1)使用ApplicationContext实现类指定访问策略。
(2)使用前缀指定访问策略。
/**
* 前缀指定访问策略:
* 就是在创建ApplicationContext实例时,通过指定前缀实现加载配置文件
* 1. classpath:类路径下,加载第一个满足条件的配置文件
* 2. classpath*:类路径下,加载全部满足条件的配置文件
* 3. bean*:以bean开头的配置文件
*/
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:bean.xml");
Resource resource = context.getResource("it.txt");
System.out.println(resource.getDescription());
八、i18n国际化
国际化也称作i18n,其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数。由于软件发行可能面向多个国家,对于不同国家的用户,软件显示不同语言的过程就是国际化。通常来讲,软件中的国际化是通过配置文件来实现的,假设要支撑两种语言,那么就需要两个版本的配置文件。
1、Java国际化
1)Java自身是支持国际化的,java.util.Locale用于指定当前用户所属的语言环境等信息,java.util.ResourceBundle用于查找绑定对应的资源文件。Locale包含了language信息和country信息
2)配置文件命名规则:
basename_language_country.properties
必须遵循以上的命名规则,java才会识别。其中,basename是必须的,语言和国家是可选的。这里存在一个优先级概念,如果同时提供了messages.properties和messages_zh_CN.propertes两个配置文件,如果提供的locale符合en_CN,那么优先查找messages_en_CN.propertes配置文件,如果没查找到,再查找messages.properties配置文件。最后,提示下,所有的配置文件必须放在classpath中,一般放在resources目录下
//创建messages_en_GB.properties
test=GB test
//messages_zh_CN.properties
test=China test
3)测试
public class ResourceI18n {
public static void main(String[] args) {
//baseName:基本名字
ResourceBundle bundle = ResourceBundle.getBundle("messages"
//language:语言,country:国家
, new Locale("zh", "CN"));
String value = bundle.getString("test");
System.out.println(value);
ResourceBundle bundle2 = ResourceBundle.getBundle("messages"
, new Locale("en", "GB"));
String value2 = bundle.getString("test");
System.out.println(value2);
}
}
2、Spring6国际化
spring中国际化是通过MessageSource这个接口来支持的
常见实现类
ResourceBundleMessageSource
这个是基于Java的ResourceBundle基础类实现,允许仅通过资源名加载国际化资源
ReloadableResourceBundleMessageSource
这个功能和第一个类的功能类似,多了定时刷新功能,允许在不重启系统的情况下,更新资源的信息
StaticMessageSource
它允许通过编程的方式提供国际化信息,一会我们可以通过这个来实现db中存储国际化信息的功能。
//创建it_en_GB.properties
www.it.com=welcome {0},时间:{1}
//创建it_zh_CN.properties
www.it.com=欢迎 {0},时间:{1}
测试
public class ResourceI18n {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("bean.xml");
//这里数组都两个参数表示:替换配置文件中的{0}和{1}两个占位符
Object[] objs = new Object[]{"it",new Date().toString()};
String value = context.getMessage("www.it.com", objs, Locale.UK);
System.out.println(value);
}
}
九、数据校验
1、Spring Validation概述
在开发中,我们经常遇到参数校验的需求,比如用户注册的时候,要校验用户名不能为空、用户名长度不超过20个字符、手机号是合法的手机号格式等等。如果使用普通方式,我们会把校验的代码和真正的业务处理逻辑耦合在一起,而且如果未来要新增一种校验逻辑也需要在修改多个地方。而spring validation允许通过注解的方式来定义对象校验规则,把校验和业务逻辑分离开,让代码编写更加方便。Spring Validation其实就是对Hibernate Validator进一步的封装,方便在Spring中使用。
在Spring中有多种校验的方式
第一种是通过实现org.springframework.validation.Validator接口,然后在代码中调用这个类
第二种是按照Bean Validation方式来进行校验,即通过注解的方式。
第三种是基于方法实现校验
除此之外,还可以实现自定义校验
2、通过Validator接口实现
1)引入相关依赖
<dependencies>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>7.0.5.Final</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>
2)创建实体类
//自己实现getter、setter
public class Person {
private String name;
private int age;
}
3)创建类实现Validator接口,实现接口方法指定校验规则
public class PersonValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Person.class.equals(clazz);
}
@Override
public void validate(Object object, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");
Person p = (Person) object;
if (p.getAge() < 0) {
errors.rejectValue("age", "error value < 0");
} else if (p.getAge() > 110) {
errors.rejectValue("age", "error value too old");
}
}
}
supports方法用来表示此校验用在哪个类型上,
validate是设置校验逻辑的地点,其中ValidationUtils,是Spring封装的校验工具类,帮助快速实现校验。
4)使用上述Validator进行测试
public class TestPerson {
public static void main(String[] args) {
//创建person对象
Person person = new Person();
//创建person对应databinder
DataBinder binder = new DataBinder(person);
//设置校验器
binder.setValidator(new PersonValidator());
//调用方法执行校验
binder.validate();
//输出校验结果
BindingResult result = binder.getBindingResult();
System.out.println(result.getAllErrors());
}
}
3、Bean Validation注解实现
使用Bean Validation校验方式,就是如何将Bean Validation需要使用的javax.validation.ValidatorFactory 和javax.validation.Validator注入到容器中。spring默认有一个实现类LocalValidatorFactoryBean,它实现了上面Bean Validation中的接口,并且也实现了org.springframework.validation.Validator接口。
1)创建配置类,配置LocalValidatorFactoryBean
@Configuration
@ComponentScan("com.it.spring6.validation.method2")
public class ValidationConfig {
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
2)创建实体类,使用注解定义校验规则
public class User {
@NotNull
private String name;
@Min(0)
@Max(150)
private int age;
}
常用注解说明
@NotNull 限制必须不为null
@NotEmpty 只作用于字符串类型,字符串不为空,并且长度不为0
@NotBlank 只作用于字符串类型,字符串不为空,并且trim()后不为空串
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@Pattern(value) 限制必须符合指定的正则表达式
@Size(max,min) 限制字符长度必须在min到max之间
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
3)使用两种不同的校验器实现
@Component
public class MyValidation1 {
@Autowired
private Validator validator;
public boolean validatorByUserOne(User user) {
Set<ConstraintViolation<User>> validate = validator.validate(user);
return validate.isEmpty();
}
}
@Component
public class MyValidation2 {
@Autowired
private Validator validator;
public boolean validatorByUserTwo(User user) {
BindException bindException = new BindException(user,user.getName());
validator.validate(user,bindException);
List<ObjectError> allErrors = bindException.getAllErrors();
System.out.println(allErrors);
return bindException.hasErrors();
}
}
4)测试
public class TestUser {
@Test
public void testValidationOne() {
ApplicationContext context =
new AnnotationConfigApplicationContext(ValidationConfig.class);
MyValidation1 validation1 = context.getBean(MyValidation1.class);
User user = new User();
user.setName("lucy");
user.setAge(20);
boolean message = validation1.validatorByUserOne(user);
System.out.println(message);
}
@Test
public void testValidationTwo() {
ApplicationContext context =
new AnnotationConfigApplicationContext(ValidationConfig.class);
MyValidation2 validation2 = context.getBean(MyValidation2.class);
User user = new User();
user.setName("lucy");
user.setAge(200);
boolean message = validation2.validatorByUserTwo(user);
System.out.println(message);
}
}
4、基于方法实现校验
1)创建配置类,配置MethodValidationPostProcessor
@Configuration
@ComponentScan("com.it.spring6.validator.three")
public class ValidationConfig {
@Bean
public MethodValidationPostProcessor validationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
2)创建实体类,使用注解设置校验规则
public class User {
@NotNull
private String name;
@Min(0)
@Max(150)
private int age;
@Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手机号码格式错误")
@NotBlank(message = "手机号码不能为空")
private String phone;
}
3)定义Service类,通过注解操作对象
@Service
@Validated //表示基于方法作校验
public class MyService {
//@Valid加在形参上,一般和@Validated一起作校验
public String testMethod(@NotNull @Valid User user) {
return user.toString();
}
}
4)测试
public class TestUser {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(ValidationConfig.class);
MyService service = context.getBean(MyService.class);
User user = new User();
user.setName("lucy");
user.setPhone("13566754321");
service.testMethod(user);
}
}
十、AOT提前编译(spring6新特性)
lidator validator;
public boolean validatorByUserOne(User user) {
Set<ConstraintViolation<User>> validate = validator.validate(user);
return validate.isEmpty();
}
}
@Component
public class MyValidation2 {
@Autowired
private Validator validator;
public boolean validatorByUserTwo(User user) {
BindException bindException = new BindException(user,user.getName());
validator.validate(user,bindException);
List<ObjectError> allErrors = bindException.getAllErrors();
System.out.println(allErrors);
return bindException.hasErrors();
}
}
4)测试
```java
public class TestUser {
@Test
public void testValidationOne() {
ApplicationContext context =
new AnnotationConfigApplicationContext(ValidationConfig.class);
MyValidation1 validation1 = context.getBean(MyValidation1.class);
User user = new User();
user.setName("lucy");
user.setAge(20);
boolean message = validation1.validatorByUserOne(user);
System.out.println(message);
}
@Test
public void testValidationTwo() {
ApplicationContext context =
new AnnotationConfigApplicationContext(ValidationConfig.class);
MyValidation2 validation2 = context.getBean(MyValidation2.class);
User user = new User();
user.setName("lucy");
user.setAge(200);
boolean message = validation2.validatorByUserTwo(user);
System.out.println(message);
}
}
4、基于方法实现校验
1)创建配置类,配置MethodValidationPostProcessor
@Configuration
@ComponentScan("com.it.spring6.validator.three")
public class ValidationConfig {
@Bean
public MethodValidationPostProcessor validationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
2)创建实体类,使用注解设置校验规则
public class User {
@NotNull
private String name;
@Min(0)
@Max(150)
private int age;
@Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手机号码格式错误")
@NotBlank(message = "手机号码不能为空")
private String phone;
}
3)定义Service类,通过注解操作对象
@Service
@Validated //表示基于方法作校验
public class MyService {
//@Valid加在形参上,一般和@Validated一起作校验
public String testMethod(@NotNull @Valid User user) {
return user.toString();
}
}
4)测试
public class TestUser {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(ValidationConfig.class);
MyService service = context.getBean(MyService.class);
User user = new User();
user.setName("lucy");
user.setPhone("13566754321");
service.testMethod(user);
}
}