Spring
Spring概述
Spring组成

①Spring Core(核心容器)
spring core提供了IOC【控制反转,将对象交给Spring管理】,DI【依赖注入】,Bean配置装载创建的核心实现。核心概念: Beans、BeanFactory、BeanDefinitions、ApplicationContext。
spring-core :IOC和DI的基本实现
spring-beans:BeanFactory和Bean的装配管理(BeanFactory)
spring-context:Spring context上下文,即IOC容器(AppliactionContext)。增加了对国际化(I18N)消息、事件传播、验证的支持。另外提供了许多企业服务,例如电子邮件、JNDI访问、EJB集成、远程以及时序调度(scheduling)服务。也包括了对模版框架例如Velocity和FreeMarker集成的支持
spring-expression:spring表达式语言
②Spring AOP
- spring-aop:面向切面编程的应用模块,整合ASM,CGLib,JDK Proxy。为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖组件,就可以将声明性事务管理集成到应用程序中,可以自定义拦截器、切点、日志等操作。
- spring-aspects:集成AspectJ,AOP应用框架
- spring-instrument:动态Class Loading模块
③Spring Data Access
- spring-jdbc:spring对JDBC的封装,用于简化jdbc操作
- spring-orm:java对象与数据库数据的映射框架,支持集成了常见的ORM框架,如Mybatis、Hibernate。
- spring-oxm:对象与xml文件的映射框架
- spring-jms: Spring对Java Message Service(java消息服务)的封装,用于服务之间相互通信
- spring-tx:spring jdbc事务管理
④Spring Web
spring-web:最基础的web支持,建立于spring-context之上,通过servlet或listener来初始化IOC容器
spring-webmvc:实现web mvc
spring-websocket:与前端的全双工通信协议
spring-webflux:Spring 5.0提供的,用于取代传统java servlet,非阻塞式Reactive Web框架,异步,非阻塞,事件驱动的服务

⑤Spring Message
- Spring-messaging:spring 4.0提供的,为Spring集成一些基础的报文传送服务
⑥Spring test
- spring-test:集成测试支持,主要是对junit的封装
Spring基于xml使用
xml创建Bean
set注入
- 通过property标签获取到属性名:clazzOne
- 通过属性名推断出set方法名:setClazzOne
- 通过反射机制调用setUserDao()方法给属性赋值
<!--id:bean的唯一标识,class:定义bean的类型,使用全类名-->
<!--property:通过set方式为对象中属性赋值-->
<bean id="clazzOne" class="com.atguigu.spring6.bean.Clazz">
<!--name:设置属性名称,value:设置基本属性数值-->
<property name="clazzId" value="1111"></property>
<property name="clazzName" value="财源滚滚班"></property>
</bean>
<!--
value可以设置的值:
基本数据类型
基本数据类型对应的包装类
String或其他的CharSequence子类
Number子类
Date子类
Enum子类
URI
URL
Temporal子类
Locale
Class
另外还包括以上简单值类型对应的数组类型。
-->
<!--注意:value后面的日期字符串格式不能随便写,必须是Date对象toString()方法执行的结果。-->
<!--如果想使用其他格式的日期字符串,就需要进行特殊处理了。具体怎么处理,可以看后面的课程!!!!-->
<property name="date" value="Fri Sep 30 15:26:38 CST 2022"/>
<!--spring6之后,会自动检查url是否有效,如果无效会报错。-->
<property name="url" value="http://www.baidu.com"/>构造注入
通过反射调用构造方法来给属性赋值。
<!--constructor-arg:通过构造器注入-->
<!--Spring在通过构造器注入创建Bean时,会调用与配置文件中<constructor-arg>标签匹配的构造方法。-->
<bean id="studentTwo" class="com.atguigu.spring6.bean.Student">
<!--装配方式一:index="0"表示构造方法的第一个参数,将orderDaoBean对象传递给构造方法的第一个参数。-->
<constructor-arg index="0" ref="orderDaoBean"/>
<!--装配方式二:使用name属性注入(顺序不一定要和构造方法的相同)-->
<constructor-arg name="orderDao" ref="orderDaoBean"/>
<!--装配方式三:自动装配,通过类型推断-->
<constructor-arg ref="orderDaoBean"/>
</bean>引用外部Bean
<bean id="studentFour" class="com.atguigu.spring6.bean.Student">
<property name="sex" value="女"></property>
<!-- ref属性:引用IOC容器中某个bean的id,将所对应的bean为属性赋值,不能使用value属性 -->
<property name="clazz" ref="clazzOne"></property>
</bean>内部Bean
<bean id="userServiceBean" class="com.powernode.spring6.service.UserService">
<!--内部bean只能用于给属性赋值,不能在外部通过IOC容器获取,因此可以省略id属性-->
<property name="clazz">
<bean class="com.atguigu.spring6.bean.Clazz">
<property name="clazzId" value="2222"></property>
<property name="clazzName" value="远大前程班"></property>
</bean>
</property>
</bean>级联属性赋值
<bean id="studentFour" class="com.atguigu.spring6.bean.Student">
<!--1、必须先引入clazzOne-->
<property name="clazz" ref="clazzOne"></property>
<!--2、clazz必须提供getter方法才可以-->
<property name="clazz.clazzId" value="3333"></property>
<property name="clazz.clazzName" value="最强王者班"></property>
</bean>数组赋值
<bean id="studentFour" class="com.atguigu.spring.bean6.Student">
<property name="hobbies">
<array>
<value>抽烟</value>
<value>喝酒</value>
<value>烫头</value>
</array>
</property>
</bean>集合赋值
<!--为list集合赋值-->
<bean id="clazzTwo" class="com.atguigu.spring6.bean.Clazz">
<property name="students">
<list>
<ref bean="studentOne"></ref>
<ref bean="studentTwo"></ref>
<ref bean="studentThree"></ref>
</list>
</property>
</bean>
<!--为set集合赋值-->
<bean id="clazzTwo" class="com.atguigu.spring6.bean.Clazz">
<property name="students">
<list>
<ref bean="studentOne"></ref>
<ref bean="studentTwo"></ref>
<ref bean="studentThree"></ref>
</list>
</property>
</bean>
<!--为Map集合赋值-->
<property name="teacherMap">
<map>
<entry>
<key>
<value>10010</value>
</key>
<ref bean="teacherOne"></ref>
</entry>
<entry>
<key>
<value>10086</value>
</key>
<ref bean="teacherTwo"></ref>
</entry>
</map>
</property>引用集合类型Bean
<!--list集合类型的bean-->
<util:list id="students">
<ref bean="studentOne"></ref>
<ref bean="studentTwo"></ref>
<ref bean="studentThree"></ref>
</util:list>
<!--map集合类型的bean-->
<util:map id="teacherMap">
<entry>
<key>
<value>10010</value>
</key>
<ref bean="teacherOne"></ref>
</entry>
<entry>
<key>
<value>10086</value>
</key>
<ref bean="teacherTwo"></ref>
</entry>
</util:map>
<!--通过ref引用-->
<property name="teacherMap" ref="teacherMap"></property>p命名空间注入
<!--本质还是通过set注入,所以需要set方法-->
<bean id="studentSix" class="com.atguigu.spring6.bean.Student"
p:id="1006" p:name="小明" p:clazz-ref="clazzOne" p:teacherMap-ref="teacherMap"></bean>c空间命名注入
<!--本质通过构造方法注入,所以需要构造方法-->
<bean id="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:_0="2008" c:_1="8" c:_2="8"/>Bean作用域
<!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对象 -->
<!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象 -->
<!-- request属性:当前请求有效,离开请求域失效-->
<!-- session属性:当前会话有效,离开当前会话失效-->
<!--global session:portlet应用中专用的。如果在Servlet的WEB应用中使用global session的话,和session一个效果。(portlet和servlet都是规范。servlet运行在servlet容器中,例如Tomcat。portlet运行在portlet容器中。)-->
<!--application:一个应用对应一个Bean。仅限于在WEB应用中使用。-->
<!--websocket:一个websocket生命周期对应一个Bean。仅限于在WEB应用中使用。-->
<bean class="com.atguigu.spring6.bean.User" scope="prototype"></bean>
<!--自定义scope,每一个线程都会创建一个新的对象-->
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="myThread">
<!--spring内置了线程范围的类-->
<bean class="org.springframework.context.support.SimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
<!--使用scope-->
<bean id="sb" class="com.powernode.spring6.beans.SpringBean" scope="myThread" />赋特殊值
<!--为name赋值为null,<property name="name" value="null"></property>赋值的是空字符串-->
<!--方式一:不赋值-->
<!--方式二-->
<property name="name">
<null />
</property>
<!--为字符串赋值为空-->
<bean id="vipBean" class="com.powernode.spring6.beans.Vip">
<!--方式一-->
<property name="email" value=""/>
<!--方式二-->
<property name="email">
<value/>
</property>
</bean>
<!-- 尖括号在xml中的表示方式 -->
<!-- 解决方案一:使用XML实体来代替 -->
<property name="expression" value="a < b"/>
<!-- 解决方案二:使用CDATA节 -->
<property name="expression">
<!-- CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据 -->
<value><![CDATA[a < b]]></value>
</property>引用外部文件属性
db.properties配置文件
#key=value
db.driverClassName=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/db220106
db.username=root
db.password=root
<!-- 加载外部属性文件db.properties-->
<context:property-placeholder location="classpath:db.properties"></context:property-placeholder>
<!-- 装配数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db.driverClassName}"></property>
<property name="url" value="${db.url}"></property>
<property name="username" value="${db.username}"></property>
<property name="password" value="${db.password}"></property>
</bean>xml自动配置
基于XML方式的自动装配,只能装配非字面量数值(或说对象)
基于xml自动装配,底层使用set注入(无论byName还是byType,都是基于set方法的)
<!--
byType:根据类型匹配IOC容器中的某个兼容类型的bean,为属性自动赋值
若在IOC中,没有任何一个兼容类型的bean能够为属性赋值,则该属性不装配,即值为默认值null
若在IOC中,有多个兼容类型的bean能够为属性赋值,则抛出异常NoUniqueBeanDefinitionException
-->
<bean id="userService" class="com.atguigu.spring6.autowire.service.impl.UserServiceImpl" autowire="byType"></bean>
<!--
byName:将自动装配的属性的属性名,作为bean的id在IOC容器中匹配相对应的bean进行赋值
-->
<bean id="userService" class="com.atguigu.spring6.autowire.service.impl.UserServiceImpl" autowire="byName"></bean>创建Bean方式
通过构造方法实例化
<bean id="userBean" class="com.powernode.spring6.bean.User"/>通过简单工厂模式实例化
public class VipFactory {
public static Vip get(){
return new Vip();
}
}
<bean id="vipBean" class="com.powernode.spring6.bean.VipFactory" factory-method="get"/>通过factory-bean实例化
//Bean类
public class Order {
}
//Bean工厂类
public class OrderFactory {
public Order get(){
return new Order();
}
}
//装配Bean工厂
<bean id="orderFactory" class="com.powernode.spring6.bean.OrderFactory"/>
//通过Bean工厂创建对象
<bean id="orderBean" factory-bean="orderFactory" factory-method="get"/>通过FactoryBean接口实例化
和普通的bean不同,FactoryBean类型的bean,在获取bean的时候得到的并不是class属性中配置的这个类的对象,而是getObject()方法的返回值。通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来。
- BeanFactory:Spring IoC容器的顶级对象,在Spring的IoC容器中,“Bean工厂”负责创建Bean对象。
- FactoryBean:辅助Spring实例化其它Bean对象的一个Bean。 在Spring中,Bean可以分为两类:
- 第一类:普通Bean
- 第二类:工厂Bean(记住:工厂Bean也是一种Bean,只不过这种Bean比较特殊,它可以辅助Spring实例化其它Bean对象。)
public class MyFactoryBean implements FactoryBean<Dept> {
//getObject():参数对象创建的方法
@Override
public Dept getObject() throws Exception {
Dept dept = new Dept(101,"研发部门");
//.....
return dept;//通过getBean返回的类型
} ,
//设置参数对象Class
@Override
public Class<?> getObjectType() {
return Dept.class;
}
//设置当前对象是否为单例
@Override
public boolean isSingleton() {
return true;
}
}
<bean id="personBean" class="com.powernode.spring6.bean.PersonFactoryBean"/>获取Bean
如果一个接口仅有一个实现类,根据接口类型可以获取 bean(若有多个则不可以,因为不知道装配哪一个)。根据类型来获取bean时,在满足bean唯一性的前提下,其实只是看:『对象 instanceof 指定的类型』的返回结果,只要返回的是true就可以认定为和类型匹配,能够获取到
@Test
public void testHelloWorld1(){
//读取Bean配置文件的位置,创建Bean,可以读取多个xml文件(xml文件要在resource路径下)
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml","spring.xml");
//通过绝对路径读取xml文件
//ApplicationContext applicationContext2 = new FileSystemXmlApplicationContext("d:/spring6.xml");
//方式一:通过Bean-Id获取,需要强制类型转换
HelloWorld helloworld = (HelloWorld) ac.getBean("helloWorld");
//方式二:通过Bean类型获取,容器中有多个相同类型bean的时候,会报错
HelloWorld bean = ac.getBean(HelloWorld.class);
//通过Bean类型和Bean-Id获取
HelloWorld bean = ac.getBean("helloworld", HelloWorld.class);
helloworld.sayHello();
}Bean生命周期
大概划分
- 通过构造器或工厂方法创建bean实例(调用无参构造器)
- 为bean的属性设置值和对其他bean的引用
- bean的后置处理器:postProcessBeforeInitialization(Object, String):在bean的初始化之前执行
- bean对象初始化(需在配置bean时指定初始化方法)
- bean的后置处理器:postProcessAfterInitialization(Object, String):在bean的初始化之后执行
- bean对象就绪可以使用
- bean对象销毁(需在配置bean时指定销毁方法)
细粒度划分
- 对于singleton作用域的Bean,Spring将会完整的跟踪Bean的整个生命周期;
- 对于 prototype 作用域的 Bean,Spring 只负责创建,所以DisposableBean及后面的周期都不会执行。

