-
-
个人笔记,如有描述不当,欢迎留言指出~
spring 日记
Bean
@Scope(作用域注解)
关键属性:
scopeName
:作用域名,”prototype”、”singleton”、”request”、”session”proxyMode
:代理模式,ScopedProxyMode. DEFAULT(默认为NO)、ScopedProxyMode.NO(不代理)、ScopedProxyMode.INTERFACES(基于jdk接口代理)、ScopedProxyMode.TARGET_CLASS(基于cglib代理)
用法:1
2
3
"xx",proxyMode=xxx) (scopeName=
public class XXX{}
或者1
2
3
4
5
6
7
8
public class XXXConfig{
"xx",proxyMode=xxx) (scopeName=
public Xxx getXxx(){
return new Xxx();
}
}
NOTE:
若scopeName=”prototype”,proxyMode=ScopedProxyMode.NO,那每次都会得到一个新的bean;
若scopeName=”prototype”,proxyMode=ScopedProxyMode.INTERFACES或TARGET_CLASS, 那么会被注入一个代理类(它是单例并非原型),代理类里根据scopeName来返回具体的bean。
缓存
cacheManager
cacheManager(缓存管理器)可以注册多个,但是id必须不同,另外必须指定其中一个cacheManager上加上@Primary
,否则CacheAspectSupport
(cache切面类)中获取cacheManager bean时返回多个会报错。
也可以不指定@Primary
,但要注册一个cache配置类继承CachingConfigurerSupport
,并覆盖其中cacheManager()方法,返回一个已注册的cacheManager
@Cacheable
中可以指定cacheManager, 故可以实现不同的缓存方式
EhcacheCacheManage
、JcacheCacheManager
、RedisCacheManager
三者都继承了 AbstractTransactionSupportingCacheManager
, 通过设置setTransactionAware(boolean transactionAware)方法,可以实现事务提交后再进行缓存操作(注意,不管事务最后成功与否,缓存都会执行,慎用!!)
redisTemplate
redisTemplate
(redis缓存模板)中 setEnableTransactionSupport(boolean enableTransactionSupport)设置开启事务支持,结合@Transactional
可以实现缓存回滚
other
若一个方法上同时存在@Cacheable
和@Transaction
,spring默认cache代理优先级高于transaction,所以会出现先进行cache操作再进行transaction操作的情况。可以通过设置@EnableCaching(order=xxx)
、@EnableTransactionManagement(order=xxx)
中order值来调节代理顺序,order越小优先级越高。
事务
@TransactionalEventListener
事务事件监听器,一般用于事务提交成功时处理一些业务逻辑
关键属性phase
:
- TransactionPhase.BEFORE_COMMIT 事务提交前触发
- TransactionPhase.AFTER_COMMIT 事务提交成功时触发
- TransactionPhase.AFTER_ROLLBACK 事务回滚时触发
- TransactionPhase.AFTER_COMPLETION 事务完成(事务提交成功后或回滚后触发)
用法:
1 | public class MyAfterTransactionEvent extends ApplicationEvent { |
NOTE: spring事件机制默认是同步的,使用 @TransactionalEventListener不过是异步调用,本质上监听方法的执行和事务是在同一线程中。而上面例子中我们的监听方法在事务提交成功时执行,千万不要在监听方法进行insert/update/delete操作,因为spring的事务是绑定线程的,事务虽然提交了,但仍和当前线程绑定,此时进行增删改操作都是无效的!!详见这篇外文
怎么解决这个问题?目前能想到2种
- 1:在监听方法里调用异步方法来避免
- 2:在监听方法上添加@Transactional(propagation = Propagation.REQUIRES_NEW),这样监听方法会创建新的事务
有的人可能会想在监听方法上加@Async,这样监听方法在子线程种执行,子线程不和主线程共享事务,从而解决上述问题。我只能说想法很美好,现实很骨感。前面说了spring的事务是绑定线程的, @TransactionalEventListener是监听当前线程的事务,而子线程中丢失了主线程的任务,结果就是你的监听器不起效
security
我们知道SecurityContextHolder 持有security的contextSecurityContextHolder
部分源码: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
42public class SecurityContextHolder {
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL"; //线程副本策略
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";//可继承线程副本策略
public static final String MODE_GLOBAL = "MODE_GLOBAL"; //全局策略
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
static {
initialize();
}
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
// Set default 如果系统变量读不到,则默认为线程副本策略
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
}
else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
}
else if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
}
else {
// Try to load a custom strategy 如果都不匹配,那么加载自定义策略,strategyName为自定义策略的类路径,自定义策略需实现SecurityContextHolderStrategy接口
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
initializeCount++;
}
...
从源码里可以看出,SecurityContextHolder 默认为线程副本策略,这就会导致异步线程中获取不到security的context,解决方法有4种:
- 1:配置文件中设置
spring.security.strategy=MODE_INHERITABLETHREADLOCAL
- 2:使用
DelegatingSecurityContextRunnable.create(Runnable delegate, SecurityContext securityContext)
装饰任务,或者使用DelegatingSecurityContextExecutor
创建excutor(线程执行器) 3:和2的方法很像 ,excutor中设置任务装饰器
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/**
* security 异步任务装饰器
*/
static class ContextCopyingDecorator implements TaskDecorator {
public Runnable decorate(@NonNull Runnable runnable) {
RequestAttributes context = RequestContextHolder.currentRequestAttributes();
SecurityContext securityContext = SecurityContextHolder.getContext();
return () -> {
try {
RequestContextHolder.setRequestAttributes(context);
SecurityContextHolder.setContext(securityContext);
runnable.run();
} finally {
SecurityContextHolder.clearContext();
RequestContextHolder.resetRequestAttributes();
}
};
}
}
public Executor createExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(new ContextCopyingDecorator());
...
}4:使用spring提供的
MethodInvokingFactoryBean
修改SecurityContextHolder
策略1
2
3
4
5
6
7
8
public MethodInvokingFactoryBean setSecurityStrategy() {
MethodInvokingFactoryBean factoryBean = new MethodInvokingFactoryBean();
factoryBean.setTargetClass(SecurityContextHolder.class);
factoryBean.setStaticMethod("setStrategyName");
factoryBean.setArguments(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
return factoryBean;
}
扩展: 从SecurityContextHolder部分源码中不难看出,SecurityContextHolder在初始化时匹配3种策略名从而生成对应策略,若都没有匹配上,则加载自定义策略,此时strategyName
(策略名)为自定义策略的类路径,自定义策略需实现SecurityContextHolderStrategy
接口
自定义用户认证
代码实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 //从spring容器中获取UserDetailsService(这个从数据库根据用户名查询用户信息,及加载权限的service)
UserDetailsService userDetailsService =
(UserDetailsService)SpringContextUtil.getBean("userDetailsService");
//根据用户名username加载userDetails
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//根据userDetails构建新的Authentication,这里使用了
//PreAuthenticatedAuthenticationToken当然可以用其他token,如UsernamePasswordAuthenticationToken
PreAuthenticatedAuthenticationToken authentication =
new PreAuthenticatedAuthenticationToken(userDetails, userDetails.getPassword(),userDetails.getAuthorities());
//设置authentication中details
authentication.setDetails(new WebAuthenticationDetails(request));
//存放authentication到SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authentication);
HttpSession session = request.getSession(true);
//在session中存放security context,方便同一个session中控制用户的其他操作
session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());