spring日记

-


-

个人笔记,如有描述不当,欢迎留言指出~

spring 日记

Bean

@Scope(作用域注解)

关键属性:

  • scopeName:作用域名,”prototype”、”singleton”、”request”、”session”
  • proxyMode:代理模式,ScopedProxyMode. DEFAULT(默认为NO)、ScopedProxyMode.NO(不代理)、ScopedProxyMode.INTERFACES(基于jdk接口代理)、ScopedProxyMode.TARGET_CLASS(基于cglib代理)

用法:

1
2
3
@Component
@Scope(scopeName="xx",proxyMode=xxx)
public class XXX{}

或者

1
2
3
4
5
6
7
8
@Configuration
public class XXXConfig{
@Bean
@Scope(scopeName="xx",proxyMode=xxx)
public Xxx getXxx(){
return new Xxx();
}
}

NOTE:
若scopeName=”prototype”,proxyMode=ScopedProxyMode.NO,那每次都会得到一个新的bean;
若scopeName=”prototype”,proxyMode=ScopedProxyMode.INTERFACES或TARGET_CLASS, 那么会被注入一个代理类(它是单例并非原型),代理类里根据scopeName来返回具体的bean。
Alt text

缓存

cacheManager

cacheManager(缓存管理器)可以注册多个,但是id必须不同,另外必须指定其中一个cacheManager上加上@Primary,否则CacheAspectSupport(cache切面类)中获取cacheManager bean时返回多个会报错。

Alt text

也可以不指定@Primary,但要注册一个cache配置类继承CachingConfigurerSupport,并覆盖其中cacheManager()方法,返回一个已注册的cacheManager
Alt text

@Cacheable中可以指定cacheManager, 故可以实现不同的缓存方式

EhcacheCacheManageJcacheCacheManagerRedisCacheManager三者都继承了 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
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
public class MyAfterTransactionEvent extends ApplicationEvent {
//自定义一些属性
//...

public MyAfterTransactionEvent(Object... source) {
super(source);
}
}

@Slf4j
@Component
public class MyTransactionListener {
//注入一些bean
//...

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onHelloEvent(/** 自定义事件**/MyAfterTransactionEvent event) {
//提交后的业务逻辑
...
}
/**
* 作用同上
**/
@EventListener
void onSaveUserEvent(MyAfterTransactionEvent event) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
//提交后的业务逻辑
//...
}
});
}
}

@Service
public class HelloServiceImpl{
@Autowired
private ApplicationEventPublisher publisher;

@Transactional
void test(){
//db操作
//...
publisher.publishEvent(new MyAfterTransactionEvent());
}

}

NOTE: spring事件机制默认是同步的,使用 @TransactionalEventListener不过是异步调用,本质上监听方法的执行和事务是在同一线程中。而上面例子中我们的监听方法在事务提交成功时执行,千万不要在监听方法进行insert/update/delete操作,因为spring的事务是绑定线程的,事务虽然提交了,但仍和当前线程绑定,此时进行增删改操作都是无效的!!详见这篇外文
怎么解决这个问题?目前能想到2种

  • 1:在监听方法里调用异步方法来避免
  • 2:在监听方法上添加@Transactional(propagation = Propagation.REQUIRES_NEW),这样监听方法会创建新的事务

有的人可能会想在监听方法上加@Async,这样监听方法在子线程种执行,子线程不和主线程共享事务,从而解决上述问题。我只能说想法很美好,现实很骨感。前面说了spring的事务是绑定线程的, @TransactionalEventListener是监听当前线程的事务,而子线程中丢失了主线程的任务,结果就是你的监听器不起效

security

我们知道SecurityContextHolder 持有security的context
SecurityContextHolder部分源码:

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
public 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 {
    @NonNull
    @Override
    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();
    }
    };
    }
    }

    @Bean
    public Executor createExecutor(){
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setTaskDecorator(new ContextCopyingDecorator());
    ...
    }
  • 4:使用spring提供的MethodInvokingFactoryBean修改SecurityContextHolder策略

    1
    2
    3
    4
    5
    6
    7
    8
         @Bean
    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());

请博主喝咖啡