Bean的初始化和销毁
方式一:编写initMethod()和destroyMethod(),创建Bean时指定
public class User {
//通过xml配置文件
public void initMethod(){
System.out.println("生命周期:3、初始化");
}
//通过注解
@Bean(initMethod="init",destroyMethod="detory")
public void destroyMethod(){
System.out.println("生命周期:5、销毁");
}
}<!-- 使用init-method属性指定初始化方法 -->
<!-- 使用destroy-method属性指定销毁方法 -->
<bean class="com.atguigu.spring6.bean.User" scope="prototype" init-method="initMethod" destroy-method="destroyMethod">
<property name="id" value="1001"></property>
<property name="username" value="admin"></property>
</bean>方式二:通过实现方法
InitializingBean(定义初始化逻辑),
DisposableBean(定义销毁逻辑);方式三:使用注解
@PostConstruct:在bean创建完成并且属性赋值完成;来执行初始化方法
@PreDestroy:在容器销毁bean之前通知我们进行清理工作bean的后置处理器
bean的后置处理器会在生命周期的初始化前后添加额外的操作,需要实现BeanPostProcessor接口,且配置到IOC容器中,需要注意的是,bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行
public class MyBeanProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("☆☆☆" + beanName + " = " + bean);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("★★★" + beanName + " = " + bean);
return bean;
}
}
//bean的后置处理器要放入IOC容器才能生效
<bean id="myBeanProcessor" class="com.atguigu.spring6.process.MyBeanProcessor"/>Bean的Aware接口
- 当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean。
- 当Bean实现了BeanClassLoaderAware,Spring会将加载该Bean的类加载器传递给Bean。
- 当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给Bean。
XMl开启注解
1、指定扫描位置
<!--开启 Spring Beans的自动扫描功能,扫描路径为com.atguigu.spring6下包及其子包-->
<context:component-scan base-package="com.atguigu.spring6"></context:component-scan>2、指定排除组件
<context:component-scan base-package="com.atguigu.spring6">
<!-- context:exclude-filter标签:指定排除规则 -->
<!--
type:设置排除或包含的依据
type="annotation",根据注解排除,expression中设置要排除的注解的全类名
type="assignable",根据类型排除,expression中设置要排除的类型的全类名
-->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:exclude-filter type="assignable" expression="com.atguigu.spring6.controller.UserController"/>
</context:component-scan>3、扫描指定组件
<context:component-scan base-package="com.atguigu" use-default-filters="false">
<!-- context:include-filter标签:指定在原有扫描规则的基础上追加的规则 -->
<!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
<!-- 此时必须设置use-default-filters="false",因为默认规则即扫描指定包下所有类 -->
<!--
type:设置排除或包含的依据
type="annotation",根据注解排除,expression中设置要排除的注解的全类名
type="assignable",根据类型排除,expression中设置要排除的类型的全类名
-->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:include-filter type="assignable" expression="com.atguigu.spring6.controller.UserController"/>
</context:component-scan>Spring全注解开发
约定>配置>代码
配置类代替XML
//配置类
//标识当前类是一个配置类
@Configuration
//设置组件扫描当前包及其子包
@ComponentScan(basePackages = "com.atguigu")
public class SpringConfig {
}
//可以使用@Components包含多个@Component
@ComponentScans(
value = {
//@ComponentScan value:指定要扫描的包
//excludeFilters = Filter[] :指定扫描的时候按照什么规则排除那些组件
//includeFilters = Filter[] :指定扫描的时候只需要包含哪些组件
//FilterType.ANNOTATION:按照注解
//FilterType.ASSIGNABLE_TYPE:按照给定的类型;
//FilterType.ASPECTJ:使用ASPECTJ表达式
//FilterType.REGEX:使用正则指定
//FilterType.CUSTOM:使用自定义规则
@ComponentScan(value="com.atguigu",includeFilters = {
@Filter(type=FilterType.ANNOTATION,classes={Controller.class}),
@Filter(type=FilterType.ASSIGNABLE_TYPE,classes={BookService.class}),
@Filter(type=FilterType.CUSTOM,classes={MyTypeFilter.class})
}
//需要关闭默认的过滤规则
,useDefaultFilters = false)
}
)
@Test
public void test0Xml(){
//使用AnnotationConfigApplicationContext容器对象,因为没有使用xml配置文件了
ApplicationContext context =
new AnnotationConfigApplicationContext(SpringConfig.class);
DeptDaoImpl deptDao = context.getBean("deptDao", DeptDaoImpl.class);
System.out.println("deptDao = " + deptDao);
}自定义规则
public class MyTypeFilter implements TypeFilter {
/**
* metadataReader:读取到的当前正在扫描的类的信息
* metadataReaderFactory:可以获取到其他任何类信息的
*/
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
throws IOException {
// TODO Auto-generated method stub
//获取当前类注解的信息
AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
//获取当前正在扫描的类的类信息
ClassMetadata classMetadata = metadataReader.getClassMetadata();
//获取当前类资源(类的路径)
Resource resource = metadataReader.getResource();
String className = classMetadata.getClassName();
System.out.println("--->"+className);
if(className.contains("er")){
return true;
}
return false;
}
}创建Bean
装配到类上
- Spring本身不区分四个注解,他们本质均是@Component(其他三个注解上全部都有@Componen),提供四个注解的目的是为了提高代码的可读性。
- 只用注解装配对象,默认将类名首字母小写作为beanId
- 可以使用value属性,设置beanId(当注解中只使用一个value属性时,value关键字可省略)
- 如果对接口使用注解,会装配其实现类。因为接口无法实例化。
| 注解 | 说明 |
|---|---|
| @Component | 装配普通组件到IOC容器 |
| @Repository | 该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
| @Service | 该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
| @Controller | 该注解通常作用在控制层(如SpringMVC 的Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
装配到方法上
@Bean:可以放在@controller、@Service、@Component、@Configuration、@Repository修饰的类当中,这样项目在启动的时候,@Bean方法返回的对象也会立马存入容器当中!
自动注入
@Autowired注解
装配流程
先按照byType进行匹配。
- 若匹配到0个,如果@Autowired(required=true):表示被标识的属性必须装配数值,如未装配,则报错,反之注入为空;
- 若匹配到1个,正常使用;
- 若匹配到多个,再按照byName进行唯一筛选。
- Spring底层的Map,以BeanId作为key,如果通过byType获取,需要遍历map,通过interface判断类型。
注解位置
成员变量上(对象):通过反射方式注入。
java@Controller public class UserController { @Autowired private UserService userService; }set方法上:通过set方法注入。
java@Controller public class UserController { private UserService userService; @Autowired public void setUserService(UserService userService) { this.userService = userService; } } //装配到set方法上,可以为静态成员变量进行赋值 private static RestHighLevelClient restHighLevelClient; @Autowired public void setRestHighLevelClient(RestHighLevelClient restHighLevelClient) { DemoApplication.restHighLevelClient = restHighLevelClient; }构造方法上:通过构造方法注入。
java@Service public class UserServiceImpl implements UserService { private UserDao userDao; @Autowired public UserServiceImpl(UserDao userDao) { this.userDao = userDao; } } //当有参数的构造方法只有一个时,@Autowired注解可以省略,如果存在多个构造方法,则会报错。 @Service public class UserServiceImpl implements UserService { private UserDao userDao; public UserServiceImpl(UserDao userDao) { this.userDao = userDao; } }形参上:通过构造方法注入,和上面一样。
java@Service public class UserServiceImpl implements UserService { private UserDao userDao; public UserServiceImpl(@Autowired UserDao userDao) { this.userDao = userDao; } }
@Qualifier注解
当系统中存在多个相同类型的Bean时,需要使用@Qualifier通过名字指定Bean,设置要装配进入非字面量数值属性中的beanID。
@Service
public class UserServiceImpl implements UserService {
@Autowired
@Qualifier("userDaoImpl") // 指定bean的名字
private UserDao userDao;
}@Resource注解
和@Autowired一样,同样用于注入,而@Autowired属于Spring,@Resource注解属于JDK。(JSR-250标准中制定的注解类型。JSR是Java规范提案。)
- @Resource注解默认根据名称装配byName,未指定name时,使用属性名作为name。通过name找不到的话会自动启动通过类型byType装配。
- @Resource注解用在属性上、setter方法上。
@Service
public class UserServiceImpl implements UserService {
@Resource(name = "myUserDao")
private UserDao myUserDao;
}@Value注解:
装配对象中属性【字面量数值】,如果没有注解,其属性是默认值。
其他注解
@Conditional
@Conditional({LinuxCondition.class}),类中组件统一设置。满足当前条件,这个类中配置的所有bean注册才能生效;
//判断是否linux系统
public class LinuxCondition implements Condition {
/**
* ConditionContext:判断条件能使用的上下文(环境)
* AnnotatedTypeMetadata:注释信息
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// TODO是否linux系统
//1、能获取到ioc使用的beanfactory
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
//2、获取类加载器
ClassLoader classLoader = context.getClassLoader();
//3、获取当前环境信息
Environment environment = context.getEnvironment();
//4、获取到bean定义的注册类
BeanDefinitionRegistry registry = context.getRegistry();
String property = environment.getProperty("os.name");
//可以判断容器中的bean注册情况,也可以给容器中注册bean
boolean definition = registry.containsBeanDefinition("person");
if(property.contains("linux")){
return true;
}
return false;
}
}@Import
@Import({Color.class,Red.class,MyImportSelector.class,MyImportBeanDefinitionRegistrar.class}),导入组件,id默认是组件的全类名
//实现ImportSelector,自定义逻辑返回需要导入的组件
public class MyImportSelector implements ImportSelector {
//返回值,就是到导入到容器中的组件全类名
//AnnotationMetadata:当前标注@Import注解的类的所有注解信息
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// TODO Auto-generated method stub
//importingClassMetadata
//方法不要返回null值
return new String[]{"com.atguigu.bean.Blue","com.atguigu.bean.Yellow"};
}
}
//实现ImportBeanDefinitionRegistrar,通过BeanDefinitionRegistry注册Bean
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
/**
* AnnotationMetadata:当前类的注解信息
* BeanDefinitionRegistry:BeanDefinition注册类;
* 把所有需要添加到容器中的bean;调用
* BeanDefinitionRegistry.registerBeanDefinition手工注册进来
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
boolean definition = registry.containsBeanDefinition("com.atguigu.bean.Red");
boolean definition2 = registry.containsBeanDefinition("com.atguigu.bean.Blue");
if(definition && definition2){
//指定Bean定义信息;(Bean的类型,Bean。。。)
RootBeanDefinition beanDefinition = new RootBeanDefinition(RainBow.class);
//注册一个Bean,指定bean名
registry.registerBeanDefinition("rainBow", beanDefinition);
}
}
}@EnableXxxx
手动控制哪些功能的开启; 手动导入。都是利用 @Import 把此功能要用的组件导入进去
@Profile
指定组件在哪个环境的情况下才能被注册到容器中,不指定,任何环境下都能注册这个组件
- 1)、加了环境标识的bean,只有这个环境被激活的时候才能注册到容器中。默认是default环境。
- 2)、写在配置类上,只有是指定的环境的时候,整个配置类里面的所有配置才能开始生效
- 3)、没有标注环境标识的bean在,任何环境下都是加载的;
//运行时添加命令行参数:java -jar myproject.jar -Dspring.profiles.active=test
//或者SpringBoot配置文件中使用spring.profiles.active=test
@Profile("prod")
@Bean("prodDataSource")
public DataSource dataSourceProd(@Value("${db.password}")String pwd) throws Exception{
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser(user);
dataSource.setPassword(pwd);
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/scw_0515");
dataSource.setDriverClass(driverClass);
return dataSource;
}@PropertySource
@PropertySource(value={"classpath:/person.properties"}),使用@PropertySource读取外部配置文件中的k/v保存到运行的环境变量中;加载完外部的配置文件以后使用${}取出配置文件的值
Spring-IOC
IOC结构
BeanFactory:Spring底层IOC实现【面向Spring框架,不是提供给开发人员使用的】 - ...
- ApplicationContext:面向程序员【BeanFactory的子接口,提供了更多高级特性。】
- ConfigurableApplicationContext:提供关闭或刷新容器对象方法
- ...
- ClassPathXmlApplicationContext:基于类路径检索xml文件
- AnnotationConfigApplicationContext:基于注解创建容器对象
- FileSystemXmlApplicationContext:基于文件系统检索xml文件
- ...
- ConfigurableApplicationContext:提供关闭或刷新容器对象方法
- ApplicationContext:面向程序员【BeanFactory的子接口,提供了更多高级特性。】
- ...
Bean的循环依赖问题
Spring循环依赖
Bean1中需要Bean2,Bean2中也引用了Bean1
public class Husband {
private Wife wife;
}
public class Wife {
private Husband husband;
}两个singleton + set:不存在循环依赖问题
<bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="singleton">
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="singleton">
<property name="husband" ref="husbandBean"/>
</bean>两个prototype+set:出现循环依赖问题
<!--因为每一次都需要new一个对象,不能将对象曝光储存-->
<bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="prototype">
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="prototype">
<property name="husband" ref="husbandBean"/>
</bean>两个singleton+构造函数:出现循环依赖问题
<!--构造函数注入,创建对象和赋值不能分开-->
<bean id="hBean" class="com.powernode.spring6.bean2.Husband" scope="singleton">
<constructor-arg name="wife" ref="wBean"/>
</bean>
<bean id="wBean" class="com.powernode.spring6.bean2.Wife" scope="singleton">
<constructor-arg name="husband" ref="hBean"/>
</bean>一个prototype+一个singleton:不存在循环依赖问题
<!--singleton可以曝光储存,赋值给prototype-->
<bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="singleton">
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="prototype">
<property name="husband" ref="husbandBean"/>
</bean>Spring解决循环依赖的机理
本质上是将“实例化Bean”和“给Bean属性赋值”这两个动作分开去完成。实例化Bean的时候:调用无参数构造方法来完成。此时可以先不给属性赋值,可以提前将该Bean对象“曝光”给外界。
- **Cache of singleton objects: bean name to bean instance. **单例对象的缓存:key存储bean名称,value存储Bean对象【一级缓存】
- **Cache of early singleton objects: bean name to bean instance. **早期单例对象的缓存:key存储bean名称,value存储早期的Bean对象【二级缓存】
- **Cache of singleton factories: bean name to ObjectFactory. **单例工厂缓存:key存储bean名称,value存储该Bean对应的ObjectFactory对象【三级缓存】
- Bean获取顺序,不存在才会调用下级缓存:一级缓存->二级缓存->三级缓存
Bean的储存
singletonObjects:这是一级缓存,用于存储已经完全初始化的单例Bean。singletonFactories:这是二级缓存,用于存储Bean的工厂对象。当一个单例Bean被创建,但还未完全初始化(例如,还未进行属性填充或者初始化方法还未调用)时,Spring会将创建该Bean的ObjectFactory放入这个缓存。earlySingletonObjects:这是三级缓存,用于存储早期的单例Bean。当一个Bean正在创建,但还未完全初始化时,为了解决循环依赖,Spring会提前暴露一个未完全初始化的Bean(即早期的Bean)。beanDefinitionMap:这个Map存储了所有Bean的定义信息,包括Bean的类名、作用域、构造函数参数、属性值等。当Spring需要创建一个新的Bean实例时,会根据beanDefinitionMap中的Bean定义信息来创建,存在于DefaultListableBeanFactory类中。
ApplicationContext相关
实现ApplicationContextAware接口
public class MyBean implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
}通过@Autowired注入
public class MyBean {
@Autowired
private ApplicationContext applicationContext;
}手动创建Bean
// 创建 默认可列表BeanFactory 对象
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 注册Bean
factory.registerSingleton("userBean", user);Spring-AOP
代理模式
将目标对象要做的事,委托给代理对象进行执行,这两个对象无法相互转化,为兄弟关系。所以无法使用目标对象去接受代理对象,但可以使用目标对象的父接口、父类取接受代理对象。

JDK动态代理
只能代理接口
//- 第一件事:在内存中生成了代理类的字节码
//- 第二件事:创建代理对象
public class Client {
public static void main(String[] args) {
// 第一步:创建目标对象
OrderService target = new OrderServiceImpl();
// 第二步:创建代理对象
//- 第一个参数:类加载器。在内存中生成了字节码,要想执行这个字节码,也是需要先把这个字节码加载到内存当中的。所以要指定使用哪个类加载器加载。
//- 第二个参数:接口类型。代理类和目标类实现相同的接口,所以要通过这个参数告诉JDK动态代理生成的类要实现哪些接口。
//- 第三个参数:调用处理器。这是一个JDK动态代理规定的接口,接口全名:java.lang.reflect.InvocationHandler。显然这是一个回调接口,也就是说调用这个接口中方法的程序已经写好了,就差这个接口的实现类了。
OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),new TimerInvocationHandler(target));
// 第三步:调用代理对象的代理方法
orderServiceProxy.detail();
orderServiceProxy.modify();
orderServiceProxy.generate();
}
}
public class TimerInvocationHandler implements InvocationHandler {
// 目标对象
private Object target;
// 通过构造方法来传目标对象
public TimerInvocationHandler(Object target) {
this.target = target;
}
//第一个参数:Object proxy,代理对象。
//第二个参数:Method method,目标方法。
//第三个参数:Object[] args,目标方法调用时要传的参数。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 目标执行之前增强。
long begin = System.currentTimeMillis();
// 调用目标对象的目标方法
Object retValue = method.invoke(target, args);
// 目标执行之后增强。
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
// 一定要记得返回哦。
return retValue;
}
}CGLIB动态代理技术
CGLIB(Code Generation Library)可以在运行期扩展Java类与实现Java接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的(final修饰的类将无法使用)。性能比JDK动态代理要好。(底层有一个小而快的字节码处理框架ASM。)
高版本JDK需要添加运行参数:
- --add-opens java.base/java.lang=ALL-UNNAMED
- --add-opens java.base/sun.net.util=ALL-UNNAMED
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>public class Client {
public static void main(String[] args) {
// 创建字节码增强器
Enhancer enhancer = new Enhancer();
// 告诉cglib要继承哪个类
enhancer.setSuperclass(UserService.class);
// 设置回调接口
enhancer.setCallback(new TimerMethodInterceptor());
// 生成源码,编译class,加载到JVM,并创建代理对象
UserService userServiceProxy = (UserService)enhancer.create();
userServiceProxy.login();
userServiceProxy.logout();
}
}
/*
第一个参数:目标对象
第二个参数:目标方法
第三个参数:目标方法调用时的实参
第四个参数:代理方法
在MethodInterceptor的intercept()方法中调用目标以及添加增强:
*/
public class TimerMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 前增强
long begin = System.currentTimeMillis();
// 调用目标
Object retValue = methodProxy.invokeSuper(target, objects);
// 后增强
long end = System.currentTimeMillis();
System.out.println("耗时" + (end - begin) + "毫秒");
// 一定要返回
return retValue;
}
}Javassist动态代理技术
Javassist通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。
AOP编程
Spring的AOP(面向切面编程)使用的动态代理是:JDK动态代理 + CGLIB动态代理技术。Spring在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理,如果要代理某个类,这个类没有实现接口,就会切换使用CGLIB。当然,你也可以强制通过一些配置让Spring只使用CGLIB。

