介绍
- Spring项目官网:https://spring.io/projects ,其中的
spring-framework
即是spring框架内容 - 历史:(1) spring 1.x,xml配置时代 (2) spring 2.x,注解时代 (3) spring 3.x,java配置
- Spring模块(每个模块有个jar包):
- 核心容器:
spring-core
,spring-beans
,spring-context
(运行时spring容器),spring-context-support
(spring对第三方包的集成支持),spring-expression
(使用表达式语言在运行时查询和操作对象) - AOP:spring-aop, spring-aspects
- 消息:spring-messaging
- 数据访问:
spring-jdbc
,spring-tx
(提供编程式和声明明式事物支持),spring-orm
,spring-oxm
(提供对对象/xml映射技术支持),spring-jms
(提供jms支持) - Web:
spring-web
(在web项目中提供spring容器),spring-webmvc
(基于Servlet的SpringMVC),spring-websocket
,spring-webmvc-portlet
- 核心容器:
- Spring生态:
Spring Boot
(使用默认开发配置来快速开发)、Spring Cloud
(为分布式系统开发提供工具集)等 - 本文档基于Spring4.3.8
HelloWorld
maven依赖
1
2
3
4
5
6<dependency>
<groupId>org.springframework</groupId>
<!--包含spring-core、spring-beans、spring-aop、spring-expression、spring-instrument-->
<artifactId>spring-context</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>调用
1
2
3
4
5// ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); // AppConfig为定义的java配置类
Hello hello = context.getBean("hello", Hello.class);
hello.hello();
常见注解
往容器中注册Bean(组件)
组件注解+包扫描
- 组件注解(下面几个注解效果一样)
@Component
没有明确的角色,或@Component(“sq”)定义别名@Service
在业务逻辑层(cn.aezo.spring.aop_spel.service)使用@Repository
在数据访问层(cn.aezo.spring.aop_spel.dao)使用@Controller
在展现层使用
- 包扫描
@ComponetScan("cn.aezo")
- @ComponentScan(basePackages = {“cn.aezo.sqbiz”, “cn.aezo.utils”})
- 定义需要扫描的包名,并将里面的
@Component
、@Service
、@Repository
、@Controller
注解的类注册为Bean
1 | // java8 @Repeatable表示可重复注解 excludeFilters不扫描过滤,includeFilters扫描过滤(需设置useDefaultFilters=false) |
@Bean导入第三方包里面的组件
@Bean
- 注解配置类中的方法,表示当前方法的返回值是一个Bean,Bean的名称(id)默认是方法名
@Bean("newBeanName")
自定义Bean名称- Springboot项目,将
@Bean
加入在test的代码中时无法注入
@Import给容器导入一个组件
@Import("cn.aezo.test.MyBean")
- Spring Boot中大量的EnableXXX都使用了@Import注解
- 可以导入普通的POJO类或带有
@Configuration
注解的配置类,或导入实现了ImportSelector
/ImportBeanDefinitionRegistrar
/DeferredImportSelector
接口的返回的类。功能类似XML配置用来导入配置类- DeferredImportSelector 为 ImportSelector 的子接口。区别是他会在所有的@Configuration类加载完成之后再加载返回的配置类,而ImportSelector会在当前Configuration类加载之前去加载返回的配置类
- 可以使用@Order注解或者Ordered接口来指定DeferredImportSelector的加载顺序
@ImportResource
和@Import类似,区别就是@ImportResource导入的是配置文件。其属性default和locations作用相同,都是用来指定配置文件的位置;reader属性则用来指定配置文件解析器,内置XmlBeanDefinitionReader和GroovyBeanDefinitionReader,也可自定义。Spring还是推荐使用@Import而不是@ImportResource
@Import
结合ImportSelector
1 | // ### 基于导入选择器 |
@Import
结合ImportBeanDefinitionRegistrar
1 | // ### 基于ImportBeanDefinitionRegistrar |
- 可对Import进一步封装
1 | ({ElementType.TYPE}) |
使用Spring提供的FactoryBean(工厂Bean)
- 通过
id(name)
获取的是FactoryBean的getObject返回的对象。使用&name
可获取FactoryBean本身 BeanFactory
和FactoryBean
区别 https://www.cnblogs.com/aspirant/p/9082858.html- 模拟mybatis参考源码
smjava -> javaee -> springArch -> spring5 -> demo3
1 | // ### 实现FactoryBean |
基于Springboot的EnableAutoConfiguration
- 第三方类(不在ComponentScan的扫描范围)
1 |
|
自动装配(取出Bean赋值给当前类属性)
@Autowired
Spring提供- 默认按ByType(根据类型),如果根据类型找到多个再按照ByName(变量名称)注入(如果此时名称不存在则报错)
- 如果想用by name,Bean定义时如
@Bean(name="my-bean-name"),再联合使用
@Qualifier(“my-bean-name”)。
对于默认的Bean可通过添加@Primary
(使用时则按照单数据源注入)
- 如果想用by name,Bean定义时如
- 可以对方法、字段、构造器、参数使用
- 注入集合方式
@Autowired List<Monitor> monitors;
也可以注入集合- 注意:如果当前类实现了Monitor接口,则注入到集合中会排除当前类(即要注入的类不能是类本身,会触发无限递归注入)
- 如果元素增加
@Order
注解,在注入时会自动进行排序。也可使用list.sort(AnnotationAwareOrderComparator.INSTANCE)
手动排序list(用于非注入的场景,元素也需要增加 @Order 注解)。值越小越优先,可以为负值
@Autowired Map<String, Monitor> monitorMap;
注入到Map中,此时将 Bean 的 name 作为 key
- 默认按ByType(根据类型),如果根据类型找到多个再按照ByName(变量名称)注入(如果此时名称不存在则报错)
@Resource
JSR-250提供(常用)- 先按ByName名称查找(当然类型也必须匹配),如果找不到再按ByType类型查找(如果根据类型找到多个则报错)
- 只能对方法、字段使用
@Inject
JSR-330提供,类似@Autowired- @Autowired和@Resource推荐用法
- 根据使用场景:@Resource倾向于确定性的单一资源,@Autowired为类型去匹配符合此类型所有资源
- 使用@Autowired时,推荐使用构造函数注入和set方法注入
- 参考:https://zhuanlan.zhihu.com/p/615487137
- 各种DI方式的优缺点
- 构造器注入:强依赖性 (即必须使用此依赖),不变性(各依赖不会经常变动)
- Setter注入:可选(没有此依赖也可以工作),可变(依赖会经常变动)
- Field注入:大多数情况下尽量少使用字段注入,一定要使用的话,@Resource相对@Autowired 对IoC容器的耦合更低
- Field注入的优点
- 使用方便(优先敏捷度再考虑松耦合)
- Field注入的缺点
- 不能像构造器那样注入不可变的对象
- 依赖对外部不可见 ,外界可以看到构造器和setter,但无法看到私有字段,自然无法了解所需依赖
- 会导致组件与IoC容器紧耦合 (这是最重要的原因,离开了IoC容器去使用组件,在注入依赖时就会十分困难)
- 导致单元测试也必须使用IoC容器 ,原因同上
- 依赖过多时不够明显 ,比如我需要10个依赖,用构造器注入就会显得庞大,这时候应该考虑一下此组件是不是违反了单一职责原则
- Field注入的优点
获取Bean
- 自动注入 ^5
1 |
|
- 实现
ApplicationContextAware
接口,参考下文ApplicationContext:SpringU- 下列工具相当于把
ApplicationContext
存储在属性中,其他类对象可通过此对象属性获取ApplicationContext - 上述其他类对象,如自行new的对象中需要注入其他Bean,此时当前类没有被Spring托管,则可通过SpringU中间缓存获取
- 下列工具相当于把
@Autowired注入给静态属性示例
1 |
|
条件注解@Conditional
- 根据满足某一特定条件来创建某个特定的Bean. 如某个Bean创建后才会创建另一个Bean(Spring 4.x)
- 类似的如
@Profile("dev")
标识仅在开发环境才会注入此Bean 内置条件
@ConditionalOnProperty
要求配置属性匹配条件havingValue
表示对应参数值。注解中如果省略此属性,则此参数为false时,条件结果才为falsematchIfMissing
表示缺少该配置属性时是否可以加载。如果为true,即表示没有该配置属性时也会正常加载;反之则不会生效- eg:@ConditionalOnProperty(value = {“feign.compression.response.enabled”}, matchIfMissing = false) 、@ConditionalOnProperty(name = “zuul.use-filter”, havingValue = “true”, matchIfMissing = false)
@ConditionalOnMissingBean
当给定的类型/类名/注解在beanFactory中不存在时返回true,各类型间是or的关系(不填参数则表示没有@Bean返回的对象类型时生效)只能对@Bean生效,如直接注解在@Bean的方法上,或注解在含有@Bean方法的类上;对@Service等类不生效,需要设置成@Bean模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// eg:@ConditionalOnMissingBean(type = {"okhttp3.OkHttpClient"})
//可以标注在类和方法上
({ElementType.TYPE, ElementType.METHOD})
(RetentionPolicy.RUNTIME)
//使用了@Conditional注解,条件类是OnBeanCondition(逻辑实现)
({OnBeanCondition.class})
public ConditionalOnMissingBean {
// 需要检查的 bean 的 class 类型。如: @ConditionalOnMissingBean(value = MyService.class)
Class<?>[] value() default {};
// 需要检查的 bean 的 class 类型名称。默认。@ConditionalOnMissingBean(type = "MyService") == @ConditionalOnMissingBean
String[] type() default {};
// 识别匹配 bean 时,可以被忽略的 bean 的 class 类型
Class<?>[] ignored() default {};
// 识别匹配 bean 时,可以被忽略的 bean 的 class 类型名称
String[] ignoredType() default {};
// 当 ApplicationContext 不包含带有这些注解的 bean 时条件匹配。如:@ConditionalOnMissingBean(annotation = MyServiceAnno.class)
Class<? extends Annotation>[] annotation() default {};
// 需要检查的 bean 的 name。如: @ConditionalOnMissingBean(name = "myService")
String[] name() default {};
// 搜索容器层级:当前容器/父容器/所有(默认)
SearchStrategy search() default SearchStrategy.ALL;
}
@ConditionalOnBean
与上相反,在存在某个bean的时候- eg:@ConditionalOnBean({Client.class})
@ConditionalOnMissingClass
当前classpath不可以找到某个类型的类时,各类型间是and的关系@ConditionalOnClass
与上相反,当前classpath可以找到某个类型的类时- eg:@ConditionalOnClass({Feign.class})、@ConditionalOnClass(name = {“feign.hystrix.HystrixFeign”})
@ConditionalOnResource
当前classpath是否存在某个资源文件@ConditionalOnWebApplication
当前spring context是否是web应用程序@ConditionalOnNotWebApplication
web环境不存在时@ConditionalOnExpression
spel表达式执行为true@ConditionalOnSingleCandidate
当给定类型的bean存在并且指定为Primary的给定类型存在时返回true@ConditionalOnCloudPlatform
当所配置的CloudPlatform为激活时返回true@ConditionalOnJava
运行时的java版本号是否包含给定的版本号,如果包含返回匹配,否则返回不匹配@ConditionalOnJndi
给定的jndi的Location 必须存在一个,否则返回不匹配
- 自定义条件
1 | // ## 条件判断 |
属性赋值(@Value)
Spring中使用属性赋值
@Value
在其中输入EL表达式(Spring-EL)。可对资源进行注入@PropertySource
注入外部配置文件值
1 | // @ComponentScan("cn.aezo.spring.base.annotation.el") |
SpringBoot中使用属性赋值
@PropertySource
在springboot应用中注入外部配置文件值,支持如下两种配置方式,如果注入application配置则@PropertySource可省略- @PropertySource + @Value
- @PropertySource + @ConfigurationProperties
springboot v2.0.1之后,定义自定义参数(MyValue.java)要么写到Application.java同目录,要么加下列依赖。这个依赖会把配置文件的值注入到@Value里面,也可以通过@PropertySource(“classpath:application.yml”)注入
1
2
3
4
5<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>定义/调用
- 优先级:命令行参数 > application.properties > JavaBean
- 说明:命令行参数设置了此属性配置,但属性的值为空,此时可以覆盖
application.properties
的初始值,但是不会覆盖JavaBean的初始值
1 | // ### 方法一 |
@PropertySource分环境读取配置
1 | // 创建 minions.yml、minions-dev.yml 等配置文件。可根据环境读取,且支持覆盖 |
组合注解和元注解
- 元注解是指可以注解到其他注解上的注解,被元注解注解之后的注解称之为组合注解
- 如
@Configuration
是包含@Component
的组合注解,@Component
为元注解 示例,将
@Configuration
和@ComponentScan
组合成一个注解1
2
3
4
5
6
7
8(ElementType.TYPE)
(RetentionPolicy.RUNTIME)
public WiselyConfiguration {
String[] value() default {};
}
其他注解
- Http相关
- @Controller 注解在类上
- @RestController 注解在类上
- @GetMapping
- @PostMapping
- @RequestMapping 同时支持GET/POST
- @PathVariable 读取URL路径参数(/api/user/{id})
- @RequestParam 读取URL地址参数,默认为必传,可配置可选
- @PathParam 类似@RequestParam,默认不必填
- @RequestBody 读取POST数据体
Bean
作用域@Scope
@Scope("prototype")
注解类(配置Bean的作用域,可和@Bean
联合使用)singleton
整个容器共享一个实例(默认配置). IOC容器启动(new AnnotationConfigApplicationContext()
),就会创建所有的Bean(@Lazy
懒加载,仅用于单例模式,只有在第一次获取Bean时才创建此Bean)prototype
每次调用新建一个实例. IOC容器启动,不会创建Bean,只有在获取的时候创建Bean- 此对象
@Autowired
注入几次就会产生几个对象,和调用此对象方法无关 - beanFactory.getBeansOfType 等API获取Bean也会创建
- 如果此类型Bean含有有状态字段,则也容易产生并发问题,prototype并不能解决
- 此对象
request
Web项目中,每一个HttpRequest新建一个实例session
Web项目中,同一个session创建一个实例globalSession
用于portal应用
- SpringBoot的作用域如:
@RequestScope
、@SessionScope
、@ApplicationScope
(@Component
等默认是singleton
)
Bean生命周期
- 初始化和销毁方法实现方式
- 指定@Bean初始化和销毁方法属性:
initMethod
,destroyMethod
- 实现
InitializingBean
,DisposableBean
(org.springframework.beans.factory.DisposableBean) - 使用JSR250:
@PostConstruct
Bean创建完成并完成属性赋值后调用,@PreDestroy
(javax.annotation.PreDestroy) 销毁Bean前调用
- 指定@Bean初始化和销毁方法属性:
- 后置处理器
BeanPostProcessor
(org.springframework.beans.factory.config.BeanPostProcessor)- postProcessBeforeInitialization 在Bean创建完成并完成属性赋值后,且在初始化方法调用之前调用
- postProcessAfterInitialization 在初始化之后调用
@Autowired
是基于BeanPostProcessor实现的
1 | public class App { |
加载优先级
- 同一个类中加载顺序
- Constructor > @Autowired/@Value > @PostConstruct > @Bean/@Component/setApplicationContext等
- @DependsOn控制顺序
@DepondensOn("springU")
如在@PostConstuct方法中使用SpringU等工具类会报空指针。因为@PostConstuct修饰的方法在Spring容器启动时会先于该工具类的setApplicationContext()方法运行。解决方法参考下文 BeanPostProcessor- 控制 bean 之间的实例顺序,需要注意的是 bean 的初始化方法调用顺序无法保证
- BeanPostProcessor 扩展优先于其他Bean,参考ApplicationContext:SpringU
@Lazy
和@Autowired结合使用,当两个Bean发生循环依赖时,可将其中一个Bean的注入设置成懒加载- SpringBoot下可使用
@AutoConfigureAfter
、@AutoConfigureBefore
、@AutoConfigureOrder
控制自动配置类加载优先级自定义配置类
: 使用@Configuration等注解的类自动配置类
: META-INF下/spring.factories文件中定义的配置类; 此文件一般用于第三方包,也可用于主项目- SpringBoot会优先加载自定义配置类,再加载自动配置类
- 上述3个注解只有在自动配置类下才会生效;如果一个配置类是通过@Configuration扫描加载,那么上述3个注解将无效
AOP
- 说明
- Spring AOP的底层原理就是动态代理,因此局限于方法拦截
- Spring AOP默认是使用JDK动态代理,如果代理的类没有接口则会使用CGLib代理
- JDK动态代理是需要实现某个接口了,而我们类未必全部会有接口,于是CGLib代理就有了,CGLib代理其生成的动态代理对象是目标类的子类。如果是单例的我们最好使用CGLib代理,如果是多例的我们最好使用JDK代理(JDK在创建代理对象时的性能要高于CGLib代理,而生成代理对象的运行性能却比CGLib的低)
- 自调用导致
@Transactional
失效问题,参考上文事物支持
- 相关注解
@Aspect
声明一个切面@Before
、@After
、@Around
、@AfterReturning
、@AfterThrowing
定义建言(advice)@DeclareParents
引介增强
- 切点表达式
- args()、this()、target()
- @annotation() 、args()、@args()、target()、@within()、@target()、this()
- 案例
- 基于注解切面判断用户权限
maven依赖
1
2
3
4
5
6
7
8
9
10
11
12<!-- 基于xml实现aop只需要spring-context,如果基于annotation使用aop则额外需要此依赖 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.10</version>
</dependency>
<!-- springboot项目依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>编写切面(基于xml的参考源码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112// 示例一(直接使用execution指定切面)
// 声明一个切面
public class LogAspect {
// 此接口/类中方法
"execution(* cn.aezo.spring.base.annotation.aop.DemoMethodService.*(..)) || execution(* cn.aezo.spring.base.annotation.aop.DemoMethodService2.*(..))") (
// execution(* cn.aezo.spring.base.annotation.aop.*.*(..)) // 此包中方法(第一个*代替了public void;此表达式也会拦截含throws的方法)
// execution(public * cn.aezo.spring.base.annotation.aop..*.*(..)) // 此包或者子包中public类型的方法
// this:方法是在那个类中被调用的;target:目标对象是否是某种类型;within:当前执行代码是否属于某个类(静态植入)
// within(cn.aezo.spring.base.*) // 任何此包中的方法
// within(cn.aezo.spring.base..*) // 任何此包或其子包中的方法
// target(org.springframework.web.client.RestTemplate) // 任何目标对象实现了此接口的方法。**如果使用 execution(* org.springframework.web.client.RestTemplate.execute(..))是无法拦截到的,RestTemplate本身是由代理执行的**
// this(cn.aezo.spring.base.annotation.aop.DemoMethodService) // 实现了此接口中的方法
// args(java.io.Serializable) // 有且只有一个Serializable参数
// @within(org.springframework.transaction.annotation.Transactional) // 任何一个目标对象声明的类型有一个 @Transactional 注解的连接点
// @target(org.springframework.transaction.annotation.Transactional) // 目标对象中有一个 @Transactional 注解的任意连接点
// @annotation(org.springframework.transaction.annotation.Transactional) // 任何一个执行的方法有一个 @Transactional 注解的连接点
// @args(org.springframework.transaction.annotation.Transactional) // 有且仅有一个参数并且参数上类型上有@Transactional注解(注意是参数类型上有@Transactional注解,而不是方法的参数上有注解)
// bean(simpleSay) // bean名字为simpleSay中的所有方法
// bean(*Impl) // bean名字匹配*Impl的bean中的所有方法
public void before(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
System.out.println("方法规则式拦截[@Before-execution]:" + method.getName());
}
}
// 示例二(使用@Pointcut指定切面)
public class LogIntercept {
"execution(public * cn.aezo.spring.base.annotation.aop..*.*(..))") (
public void pointcut() {}
// @annotation(sysLog) 中的 sysLog 指下面函数参数变量名
"@annotation(sysLog)") (
public void pointcut2(SysLog sysLog) {}
"pointcut()") (
public void before(JoinPoint joinPoint) {
this.printLog("execution方法执行前");
}
"pointcut()") (
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs(); // 获取被切入方法参数值
this.printLog("execution方法执行前");
Object retObj = pjp.proceed();
this.printLog("execution方法执行后");
return retObj;
}
"pointCut2(sysLog)", argNames = "pjp,sysLog") (value =
public Object around2(ProceedingJoinPoint pjp, SysLog sysLog) throws Throwable {
}
"recordLog()") (
public void after() {
this.printLog("execution方法执行后");
}
// returning属性指定一个形参名,用于表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法的返回值
"pointcut()", returning = "result") (value =
public void afterReturning(JoinPoint joinPoint, Object result) {
Object[] args = joinPoint.getArgs(); // 获取被切入方法参数值
System.out.println("afterReturning...");
}
"pointcut() && @annotation(myExecption)", throwing = "ex") (value =
public void afterThrowing(JoinPoint joinPoint, MyException myExecption, Exception ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("afterThrowing...");
}
}
// 示例三
public class OptimisticCheckAspect {
"sq.tableName:ds_test") (
private String tableName;
private List<String> tableList;
public JdbcTemplateAspect() {
tableList = new ArrayList<>();
if(tableName != null) {
tableList.addAll(Arrays.asList(tableName.split(",")));
}
}
// 检查通过 JdbcTemplate 执行的sql语句是否携带session字段
"execution(* org.springframework.jdbc.core.JdbcTemplate.update(..)) " + (
" || execution(* org.springframework.jdbc.core.JdbcTemplate.batchUpdate(..))")
public void before(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
if(args != null && args.length > 0 && args[0] instanceof String) {
String sql = ((String) args[0]).trim();
if(sql.toLowerCase().startsWith("insert") || sql.toLowerCase().startsWith("update")) {
if(!sql.matches("(.*)\\s+((?i)version)\\s*=(.*)")) {
throw new RuntimeException("修改此实体需要基于乐观锁实现");
}
}
}
}
}调用service
Profile
- 不同的环境读取不同的配置文件:
dev
/prod
ApplicationEvent
- 事件:一个Bean(A)完成某个任务后,可以给另外一个Bean(B)发送事件,前提是B对A进行了监听
方法:
继承
ApplicationEvent
进行事件定义1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class DemoEvent extends ApplicationEvent {
private String message;
public DemoEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}实现
ApplicationListener
进行事件监听1
2
3
4
5
6
7
8
public class DemoListener implements ApplicationListener<DemoEvent> {
public void onApplicationEvent(DemoEvent demoEvent) {
String message = demoEvent.getMessage();
System.out.println("DemoListener.onApplicationEvent==" + message);
}
}applicationContext.publishEvent(new DemoEvent(this, message));
发布事件1
2
3
4
5
6
7
8
9
public class DemoPublisher {
ApplicationContext applicationContext;
public void publish(String message) {
applicationContext.publishEvent(new DemoEvent(this, message));
}
}
SpringAware
- Spring依赖注入最大的亮点就是你所有的Bean对Spring容器的存在是无意识的。即你可以将容器换成其他容器,如Google Guice,这是Bean之间的耦合度很低。
- Spring Aware可以让你的Bean调用Spring提供的资源,缺点是Bean会和Spring框架耦合。
- 相关接口
BeanNameAware
获得容器中Bean的名称BeanFactoryAware
获得当前BeanFactory,这样就有可以调用容器服务ApplicationContextAware
获得当前ApplicationContext,这样就有可以调用容器服务MessageSourceAware
获得当前MessageSource,可以获得文本信息ApplicationEventPublisherAware
应用事件发布器,可以发布事件ResourceLoaderAware
获得资源加载器,可以获取外部资源
- 实例
1 |
|
ApplicationContext
1 |
|
多线程@EnableAsync
- Spring通过任务执行器(TaskExecutor)来实现多线程和并发编程。使用
ThreadPoolTaskExecutor
可实现一个基于线程池的TaskExecutor。 @EnableAsync
可开启对异步任务的支持。需要对应的配置类实现@Async
注解执行异步任务的方法示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34// 获取线程池
"cn.aezo.spring.base.annotation.thread") (
// 开启异步任务支持
public class TaskExecutorConfig implements AsyncConfigurer {
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(5);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setQueueCapacity(25);
taskExecutor.initialize();
return taskExecutor;
}
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}
// 定义异步方法
public class AsyncTaskService {
public void executeAsyncTask(Integer i) {
System.out.println("i = " + i);
}
public void executeAsyncTaskPlus(Integer i) {
System.out.println("i+1 = " + (i+1));
}
}
计划任务@Scheduled
@EnableScheduling
开启定时任务@Scheduled
执行任务的方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"cn.aezo.spring.base.annotation.scheduled") // springboot无需扫描@Scheduled所在包 (
public class TaskScheduledConfig {
// 默认同一时刻只会运行一个@Scheduled修饰的方法,时间太长会阻塞其他定时
// 此时定义成5个线程并发(**被@Scheduled修饰的不同方法可以并发执行,同一个方法不会产生并发**)
public Executor taskScheduler() { // java.util.concurrent.Executor
return Executors.newScheduledThreadPool(5);
}
}
public class ScheduledTaskService {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
// 方法访问权限必须为protected或以下
5000) // 5000毫秒. fixedRate每隔固定时间执行 (fixedRate =
public void reportCurrentTime() {
System.out.println("每隔5秒执行一次:" + dateFormat.format(new Date()));
}
"0 50 14 ? * *") // 每天14.50执行。程序启动并不会立即运行,比如 14:45 启动,只有等到 14:50 才会第一次运行 (cron =
// @Scheduled(cron = "${myVal.cron}") // springboot只需再yml里面定义配置即可,无需创建JavaBean配置
public void fixTimeException() {
System.out.println("在指定时间执行:" + dateFormat.format(new Date()));
}
}cron配置说明 ^1
{秒} {分} {时} {日} {月} {周} {年(可选)}
- Seconds/Minutes 有效范围为0-59的整数
- Hours 有效范围为0-23的整数
- 符号说明
*
代表所有值,每隔1秒/分/时触发?
表示不指定值- 如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为”?”,具体设置为
0 0 0 10 * ?
- 如:每周一执行,此时一般不关心”日”,案例如
0 0 9 ? * 2
- 如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为”?”,具体设置为
/
代表触发步进(step)- “/“前面的值代表初始值(“*“等同”0”),后面的值代表偏移量。比如”0/25”或者”*/25”代表从0秒/分/时开始,每隔25秒/分/时触发1次
,
代表在指定的秒/分/时触发- 比如”10,20,40”代表10秒/分/时、20秒/分/时和40秒/分/时时触发任务
-
代表在指定的范围内触发- 比如”5-30”代表从5秒/分/时开始触发到30秒/分/时结束触 发,每隔1秒/分/时触发
- cron在线生成
常用定时配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26"0/10 * * * * ?" 每10秒触发(程序启动后/任务添加后,第一次触发为0/10/20/30/40/50秒中离当前时间最近的时刻。下同)
"0 0/5 * * * ?" **每5分钟执行一次(注意第一个为0)**
"* 0/5 * * * ?" 每5分钟连续执行60秒,这60秒期间每秒执行一次
"0 5 * * * ?" 表示每个小时的第5分钟执行
"0 0 12 * * ?" 每天中午12点触发
"0 0 0/12 * * ?" 每12个小时触发一次
"0 15 10 ? * *" 每天上午10:15触发
"0 15 10 * * ?" 每天上午10:15触发
"0 15 10 * * ? *" 每天上午10:15触发
"0 0 10,14,16 * * ?" 每天上午10点,下午2点,4点
"0 0/30 9-17 * * ?" 朝九晚五工作时间内每半小时
"0 0 9 ? * 2" 表示每个星期一上午9点(1表示这周的第一天,即周日;2则表示周一)
"0 0 9 ? * MON"
"0 15 10 * * ? 2005" 2005年的每天上午10:15触发
"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发
"0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发
"0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发
"0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发
"0 15 10 15 * ?" 每月15日上午10:15触发
"0 15 10 L * ?" 每月最后一日的上午10:15触发
"0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发(6代表一周中的第6天,即周五)
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发
手动启动任务和停止任务(基于springboot v2.0.1测试)
1 | // ## Job接口 |
事物支持
事物
- 在Spring中,事务有两种实现方式
- 编程式事务管理:使用TransactionTemplate可实现更细粒度的事务控制
- 申明式事务管理:基于Spring AOP实现。常用
- Spring事务管理是基于接口代理或动态字节码技术,通过AOP实施事务增强的
- 事物生命周期是从AOP调用的目标方法开始的,到该方法执行完成事物环境即消失
- 不一定非要有接口实现,普通Bean只要通过AOP调用即可
@Transactional
注解只能被应用到 public 可见度的方法上或注解到类上,注解到类上则该类的所有public方法再进行AOP调用时都存在事物- 默认遇到运行期异常(RuntimeException)会回滚,遇到捕获异常(Exception)时不回滚
@Transactional(rollbackFor=Exception.class)
指定回滚,遇到(声明上throws出来的)捕获异常Exception时也回滚@Transactional(noRollbackFor=RuntimeException.class)
指定不回滚
- 一个带事物的方法调用了另外一个事物方法,第二个方法的事物默认无效(Propagation.REQUIRED),具体见下文事物传播行为
- 如果事物比较复杂,如当涉及到多个数据源,可使用
@Transactional(value="transactionManagerPrimary")
定义个事物管理器transactionManagerPrimary
- 事物生命周期是从AOP调用的目标方法开始的,到该方法执行完成事物环境即消失
- 常见问题
- 自调用导致
@Transactional
失效问题- 同一个类中的方法相互调用,发起方法无
@Transactional
,则被调用的方法@Transactional
无效 ^3 - 原因:由于@Transactional的实现原理是AOP,AOP的实现原理是动态代理,自调用时不存在代理对象的调用,这时不会产生注解@Transactional配置的参数,因此无效
- 通过
SpringU.getBean(UserService.class);
解决(不需要设置exposeProxy属性) - 通过AopContext解决
- 同一个类中的方法相互调用,发起方法无
- 捕获嵌套事物异常导致报错
Transaction rolled back because it has been marked as rollback-only
(事务已经被标记为回滚,无法提交) - 服务内部捕获异常Exception/RuntimeException,统一返回错误结果对象,如自定义
Result
,此时无法回滚事物,解决方案如下TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
程序内部手动回滚(手动回滚必须当前执行环境有Transactional配置,而不是执行此语句的方法有@Transactional
注解就可以回滚,具体见下文示例)。Debug过程中,发现有问题,可通过执行此语句进行手动回滚。调试时很好用- 或者手动抛出RuntimeException
- 或者基于自定义注解统一回滚
- 自调用导致
- 手动回滚方式(前提是当前有事物)
1 | // 回滚整个方法 |
- 完全手动管理事物
1 |
|
隔离级别
- 隔离级别
@Transactional(isolation = Isolation.DEFAULT)
:org.springframework.transaction.annotation.Isolation
枚举类中定义了五个表示隔离级别的值。脏读取、重复读、幻读 ^2DEFAULT
- 这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是
READ_COMMITTED
;然而mysql的默认值是REPEATABLE_READ
- 这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是
READ_UNCOMMITTED
- 该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据
- 读操作不加S锁
- 该级别不能防止脏读、不可重复读、幻读。因此很少使用该隔离级别
- 比如,事务1修改一行,事务2在事务1提交之前读取了这一行。如果事务1回滚,事务2就读取了一行没有提交的数据(读取数据不需要加S锁,这样就不会跟被修改的数据上的X锁冲突)
READ_COMMITTED
- 该隔离级别表示一个事务只能读取另一个事务已经提交的数据
- 读操作需要加S锁,但是在语句执行完以后释放S锁
- 该级别可以防止脏读,可能会出现不可重复读、幻读。这也是大多数情况下的推荐值
- Sql Server、Oracle默认为此级别
- 比如,事务1读取了一行,事务2修改或者删除这一行并且提交。如果事务1想再一次读取这一行,它将获得修改后的数据或者发现这一样已经被删除,因此事务的第二次读取结果与第一次读取结果不同,因此也叫不可重复读
REPEATABLE_READ
- 该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略
- 读操作需要加S锁,语句执行完并不会释放S锁,必须等待事务执行完毕以后才释放S锁
- 该级别可以防止脏读、不可重复读,可能出现幻读
- MySQL默认为此级别
- 比如,事务1读取了一行,事务2想修改或者删除这一行并且提交,但是因为事务1尚未提交,数据行中有事务1的锁,事务2无法进行更新操作,因此事务2阻塞。如果这时候事务1想再一次读取这一行,它读取结果与第一次读取结果相同,因此叫可重复读
SERIALIZABLE
- 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰
- 会在Repeatable Read级别的基础上,添加一个范围锁
- 该级别可以防止脏读、不可重复读、幻读。但是这将严重影响程序的性能,通常情况下也不会用到该级别
- 幻读
- 一般出现在事务不是独立执行时发生,如使用REQUIRES_NEW时容易出现(业务逻辑导致)
- 如:事务A首先根据条件索引得到10条数据,然后事务B改变了数据库一条数据,导致也符合事务A当时的搜索条件,这样事务A再次搜索发现有9条(B删除了一条)或11条数据(B新增了一条),就产生了幻读
- 数据真正入库
- 入库事务有spring事务,数据库事务,只有当这两个事务都结束,才代表数据真正可查
传播行为
- 传播行为
@Transactional(propagation = Propagation.REQUIRED)
:所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。org.springframework.transaction.annotation.Propagation
枚举类中定义了6个表示传播行为的枚举值REQUIRED
:这是默认值,如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务REQUIRES_NEW
:创建一个新的事务,如果当前存在事务,则把当前事务挂起SUPPORTS
:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行NOT_SUPPORTED
:以非事务方式运行,如果当前存在事务,则把当前事务挂起MANDATORY
:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常NEVER
:以非事务方式运行,如果当前存在事务,则抛出异常NESTED
:(适合总分总结构) 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED
REQUIRES_NEW 和 NESTED的区别
REQUIRES_NEW
执行到B时,A事物被挂起,B会新开了一个事务进行执行。B发生异常后,B中的修改都会回滚,然后外部事物继续执行;B正常执行提交后,则数据已经持久化了,可能产生脏读,且A如果之后失败回滚时,B是不会回滚的NESTED
执行到B时,会创建一个savePoint,如果B中执行失败,会将数据回滚到这个savePoint,A可以继续提交;如果B正常执行,此时B中的修改并不会立即提交,而是在A提交时一并提交,如果A失败,则A和B都会回滚(适合总分总结构)示例(https://www.jianshu.com/p/339d59f1ecd9)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/*
NESTED
(1) B、C为A的子事务,可以读取A未提交的数据。但是REQUIRES_NEW却不行,除非B、C的隔离级别是Read Uncommitted
(2) 如果A事务在B/C执行完后,还有更改数据库的操作,如果更改失败,那么B/C是要回滚的,但是REQUIRES_NEW则B/C不会回滚,B/C事务已提交
(3) A与B/C方法中,可以修改同一条数据。但是对于REQUIRES_NEW会造成死锁
REQUIRES_NEW
(1) B/C作为内部事务,提交后可以被修改,这会造成A的脏读(A读取了金额为100, 然后java代码中+10, 之后B把金额改成0并提交, A最后把计算的金额110进行保存, 从而脏读. 按照顺序此时A应该重新读取B提交的数据则为0, 再进行加10操作)
*/
A.service() {
insert();
try {
// PROPAGATION_NESTED
B.service();
} catch(Exception e) {
// PROPAGATION_NESTED
C.service();
}
update();
}
事物示例
1 | // 1.## 在Test测试程序中,通过此Controller相关Bean调用该方法时,正常回滚 |
自调用导致@Transactional失效问题
1 | // 假设UserController调用UserService |
捕获嵌套事物异常导致报错
1 | Class ServiceA { |
- 解决方案如下
- 业务允许情况下减少嵌套事物出现,如去掉某一个方法中的@Transactional
- 如果希望内层事务抛出异常时中断程序执行,直接在外层事务的catch代码块中抛出e(这样整个事物也不会提交,即a中的不会保存)
- 在catch语句中增加
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
语句,手动回滚。此时a中是否能保存成功?
- 在catch语句中增加
- 将嵌套事物开启成新事物,如editById注解成@Transactional(propagation = Propagation.REQUIRES_NEW)
- 如果希望内层事务回滚,但不影响外层事务提交,需要将内层事务的传播方式指定为PROPAGATION_NESTED
- 注:PROPAGATION_NESTED基于数据库savepoint实现的嵌套事务,外层事务的提交和回滚能够控制嵌内层事务,而内层事务报错时,可以返回原始savepoint,外层事务可以继续提交
在事物中提前关闭了连接
1 | (rollbackFor = Exception.class) |
Spring-XML配置
1 | "1.0" encoding="UTF-8" xml version= |
SPEL
- 参考属性赋值(@Value))
自定义注解结合EL表达式
- 自定义简单的SPEL解析工具类,参考
@Value
- 参考项目
report-table-backend
下的@HasPermission
SpringMVC
- 关键类:
DispatcherServlet#doDispatch
- SpringMVC 的整个请求流程
- 前端控制器 - 处理映射器 - 处理适配器 - 处理器 - 视图解析器 - 视图渲染
映射处理器HandlerMapping
- 映射处理器:就是实现了HandlerMapping接口,处理url到bean的映射
- 常见的
RequestMappingHandlerMapping
处理@RequestMappingBeanNameUrlHandlerMapping
将bean的name作为url进行查找,需要在配置Handler时指定bean name,且必须以 / 开头SimpleUrlHandlerMapping
可以通过内部参数去配置请求的 url 和 handler 之间的映射关系。springboot中使用此类进行映射的地方(调用其setUrlMap进行注入)- ResourceHandlerRegistry
- ViewControllerRegistry
- WebMvcAutoConfiguration.FaviconConfiguration
- DefaultServletHandlerConfigurer 默认没有设置handle,可基于WebMvcConfigurer实现配置(仅使用默认DefaultServletHandlerConfigurer,无法注入自定义的Interceptor,可自定义默认ServletHandler解决)
- 自定义HandlerMapping ^4
1 | 4j |
拦截器
Filter 与 Interceptor 区别
- Filter作用在 DispatcherServlet 调用前,Interceptor作用在调用后
- Filter 由 Servlet 标准定义,要求 Filter 需要在 Servlet 被调用之前调用,作用顾名思义,就是用来过滤请求。在 Spring Web 应用中,DispatcherServlet 就是唯一默认的 Servlet 实现
Interceptor 由 Spring 自己定义,由 DispatcherServlet 调用,可以定义在 Handler 调用前后的行为。这里的 Handler ,在多数情况下,就是我们的 Controller 中对应的方法
- 参考 DispatcherServlet#doDispatch -> mappedHandler.applyPreHandle -> interceptor.preHandle(只有URL匹配到了对应的Handler,才会调用preHandle方法)
- 默认
/**
路径会被 SimpleUrlHandlerMapping 拦截(静态资源映射使用的类),因此如果重写了spring.mvc.static-path-pattern
则可能有些路径找不到对应Handler,从而不执行preHandle
- 实现方式
- 实现 Filter 或继承 OncePerRequestFilter,并增加注解@Component
- 往 FilterRegistrationBean 中注册Filter,可指定拦截某路径。可以创建多个FilterRegistrationBean对象,优先级按照Order属性值来(小的优先)
- 实现 WebMvcConfigurer,并加入自定义的HandlerInterceptor,可指定拦截路径和设定Order顺序
基于Filter进行拦截
1 | // 暴露即可注入到拦截链中(如果要指定拦截路径,需要手动判断) |
基于HandlerInterceptor/WebRequestInterceptor进行拦截
1 | // 定义拦截器(且需如下文注入到 WebMvcConfigurer) |
拦截request的body数据
- 通过request的body数据(request.getParameter无法获取body)只能通过InputStream获取,而且只能获取一次
- 常见问题:可能出现自定义Filter中使用了body,导致Controller中无法再使用@RequestBody获取数据
request.setAttribute("body", body);
灵活度不高,会影响其他Filter- 如果需要多次获取可以使用HttpServletRequestWrapper进行缓存
application/x-www-form-urlencoded
请求类型时,数据会以a=1&b=2的形式保存在body里面。当执行request.getParameterMap()时,会将数据从body中取出进行缓存
1 | public class CustomerHttpServletRequestWrapper extends HttpServletRequestWrapper { |
拦截response的数据
1 |
|
WebMvcConfigurer
1 |
|
异常处理
- 处理方式,参考:https://www.freesion.com/article/86641357119/
- @Controller + @ExceptionHandler
- @ControllerAdvice + @ExceptionHandler
- 实现HandlerExceptionResolver接口
Spring相关类/接口说明
org.springframework.context
- annotation
@Import
导入Bean,具体见上文@ImportResource
ImportSelector
DeferredImportSelector
ImportBeanDefinitionRegistrar
org.springframework.boot
- context.properties
@ConfigurationProperties
将properties配置文件中的属性对应到类上,需要和@EnableConfigurationProperties或@Component等结合使用@EnableConfigurationProperties
使使用@ConfigurationProperties注解的类生效,可在任何配置类上定义- 如果一个配置类只配置@ConfigurationProperties注解,而没有使用@Component,那么在IOC容器中是获取不到properties配置文件转化的bean(但是可以通过@Value直接获取properties的值)。说白了@EnableConfigurationProperties相当于把使用@ConfigurationProperties的类进行了一次注入。如:@EnableConfigurationProperties({FeignClientEncodingProperties.class})
- autoconfigure
@AutoConfigureBefore
@AutoConfigureAfter
在加载某配置的类之后再加载当前类。如:@AutoConfigureAfter({FeignAutoConfiguration.class})
org.springframework.util
AntPathMatcher
路径通配符匹配示例
1 | // AntPathMatcher |
参考文章