相关术语
横切关注点:非核心业务代码,如日志
切面(Aspect):将横切关注点提取到类中,这个类称之为切面类

通知(Advice):将横切关注点提取到类中之后,横切关注点更名为:通知
目标(Target):目标对象,指的是需要被代理的对象【实现类(CalcImpl)】
代理(Proxy):代理对象可以理解为:中介
连接点(Joinpoint):通知方法需要指定通知位置,这个位置称之为:连接点【通知之前】 ,通俗说,就是spring允许你使用通知的地方

切入点(pointcut):通知方法需要指定通知位置,这个位置称之为:切入点【通知之后】

AspectJ
注意,表达式中创建的切点方法,绝对不能包括WebsocketController中的方法,否则会@ServerEndPoint注解的类注册失败
xml配置
<!--pom文件中:添加AspectJ-->
<!--spirng-aspects的jar包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
<!--配置文件中-->
<!--开启组件扫描-->
<context:component-scan base-package="com.atguigu"></context:component-scan>
<!--开启AspectJ注解支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>基于注解创建切面类
- Spring版本5.3.x以前:
- 前置通知
- 目标操作
- 后置通知
- 返回通知或异常通知
- Spring版本5.3.x以后:
- 前置通知
- 目标操作
- 返回通知或异常通知
- 后置通知
//@Aspect表示这个类是一个切面类
@Aspect
//@Component注解保证这个切面类能够放入IOC容器
@Component
public class LogAspect {
//前置通知:使用@Before注解标识,在被代理的目标方法前执行
@Before("execution(public int com.atguigu.aop.annotation.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
//后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行(盖棺定论)
@After("execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->后置通知,方法名:"+methodName);
}
//返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行(寿终正寝)
@AfterReturning(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
}
//异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行(死于非命)
@AfterThrowing(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
}
//环绕通知:使用@Around注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
@Around("execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(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;
}
}基于xml创建切面类
<context:component-scan base-package="com.atguigu.aop.xml"></context:component-scan>
<aop:config>
<!--配置切面类-->
<aop:aspect ref="loggerAspect">
<aop:pointcut id="pointCut"
expression="execution(* com.atguigu.aop.xml.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>切点表达式

【*】:
【*】:可以代表任意权限修饰符&返回值类型
【*】:可以代表任意包名、任意类名、任意方法名
【..】:
【..】:代表任意参数类型及参数个数
//声明切点表达式
@Pointcut("execution(* com.atguigu.aop.annotation.*.*(..))")
public void pointCut(){}
//本包下重用切点表达式
@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
//在其他包下重用
@Before("com.atguigu.aop.CommonPointCut.pointCut()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}获取通知相关信息
①获取连接点信息
获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参
@Before("execution(public int com.atguigu.aop.annotation.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint){
//获取连接点的签名信息,方法名+参数列表
String methodName = joinPoint.getSignature().getName();
//获取目标方法到的实参信息
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}②获取目标方法的返回值
@AfterReturning中的属性returning,用来将通知方法的某个形参,接收目标方法的返回值
@AfterReturning(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
}③获取目标方法的异常
@AfterThrowing中的属性throwing,用来将通知方法的某个形参,接收目标方法的异常
@AfterThrowing(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
}切面优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
使用@Order注解可以控制切面的优先级,Order内默认为int类型最大达值:
- @Order(较小的数):优先级高
- @Order(较大的数):优先级低

注解参数说明
public @interface AfterReturning {
//切点表达式
String value() default "";
//和value作用相同
String pointcut() default "";
//方法的返回值
String returning() default "";
//方法的参数名
String argNames() default "";
}
//@annotation(controllerLog)表示需要有方法controllerLog参数对应的注解类型才会被扫描,returning = "jsonResult"。在方法参数中接收jsonResult,即可获得AOP代理方法的返回值,
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
handleLog(joinPoint, controllerLog, null, jsonResult);
}AOP实战
登录验证
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Login {
}
@Aspect
@Component
public class LoginConfig {
@Before("execution(* com.fjut.personalwebsite.controller..*.*(..)) && @annotation(login)")
public void login(Login login) throws Throwable {
// 获取请求对象
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 强制转换
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
// 请求对象
HttpServletRequest request = servletRequestAttributes.getRequest();
boolean res = TokenUtil.hasToken(request);
if(!res){
HttpServletResponse response = servletRequestAttributes.getResponse();
response.sendRedirect("/user/toLogin");
return;
}
}
}集成日志
/**
* 自定义操作日志记录注解
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 模块
*/
public String title() default "";
/**
* 功能:使用枚举类表示,如增加,删除,修改
*/
public BusinessType businessType() default BusinessType.OTHER;
/**
* 操作人类别,使用枚举类表示:如管理员,用户等
*/
public OperatorType operatorType() default OperatorType.MANAGE;
/**
* 是否保存请求的参数
*/
public boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
public boolean isSaveResponseData() default true;
}
/**
* 自定义操作日志切面类
*/
@Aspect
@Component
public class LogAspect {
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
//微服务切换为feign调用接口
@Resource
private SysOperLogFeignClient sysOperLogFeignClient;
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
handleLog(joinPoint, controllerLog, null, jsonResult);
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
handleLog(joinPoint, controllerLog, e, null);
}
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
try {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
// *========数据库日志=========*//
SysOperLog operLog = new SysOperLog();
operLog.setStatus(1);
// 请求的地址
String ip = IpUtil.getIpAddress(request);//IpUtil.getIpAddr(ServletUtils.getRequest());
operLog.setOperIp(ip);
operLog.setOperUrl(request.getRequestURI());
if (e != null) {
operLog.setStatus(0);
operLog.setErrorMsg(e.getMessage());
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(request.getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 保存数据库
sysOperLogFeignClient.saveSysLog(operLog);
log.info("操作日志:"+JSON.toJSONString(operLog));
} catch (Exception exp) {
// 记录本地异常日志
log.error("==前置通知异常==");
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
}
制作缓存
//创建注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GuiGuCache {
// 定义缓存key 的前缀:
String prefix() default "cache:";
}
@Component
@Aspect
@Slf4j
public class GuiGuCacheAspect {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
// 使用环绕通知!
@SneakyThrows
@Around("@annotation(com.atguigu.tingshu.common.cache.GuiGuCache)")
public Object cacheAspect(ProceedingJoinPoint point){
// 创建一个对象
Object object;
// 1. 要组成缓存key,必须先获取到注解! 需要方法的参数列表
MethodSignature methodSignature = (MethodSignature) point.getSignature();
// 2. 获取到方法上的签名.
GuiGuCache guiGuCache = methodSignature.getMethod().getAnnotation(GuiGuCache.class);
// 3. 获取注解前缀,组成key,从缓存中获取数据.
Object[] args = point.getArgs();
// 如果参数一样,前缀必须不一样!
String key = guiGuCache.prefix()+ Arrays.asList(args);
try {
// 从缓存中获取数据.
object = this.redisTemplate.opsForValue().get(key);
// 判断数据
if (null == object){
// 存在key,但是缓存为空,直接返回null
if(this.redisTemplate.hasKey(key))
return null;
// 上锁: 组成锁的key
String locKey = key+":lock";
// 上锁:
RLock lock = redissonClient.getLock(locKey);
//ALBUM_LOCK_EXPIRE_PX1:单位:秒 尝试获取锁的最大等待时间
//ALBUM_LOCK_EXPIRE_PX2:单位:秒 锁的持有时间
boolean result = lock.tryLock(RedisConstant.ALBUM_LOCK_EXPIRE_PX1, RedisConstant.ALBUM_LOCK_EXPIRE_PX2, TimeUnit.SECONDS);
//方式一:通过自旋防止大量缓存打到数据库
/*if (result){
try {
// 查询数据:
object = point.proceed(point.getArgs());
// 判断
if (null == object){
// 数据库中根本没有这个数据
this.redisTemplate.opsForValue().set(key, object, RedisConstant.ALBUM_TEMPORARY_TIMEOUT, TimeUnit.SECONDS);
return object;
}
// 数据库中有数据放入缓存.
this.redisTemplate.opsForValue().set(key, object, RedisConstant.ALBUM_TIMEOUT, TimeUnit.SECONDS);
return object;
} catch (Throwable e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}else {
// 自旋
return cacheAspect(point);
}*/
//方式二:通过双重检测锁防止大量请求打到数据库
if (result){
try {
object = this.redisTemplate.opsForValue().get(key);
if (null != object){
return object;
}else{
// 查询数据:
object = point.proceed(point.getArgs());
// 判断,因为缓存设置的时间不同,所以需要判断
if (null == object){
// 数据库中根本没有这个数据,缓存一个空对象进去,但是这个对象是没有用的,所以这个对象的过期时间应该不能太长
this.redisTemplate.opsForValue().set(key, object, RedisConstant.ALBUM_TEMPORARY_TIMEOUT, TimeUnit.SECONDS);
return object;
}
// 数据库中有数据放入缓存.
this.redisTemplate.opsForValue().set(key, object, RedisConstant.ALBUM_TIMEOUT, TimeUnit.SECONDS);
return object;
}
} finally {
lock.unlock();
}
}
}else {
// 缓存不为空
return object;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 返回数据
return point.proceed(point.getArgs());
}
}Spring-Junit
Junit4
导入依赖
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>使用注解
@ContextConfiguration(locations = "classpath:applicationContext.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class TestSpringJunit4 {
@Autowired
private DeptService deptService;
@Test
public void testService(){
//自动装配是对于创建对象后对其属性的自动赋值,因为此处没有创建对象,如果不使用Junit,则无法deptService为默认值null。
//所以Test时无法直接进行。
deptService.saveDept(new Dept());
}
}Junit5
导入依赖
<!--spring对junit的支持相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--junit5依赖-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.0</version>
<scope>test</scope>
</dependency>使用注解
//方式一:
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:spring.xml")
public class SpringJUnit5Test{
@Autowired
private User user;
@Test
public void testUser(){
System.out.println(user.getName());
}
}
//方式二:
springJunitconfig(locations ="classpath:applicationcontext_transactionmanager.xml")
public class SpringJUnit5Test{
@Autowired
private User user;
@Test
public void testUser(){
System.out.println(user.getName());
}
}Spring-JdbcTemplate
配置
概念:Spring提供的JdbcTemplate是一个小型持久化层框架,简化Jdbc代码。对应的Mybatis是一个半自动化的ORM持久化层框架
导入jar包
xml<!--spring-context--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.1</version> </dependency> <!--spring-jdbc--> <!--spring-orm--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>5.3.1</version> </dependency> <!--导入druid的jar包--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency> <!--导入mysql的jar包--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.37</version> </dependency>编写配置文件
db.properties:设置德鲁伊连接数据库属性
jdbc.user=root jdbc.password=root jdbc.url=jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false jdbc.driver=com.mysql.cj.jdbc.DriverapplicationContext.xml【spring配置文件】
- 加载外部属性文件
- 装配数据源【DataSources】
- 装配JdbcTemplate
示例代码
xml<!-- 加载外部属性文件--> <context:property-placeholder location="classpath:db.properties"></context:property-placeholder> 在系统中也有个username属性,这时系统变量覆盖了Properties中的值,使得获取username的值为系统的用户名xx,密码为properties中的password去查询数据库,此时用户名名和密码并不匹配就会报错。 所以配置文件中不能使用username作为用户名 <!-- - 装配数据源【DataSources】--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${db.driverClassName}"></property> <property name="url" value="${db.url}"></property> <property name="username" value="${db.username}"></property> <property name="password" value="${db.password}"></property> </bean> <!-- - 装配JdbcTemplate--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean>
使用
获取JdbcTemplate
//通过ApplicationContext获取
ApplicationContext applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
//自动注入
@Autowired
private JdbcTemplate jdbcTemplate;常用API:JdbcTemplate默认:自动提交事务
jdbcTemplate.update(String sql,Object... args):通用的增删改方法
jdbcTemplate.batchUpdate(String sql,List<Object[]> args):通用批处理增删改方法
jdbcTemplate.queryForObject(String sql,Class clazz,Object... args):查询单个数值,clazz为返回单个值类型
- String sql = "select count(1) from tbl_xxx";
jdbcTemplate.queryForObject(String sql,RowMapper<T> rm,Object... args):查询单个对象
- String sql = "select col1,col2... from tbl_xxx";
javaApplicationContext applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml"); JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class); //作用是将返回的结果映射到类中 RowMapper<User> userRowMapper=new BeanPropertyRowMapper<>(User.class); User user = jdbcTemplate.queryForObject("select * from user where username='32213fujian'", userRowMapper); System.out.println(user.toString());
jdbcTemplate.query(String sql,RowMapper<T> rm,Obejct... args):查询多个对象
javaApplicationContext applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml"); JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class); //返回集合时,指定的是集合中的元素类型 RowMapper<User> userRowMapper=new BeanPropertyRowMapper<>(User.class); List<User> query = jdbcTemplate.query("select * from user where id>20", userRowMapper); for (User user : query) { System.out.println(user); }
Spring声明式事务管理
事务概述:
事务四大特征【ACID】
- 原子性:要么全部完成,要么全部不完成,如果事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。。
- 一致性:事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如转账前后的一致性规则是:转账前后两个账户的总金额是不变的
- 隔离性:指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
- 持久性:指的是只要事务成功结束,它对数据库所做的更新就必须保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。
事务三种行为
- 开启事务:connection.setAutoCommit(false)
- 提交事务:connection.commit()
- 回滚事务:connection.rollback()
编程式事务管理【传统事务管理】
获取数据库连接Connection对象
取消事务的自动提交【开启事务】
执行操作
正常完成操作时手动提交事务
执行失败时回滚事务
关闭相关资源
- 不足:
- 事务管理代码【非核心业务】与核心业务代码相耦合
- 事务管理代码分散
- 事务管理代码混乱
- 事务管理代码【非核心业务】与核心业务代码相耦合
声明式事务开启
即AOP事务管理,先横向提取事务管理代码,再动态织入
基于XMl
添加支持
xml<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.1</version> </dependency>编写配置文件
- 配置事务管理器
- 开启事务注解支持
- 注意:tx:annotation-drive中使用的是以tx为结尾的链接,而不是其他
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" 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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 配置事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 开启事务注解支持 transaction-manager默认值:transactionManager--> <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven> </beans>添加注解
- @Transactional标识在方法上,则只会影响该方法
- @Transactional标识的类上,则会影响类中所有的方法
- 注意,在多线程的情况下,@Transactional不会回滚子线程中的数据,因为@Transactional是基于ThreadLocal
java@Transactional public void purchase(String username, String isbn) { //查询book价格 Integer price = bookShopDao.findBookPriceByIsbn(isbn); //修改库存 bookShopDao.updateBookStock(isbn); //修改余额 bookShopDao.updateUserAccount(username, price); }
基于配置类
@Configuration
@ComponentScan("com.atguigu.spring6")
@EnableTransactionManagement
public class SpringConfig {
@Bean
public DataSource getDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false");
dataSource.setUsername("root");
dataSource.setPassword("root");
return dataSource;
}
@Bean(name = "jdbcTemplate")
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}事务属性
只读属性:readOnly
就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。默认值: false
//方法会覆盖类上的注解
@Transactional(readOnly = true)
public class YourService {
public void queryMethod1() {
// 查询操作
}
public void queryMethod2() {
// 查询操作
}
@Transactional(readOnly = false)
public void updateMethod() {
// 删改操作
}
}超时属性:timeout
超时回滚,释放资源。默认值:-1【未设置强制回滚时间】。超时时间是指在当前事务当中,最后一条DML语句执行之前的时间。如果最后一条DML语句后面很有很多业务逻辑,这些业务代码执行的时间不被计入超时时间。
//超时时间单位秒
@Transactional(timeout = 3)
public void buyBook(Integer bookId, Integer userId) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
//System.out.println(1/0);
}事务传播行为:Propagation
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。Spring的7种传播行为
| 传播属性 | 描述 |
|---|---|
| REQUIRED | 如果有事务在运行,当前的方法就在这个事务内运行;否则就启动一个新的事务,并在自己的事务内运行。没有就新建,有就加入 |
| REQUIRES_NEW | 当前的方法必须启动新事务,并在自己的事务内运行;如果有事务正在运行,应该将它挂起。不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起 |
| SUPPORTS | 如果有事务在运行,当前的方法就在这个事务内运行,否则可以不运行在事务中。有就加入,没有就不管了 |
| NOT_SUPPORTED | 当前的方法不应该运行在事务中,如果有运行的事务将它挂起。不支持事务,存在就挂起 |
| MANDATORY | 当前的方法必须运行在事务中,如果没有正在运行的事务就抛出异常。有就加入,没有就抛异常 |
| NEVER | 当前的方法不应该运行在事务中,如果有正在运行的事务就抛出异常。不支持事务,存在就抛异常 |
| NESTED | 如果有事务正在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则就启动一个新的事务,并在它自己的事务内运行。有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。 |
REQUIRED:即两个事务方法必须同时失败或者同时成功

REQUIRES_NEW:即两个事务之间不进行强制关联

事务回滚
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
rollbackFor属性:需要设置一个Class类型的对象,设置回滚的异常Class。
rollbackForClassName属性:需要设置一个字符串类型的全类名,设置回滚的异常Class。
noRollbackFor属性:需要设置一个Class类型的对象,设置不回滚异常Class。
rollbackFor属性:需要设置一个字符串类型的全类名,设置不回滚异常全类名。
事务隔离级别:Isolation
三种问题
所以不可重复读和幻读都是读的过程中数据前后不一致,只是前者侧重于修改,后者侧重于增加。严格来讲“幻读”可以被称为“不可重复读”的一种特殊情况,但是从数据库管理的角度来看二者是有区别的。解决“不可重复读”只要加行级锁就可以了(对第一次查出来的数据行加锁,禁止了修改和删除)。而解决“幻读”则需要加表级锁(加行级锁无法禁止添加新的数据,所以需要用表级锁)
脏读:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问 这个数据,然后使用了这个数据。(读取了未提交的数据,禁止写时读,避免了“脏读”)
幻读:指读的过程中,某些元组被增加或删除,这样进行一些集合操作,比如算总数,平均值等等,就会每次算出不一样的数。(指加入了新的行)

不可重复读:读到的是已提交的数据,比如某个读事务持续时间比较长,期间多次读取某个元组,每次读到的都是被别人改过并已提交的不同数据。可以理解为在执行任务的过程中,领导的指令一直在变。。但好歹是正式下达的指令。(读取的数据被改变,禁止读时写,避免了“不可重复读)
4 种事务隔离级别
Oracle 支持的 2 种事务隔离级别:READ-COMMITED, SERIALIZABLE。 Oracle 默认的事务隔离级别为: READ COMMITED 。
| 隔离级别 | 描述 |
|---|---|
| read-uncommitted 读未提交 | 允许A事务读取其他事务未提交和已提交的数据。会出现脏读、不可重复读、幻读问题 |
| read-committed读已提交 | 只允许A事务读取其他事务已提交的数据。可以避免脏读,但仍然会出现不可重复读、幻读问题 |
| repeatable-read可重复读 | 数据库会为查询到的每一行数据都创建一个数据版本(snapshot),并在事务内保持这个版本的一致性。也就是说,即使其他事务修改了这些数据,当前事务读取的还是开始时的数据版本,从而避免了"不可重复读"的问题。可以避免脏读和不可重复读。但是幻读问题仍然存在。 |
| serializable序列化 | 确保事务可以从一个表中读取相同的行,相同的记录。在这个事务持续期间,禁止其他事务对该表执行插入、更新、删除操作。所有并发问题都可以避免,但性能十分低下。【锁表,同一表不能被同时操作】 |
Spring Validation
Spring Validation提供了对 Bean Validation 的内置封装支持,Bean Validation 和我们很久以前学习过的 JPA 一样,只提供规范,不提供具体的实现。例如说,Hibernate Validator 。允许通过注解的方式来定义对象校验规则,把校验和业务逻辑分离开,让代码编写更加方便。
<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>Bean Validation和Hibernate Validation
Bean Validation 定义的约束注解
空和非空检查
@NotBlank:只能用于字符串不为null,并且字符串#trim()以后 length 要大于 0 。@NotEmpty:集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为null,长度不为0 (未trim的)。@NotNull:不能为null。@Null:必须为null。
数值检查
@DecimalMax(value=, inclusive=):可以用于所有的数值类型,包括整数和浮点数。当 inclusive=false时,检查带注释的值是否小于指定的最大值。否则,该值是否小于或等于指定的最大值。
@DecimalMin(value=, inclusive=):可以用于所有的数值类型,包括整数和浮点数。当 inclusive=false时,检查带注释的值是否大于指定的最小值。 否则,该值是否大于或等于指定的最小值。@Digits(integer, fraction):被注释的元素必须是一个数字,整数部分的位数不超过integer, 小数部分的位数不超过fraction。@Positive:判断正数。@PositiveOrZero:判断正数或 0 。@Max(value):只能用于整数类型,该字段的值只能小于或等于该值。@Min(value):只能用于整数类型,该字段的值只能大于或等于该值。@Negative:判断负数。@NegativeOrZero:判断负数或 0 。
Boolean 值检查
@AssertFalse:被注释的元素必须为true。@AssertTrue:被注释的元素必须为false。
长度检查
@Size(max, min):检查该字段的size是否在min和max之间,可以是字符串、数组、集合、Map 等。
日期检查
@Future:被注释的元素必须是一个将来的日期。@FutureOrPresent:判断日期是否是将来或现在日期。@Past:检查该字段的日期是在过去。@PastOrPresent:判断日期是否是过去或现在日期。
其它检查
@Email:被注释的元素必须是电子邮箱地址。@Pattern(value):被注释的元素必须符合指定的正则表达式。
Hibernate Validator 附加的约束注解
org.hibernate.validator.constraints包下,定义了一系列的约束( constraint )注解。如下:
@Range(min=, max=):被注释的元素必须在合适的范围内,用于数值类型。@Length(min=, max=):被注释的字符串的大小必须在指定的范围内。@URL(protocol=,host=,port=,regexp=,flags=):被注释的字符串必须是一个有效的 URL 。@SafeHtml:判断提交的 HTML 是否安全。例如说,不能包含 javascript 脚本等等。
一、实现接口校验数据
通过实现org.springframework.validation.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,是Spring封装的校验工具类,帮助快速实现校验。
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");
}
}
}
//在代码中进行校验
public static void main(String[] args) {
//创建person对象
Person person = new Person();
person.setName("lucy");
person.setAge(-1);
// 创建Person对应的DataBinder
DataBinder binder = new DataBinder(person);
// 设置校验
binder.setValidator(new PersonValidator());
// 由于Person对象中的属性为空,所以校验不通过
binder.validate();
//输出结果
BindingResult results = binder.getBindingResult();
System.out.println(results.getAllErrors());
}二、Bean Validation注解实现
//一、创建配置类,配置LocalValidatorFactoryBean
@Configuration
@ComponentScan("com.atguigu.spring6.validation.method2")
public class ValidationConfig {
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
//二、创建实体类,使用注解定义校验规则
public class User {
@NotNull
private String name;
@Min(0)
@Max(120)
private int age;
}
//三、使用两种不同的校验器实现
//使用jakarta.validation.Validator校验
@Service
public class MyService1 {
@Autowired
private Validator validator;
public boolean validator(User user){
Set<ConstraintViolation<User>> sets = validator.validate(user);
return sets.isEmpty();
}
}
//使用org.springframework.validation.Validator校验
@Service
public class MyService2 {
@Autowired
private Validator validator;
public boolean validaPersonByValidator(User user) {
BindException bindException = new BindException(user, user.getName());
validator.validate(user, bindException);
return bindException.hasErrors();
}
}三、基于方法校验
@Valid 和 @Validated
如果校验失败则会抛出 ConstraintViolationException 异常
| @Validated | @Valid |
|---|---|
| Bean Validation 所定义 | Spring Validation 所定义 |
| 提供分组功能,可在入参验证时,根据不同的分组采用不同的验证机制。 | 无分组功能 |
| 可以用在类型、方法和方法参数上。但是不能用在成员属性上 | 可以用在方法、构造函数、方法参数和成员属性上(两者是否能用于成员属性上直接影响能否提供嵌套验证的功能) |
| 用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性上。也无法提供框架进行嵌套验证。但是能配合嵌套验证注解@Valid进行嵌套验证。 | 用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。 |
如果校验失败则会抛出 ConstraintViolationException 异常
requestParam校验
用于校验URL上拼接的参数
1、首先在 Controller 类上添加注解:@Validated,否则校验不生效
2、在标记了 @RequestParam 的参数前添加上相应的校验注解如 @Length 等
@RequestMapping(value = "/valid")
@Validated // 标注类上,可以对接口的 RequestParam,PathVariable 参数进行校验
@RestController
@Slf4j
public class ValidationController {
@GetMapping(value = "/test1")
public String test1(@RequestParam @Max(value = 2) String name,
@RequestParam @Min(value = 3) String address) {
return "ok";
}
}PathVariable校验
用于校验请求 URL 的上的占位参数校验
1、也是在类上标记注解:@Validated
2、在标记了 @PathVariable 注解的参数前添加校验注解
@RequestMapping(value = "/valid")
@Validated // 标注类上,可以对接口的 RequestParam,PathVariable 参数进行校验
@RestController
@Slf4j
public class ValidationController {
@GetMapping(value = "/test2/{id}")
public String test2(@PathVariable @Length(max = 3, min = 1) String id) {
return "ok";
}
}RequestBody校验
用于校验入参是POST请求的 JSON 字符串
1、在对应的入参当前有 @RequestBody 注解时,Spring会将其识别为 JSON 字符串,要求入参格式为:application/json,在参数前添加注解:@Validated 或者 @Valid,两者都支持。注意:此时类上不需要标注注解
2、在实体类的属性变量上添加校验注解,标记的属性才会被校验(如果实体类的属性变量是一个对象,就需要使用@Valid标注进行嵌套验证)
public class Address {
@NotBlank
private String province;
@Length(max = 10, min = 3)
private String city;
//进行嵌套验证
@Valid
private City city;
}
@RequestMapping(value = "/valid")
@Validated // 标注类上,可以对接口的 RequestParam,PathVariable 参数进行校验
@RestController
@Slf4j
public class ValidationController {
@PostMapping(value = "/test3")
public String test3(@RequestBody @Validated Address address) {
return "ok";
}
}嵌套校验
如果实体类的属性变量是一个对象,就需要使用@Valid标注进行嵌套验证
public class Address {
//进行嵌套验证
@Valid
private City city;
//对集合中的每一个元素验证
@Valid
private List<City> cityList;
}四、自定义校验规则
@Target({FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = GenderValidator.class) // 指定校验规则类
public @interface Gender {
// 校验失败时返回的信息
String message() default "性别为:male/female/unKnow";
//分组校验
Class<?>[] groups() default {};
// 参数取值是否可以为空
boolean required() default false;
Class<? extends Payload>[] payload() default {};
}
.
@Slf4j
public class GenderValidator implements ConstraintValidator<Gender, String> {
// 读取存放入参的属性值
private boolean required;
/**
* 校验实现
*
* @param value 参数的实际传值
* @param context ?
* @return true-校验成功,false-校验失败
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtils.hasText(value)) {
return value.equals("male") || value.equals("female")
|| value.equals("unKnow");
}
return !required;
}
/**
* 初始化校验值,该方法可以不重写
* 可以读取注解的属性值到当前对象
*
* @param constraintAnnotation {@link Gender}
*/
@Override
public void initialize(Gender constraintAnnotation) {
required = constraintAnnotation.required();
}
}校验配置
@Configuration // 表示这是一个Spring配置类
public class ValidationConfig {
@Bean // 表示这个方法返回一个Spring Bean
public Validator validator(){
// 创建一个Hibernate Validator工厂,配置为快速失败模式
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true) // 当出现校验失败时立即结束校验,不进行后续其他参数的校验
.buildValidatorFactory();
// 从工厂中获取Validator实例并返回
return validatorFactory.getValidator();
}
// SpringBoot会自动创建下面这个类
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor(){
// 创建一个MethodValidationPostProcessor实例
MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
// 设置其使用的Validator
methodValidationPostProcessor.setValidator(validator());
// 返回MethodValidationPostProcessor实例
return methodValidationPostProcessor;
}
}分组校验
如果同一个参数在不同使用场景下的校验规则不同,则需要使用到分组校验。例如:新增用户时不需要指定用于昵称,可以为空;但是在更新用户时,昵称不能空。
注意:@Validated(Group1.class) 指定分组校验规则后,那些校验注解都必须加上这些groups属性指定分组,否则校验不生效!
1、自定义分组类或者接口,作为分组使用,这个类无实际使用,仅作为分组标志。
2、在校验注解上使用 groups 属性指定分组。
3、在 @Validated 指定是哪个分组的规则。
public interface Group1 {
}
@Data
public class Human {
@NotBlank(groups = Group1.class)
private String name;
private String address;
}
@PostMapping(value = "/test5")
public String test5(@RequestBody @Validated(Group1.class) Human human) {
return "test5";
}Spring全流程
===============BeanFactory的创建及预准备工作================
1、prepareRefresh()刷新前的预处理;
1)、initPropertySources()初始化一些属性设置;子类自定义个性化的属性设置方法;
2)、getEnvironment().validateRequiredProperties();检验属性的合法等
3)、earlyApplicationEvents= new LinkedHashSet<ApplicationEvent>();保存容器中的一些早期的事件;
2、obtainFreshBeanFactory();获取BeanFactory;
1)、refreshBeanFactory();创建BeanFactory;
2)、getBeanFactory();返回刚才GenericApplicationContext创建的BeanFactory对象;
3)、将创建的BeanFactory【DefaultListableBeanFactory】返回;
3、prepareBeanFactory(beanFactory);BeanFactory的预准备工作(BeanFactory进行一些设置);
1)、设置BeanFactory的类加载器、支持表达式解析器...
2)、添加部分BeanPostProcessor【ApplicationContextAwareProcessor】
3)、设置忽略的自动装配的接口EnvironmentAware、EmbeddedValueResolverAware、xxx;
4)、注册可以解析的自动装配;我们能直接在任何组件中自动注入:
BeanFactory、ResourceLoader、ApplicationEventPublisher、ApplicationContext
5)、添加BeanPostProcessor【ApplicationListenerDetector】
6)、添加编译时的AspectJ;
7)、给BeanFactory中注册一些能用的组件;
environment【ConfigurableEnvironment】、
systemProperties【Map<String, Object>】、
systemEnvironment【Map<String, Object>】
4、postProcessBeanFactory(beanFactory);BeanFactory准备工作完成后进行的后置处理工作;
1)、子类通过重写这个方法来在BeanFactory创建并预准备完成以后做进一步的设置
==================BeanFactory后置处理器执行=====================
5、invokeBeanFactoryPostProcessors(beanFactory);执行BeanFactoryPostProcessor的方法;
BeanFactoryPostProcessor:BeanFactory的后置处理器。在BeanFactory标准初始化之后执行的;
两个接口:BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor
1)、执行BeanFactoryPostProcessor的方法;
先执行BeanDefinitionRegistryPostProcessor
1)、获取所有的BeanDefinitionRegistryPostProcessor;
2)、看先执行实现了PriorityOrdered优先级接口的BeanDefinitionRegistryPostProcessor、
postProcessor.postProcessBeanDefinitionRegistry(registry)
3)、在执行实现了Ordered顺序接口的BeanDefinitionRegistryPostProcessor;
postProcessor.postProcessBeanDefinitionRegistry(registry)
4)、最后执行没有实现任何优先级或者是顺序接口的BeanDefinitionRegistryPostProcessors;
postProcessor.postProcessBeanDefinitionRegistry(registry)
再执行BeanFactoryPostProcessor的方法
1)、获取所有的BeanFactoryPostProcessor
2)、看先执行实现了PriorityOrdered优先级接口的BeanFactoryPostProcessor、
postProcessor.postProcessBeanFactory()
3)、在执行实现了Ordered顺序接口的BeanFactoryPostProcessor;
postProcessor.postProcessBeanFactory()
4)、最后执行没有实现任何优先级或者是顺序接口的BeanFactoryPostProcessor;
postProcessor.postProcessBeanFactory()
==================注册Bean后置处理器====================
6、registerBeanPostProcessors(beanFactory);注册BeanPostProcessor(Bean的后置处理器)【 intercept bean creation】
不同接口类型的BeanPostProcessor;在Bean创建前后的执行时机是不一样的
BeanPostProcessor、
DestructionAwareBeanPostProcessor、
InstantiationAwareBeanPostProcessor、
SmartInstantiationAwareBeanPostProcessor、
MergedBeanDefinitionPostProcessor【internalPostProcessors】、
1)、获取所有的 BeanPostProcessor;后置处理器都默认可以通过PriorityOrdered、Ordered接口来执行优先级
2)、先注册PriorityOrdered优先级接口的BeanPostProcessor;
把每一个BeanPostProcessor;添加到BeanFactory中
beanFactory.addBeanPostProcessor(postProcessor);
3)、再注册Ordered接口的
4)、最后注册没有实现任何优先级接口的
5)、最终注册MergedBeanDefinitionPostProcessor;
6)、注册一个ApplicationListenerDetector;来在Bean创建完成后检查是否是ApplicationListener,如果是
applicationContext.addApplicationListener((ApplicationListener<?>) bean);
====================创建MessageSource用于国际化==============
7、initMessageSource();初始化MessageSource组件(做国际化功能;消息绑定,消息解析);
1)、获取BeanFactory
2)、看容器中是否有id为messageSource的,类型是MessageSource的组件
如果有赋值给messageSource,如果没有自己创建一个DelegatingMessageSource;
MessageSource:取出国际化配置文件中的某个key的值;能按照区域信息获取;
3)、把创建好的MessageSource注册在容器中,以后获取国际化配置文件的值的时候,可以自动注入MessageSource;
beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);
MessageSource.getMessage(String code, Object[] args, String defaultMessage, Locale locale);
=======================创建事件派发器======================
8、initApplicationEventMulticaster();初始化事件派发器;
1)、获取BeanFactory
2)、从BeanFactory中获取applicationEventMulticaster的ApplicationEventMulticaster;
3)、如果上一步没有配置;创建一个SimpleApplicationEventMulticaster
4)、将创建的ApplicationEventMulticaster添加到BeanFactory中,以后其他组件直接自动注入
=========================预留自定义接口=====================
9、onRefresh();留给子容器(子类)
1、子类重写这个方法,在容器刷新的时候可以自定义逻辑;
===================添加监听器到事件派发器中====================
10、registerListeners();给容器中将所有项目里面的ApplicationListener注册进来;
1、从容器中拿到所有的ApplicationListener
2、将每个监听器添加到事件派发器中;
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
3、派发之前步骤产生的事件;
====================初始化单例Bean=========================
11、finishBeanFactoryInitialization(beanFactory);初始化所有剩下的单实例bean;
1、beanFactory.preInstantiateSingletons();初始化后剩下的单实例bean
1)、获取容器中的所有Bean,依次进行初始化和创建对象
2)、获取Bean的定义信息;RootBeanDefinition
3)、Bean不是抽象的,是单实例的,是懒加载;
1)、判断是否是FactoryBean;是否是实现FactoryBean接口的Bean;
2)、不是工厂Bean。利用getBean(beanName);创建对象
0、getBean(beanName); ioc.getBean();
1、doGetBean(name, null, null, false);
2、先获取缓存中保存的单实例Bean。如果能获取到说明这个Bean之前被创建过(所有创建过的单实例Bean都会被缓存起来)
从private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);获取的
3、缓存中获取不到,开始Bean的创建对象流程;
4、标记当前bean已经被创建
5、获取Bean的定义信息;
6、【获取当前Bean依赖的其他Bean;如果有按照getBean()把依赖的Bean先创建出来;】
7、启动单实例Bean的创建流程;
1)、createBean(beanName, mbd, args);
2)、Object bean = resolveBeforeInstantiation(beanName, mbdToUse);让BeanPostProcessor先拦截返回代理对象;
【InstantiationAwareBeanPostProcessor】:提前执行;
先触发:postProcessBeforeInstantiation();
如果有返回值:触发postProcessAfterInitialization();
3)、如果前面的InstantiationAwareBeanPostProcessor没有返回代理对象;调用4)
4)、Object beanInstance = doCreateBean(beanName, mbdToUse, args);创建Bean
1)、【创建Bean实例】;createBeanInstance(beanName, mbd, args);
利用工厂方法或者对象的构造器创建出Bean实例;
2)、applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
调用MergedBeanDefinitionPostProcessor的postProcessMergedBeanDefinition(mbd, beanType, beanName);
3)、【Bean属性赋值】populateBean(beanName, mbd, instanceWrapper);
赋值之前:
1)、拿到InstantiationAwareBeanPostProcessor后置处理器;
postProcessAfterInstantiation();
2)、拿到InstantiationAwareBeanPostProcessor后置处理器;
postProcessPropertyValues();
3)、应用Bean属性的值;为属性利用setter方法等进行赋值;
applyPropertyValues(beanName, mbd, bw, pvs);
4)、【Bean初始化】initializeBean(beanName, exposedObject, mbd);
1)、【执行Aware接口方法】invokeAwareMethods(beanName, bean);执行xxxAware接口的方法
BeanNameAware\BeanClassLoaderAware\BeanFactoryAware
2)、【执行后置处理器初始化之前】applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
BeanPostProcessor.postProcessBeforeInitialization();
3)、【执行初始化方法】invokeInitMethods(beanName, wrappedBean, mbd);
1)、是否是InitializingBean接口的实现;执行接口规定的初始化;
2)、是否自定义初始化方法;
4)、【执行后置处理器初始化之后】applyBeanPostProcessorsAfterInitialization
BeanPostProcessor.postProcessAfterInitialization();
5)、注册Bean的销毁方法;
5)、将创建的Bean添加到缓存中singletonObjects;
ioc容器就是这些Map;很多的Map里面保存了单实例Bean,环境信息。。。。;
所有Bean都利用getBean创建完成以后;
检查所有的Bean是否是SmartInitializingSingleton接口的;如果是;就执行afterSingletonsInstantiated();
12、finishRefresh();完成BeanFactory的初始化创建工作;IOC容器就创建完成;
1)、initLifecycleProcessor();初始化和生命周期有关的后置处理器;LifecycleProcessor
默认从容器中找是否有lifecycleProcessor的组件【LifecycleProcessor】;如果没有new DefaultLifecycleProcessor();
加入到容器;
写一个LifecycleProcessor的实现类,可以在BeanFactory
void onRefresh();
void onClose();
2)、 getLifecycleProcessor().onRefresh();
拿到前面定义的生命周期处理器(BeanFactory);回调onRefresh();
3)、publishEvent(new ContextRefreshedEvent(this));发布容器刷新完成事件;
4)、liveBeansView.registerApplicationContext(this);
========================总结=====================
1)、Spring容器在启动的时候,先会保存所有注册进来的Bean的定义信息;
1)、xml注册bean;<bean>
2)、注解注册Bean;@Service、@Component、@Bean、xxx
2)、Spring容器会合适的时机创建这些Bean
1)、用到这个bean的时候;利用getBean创建bean;创建好以后保存在容器中;
2)、统一创建剩下所有的bean的时候;finishBeanFactoryInitialization();
3)、后置处理器;BeanPostProcessor
1)、每一个bean创建完成,都会使用各种后置处理器进行处理;来增强bean的功能;
AutowiredAnnotationBeanPostProcessor:处理自动注入
AnnotationAwareAspectJAutoProxyCreator:来做AOP功能;
xxx....
增强的功能注解:
AsyncAnnotationBeanPostProcessor
....
4)、事件驱动模型;
ApplicationListener;事件监听;
ApplicationEventMulticaster;事件派发:BeanFactoryPostProcessor
允许我们在 Spring IoC 容器实例化任何 bean 之前,读取配置的元数据,并可能进行修改。
public class RolePermission implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
}BeanDefinitionRegistryPostProcessor
BeanFactoryPostProcessor的子类,多了一个方法
public class RolePermission implements BeanDefinitionRegistryPostProcessor {
//先执行
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
}
//后执行
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
}
//BeanDefinitionRegistry方法
// 注册一个新的Bean定义。Bean名称是唯一的,如果在Bean定义注册表中已经存在相同名称的Bean定义,将会抛出BeanDefinitionStoreException异常。
void registerBeanDefinition(String var1, BeanDefinition var2) throws BeanDefinitionStoreException;
// 移除已注册的Bean定义。如果没有找到指定名称的Bean定义,将会抛出NoSuchBeanDefinitionException异常。
void removeBeanDefinition(String var1) throws NoSuchBeanDefinitionException;
// 获取指定名称的Bean定义。如果没有找到指定名称的Bean定义,将会抛出NoSuchBeanDefinitionException异常。
BeanDefinition getBeanDefinition(String var1) throws NoSuchBeanDefinitionException;
// 检查是否包含指定名称的Bean定义。
boolean containsBeanDefinition(String var1);
// 获取所有已注册的Bean定义的名称。
String[] getBeanDefinitionNames();
// 获取已注册的Bean定义的数量。
int getBeanDefinitionCount();
// 检查指定的Bean名称是否已经在使用。
boolean isBeanNameInUse(String var1);InstantiationAwareBeanPostProcessor
public class RolePermission implements InstantiationAwareBeanPostProcessor {
//实例化 Bean 之前调用
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
return InstantiationAwareBeanPostProcessor.super.postProcessBeforeInstantiation(beanClass, beanName);
}
//实例化 Bean 之后调用
@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
return InstantiationAwareBeanPostProcessor.super.postProcessAfterInstantiation(bean, beanName);
}
//对 Bean 进行属性填充(依赖注入)之前调用,主要用于对 Bean 的属性值进行操作
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
return InstantiationAwareBeanPostProcessor.super.postProcessProperties(pvs, bean, beanName);
}
//对 Bean 进行属性填充(依赖注入)之前调用,不仅可以对 Bean 的属性值进行操作,还可以对 Bean 的属性进行更复杂的操作,例如,修改属性的可访问性,修改属性的类型等
@Override
public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
return InstantiationAwareBeanPostProcessor.super.postProcessPropertyValues(pvs, pds, bean, beanName);
}
//Bean 初始化之前调用
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return InstantiationAwareBeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
}
//Bean 初始化之后调用
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return InstantiationAwareBeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}
}SmartInitializingSingleton
在所有单例 Bean 都初始化完成之后被调用,也就是说,当所有的 Bean 都已经被加载、初始化、并且所有的 BeanPostProcessor 都已经被应用之后,这个方法才会被调用。
public class RolePermission implements SmartInitializingSingleton {
@Override
public void afterSingletonsInstantiated() {
}
}事件监听
//创建事件监听器,并将其加入到容器
//Spring会发布以下事件,其都是ApplicationEvent的子类
//ContextClosedEvent:关闭容器会发布这个事件;
//ContextRefreshedEvent:容器刷新完成(所有bean都完全创建)会发布这个事件;
//通过实现ApplicationEvent接口来监听事件
@Component
public class MyApplicationListener implements ApplicationListener<ApplicationEvent> {
//当容器中发布此事件以后,方法触发
@Override
public void onApplicationEvent(ApplicationEvent event) {
//TODO Auto-generated method stub
System.out.println("收到事件:"+event);
}
}
//通过注解标识来监听事件
@Service
public class UserService {
@EventListener(classes={ApplicationEvent.class})
public void listen(ApplicationEvent event){
System.out.println("UserService。。监听到的事件:"+event);
}
}
//发布一个事件
AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
applicationContext.publishEvent(new ApplicationEvent("event") {
});Spring5新注解
| 含义 | 名称 | 可标注位置 |
|---|---|---|
| @Nullable | 可以为空 | @Target({ElementType.METHOD, ElementType.PARAMETER,ElementType.FIELD} |
| @NonNull | 不应为空 | @Target({ElementType.METHOD, ElementType.PARAMETER,ElementType.FIELD}) |
| @NonNullFields | 在特定包下的字段不应为空 | @Target(ElementType.PACKAGE)<br/>@TypeQualifierDefault(ElementType.*FIELD@Target(ElementType.PACKAGE) |
| @NonNullApi | 参数和方法返回值不应为空 | @TypeQualifierDefault({ElementType.METHOD,ElementType.PARAMETER) |
@Nullable作用
位置:可以书写在方法&属性上面&参数前面。
作用:表示当前方法或属性可以为空,当前属性为空时,消除空指针异常。