前阵子部门要求我们的项目接入新框架,使用Spring Boot 框架。所以我们将我们两个web项目全部改造成了Spring Boot.
注:以下项目均为前后端分离的项目
改造前框架:
框架:Spring MVC+Spring+Mybatis Plus,Mysql+Druid+Redis(客户端Jedis)
版本:Spring 4.2.3-RELEASE,Mybatis Plus 3.2
改造后框架
框架:Spring Boot+Mybatis Plus,Mysql+Druid+Redis(客户端Jedis/Lettuce)
版本:Spring Boot 2.0.9,Mybatis Plus 3.3
后台项目
这个后台(CONSOLE)项目整体架构没有面向客户的WEB项目复杂,功能也比较单一,但是作为后台项目必然会涉及接管多个项目的后台,所以这个项目的改造难点在于Redis的配置。这个项目基本框架都是Copy的WEB项目,所以先改造后台项目可以给WEB提供一下踩坑经验。
这个项目原本的配置已经全部使用Java Config完成,所以在改造起来也是省了一大笔功夫。项目存在的配置项:继承WebApplicationInitializer
充当Web.xml作用的初始化配置WebInitializer
类,继承ConfiguarationSupport
的WebConfig
,项目的个性化配置的AppConfig
,以及RedisConfig
,CacheConfig
,DataSourceConfig
这些数据库缓存相关的配置。
步骤:
- 依赖调整;
WebInitializer
实现类替代,转yml、新配置或者注解实现;- 数据库,redis改造
- 启动方式改造
- 国际化
改造进程
一、依赖调整
去除所有的Spring 4.X的依赖,引入需要接入新框架的包,内含Spring Boot的全部需要的包(如果不清楚需要哪些包可以去Spring.io进行生成一个简单demo)
二、WebInitializer
替代
servletContext.setInitParameter(ContextLoader.CONTEXT_CLASS_PARAM,
AnnotationConfigWebApplicationContext.class.getName());
servletContext.setInitParameter(ContextLoader.CONFIG_LOCATION_PARAM,
configClassList(AppConfig.class, CacheConfig.class, DataBaseConfig.class,
RedisConfig.class, RedisSessionConfig.class));
首先在以前web.xml中常用的<context-param>
此类,在Java中常用的servletContext.setInitParameter
已经不再需要,大胆删掉这些配置。
servletContext.getServletRegistration("jsp")
.addMapping("*.html");
这句话的意思是将使用jsp的Servlet来处理html结尾的文件,这么做的目的就是在html的文件中你也可以使用jsp的语法标签,因为可以被正常识别处理。那么这一步我的处理是直接将原本的首页index.html
改成index.jsp
。
AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
webContext.register(WebConfig.class);
ServletRegistration.Dynamic servlet = servletContext.addServlet("springmvc", new DispatcherServlet(webContext));
servlet.setLoadOnStartup(1);
servlet.addMapping("/");
接下来就是DispatcherServlet
的注册,在Spring Boot中完全不必要配置了,因为他已经自动装配了一个名为dispatcherServlet
的bean,参见DispatcherServletAutoConfiguration
。
FilterRegistration.Dynamic encodingFilter = servletContext.addFilter("encodingFilter",
CharacterEncodingFilter.class);
encodingFilter.setInitParameter("encoding", String.valueOf(StandardCharsets.UTF_8));
encodingFilter.setInitParameter("forceEncoding", "true");
encodingFilter.addMappingForUrlPatterns(null, false, "/*");
servletContext.addFilter("customFilter", CustomFilter.class)
.addMappingForUrlPatterns(null, false, "/*");
servletContext.addFilter("springSessionRepositoryFilter", DelegatingFilterProxy.class)
.addMappingForUrlPatterns(null, false, "/*");
}
过滤器配置稍微有些不同,Spring Boot中采用的Bean注册方式注册Filter,首先我们观察看看除了自定义的哪些Spring Boot已经帮我们做好了呢?CharacterEncodingFilter
看看这个类是不是有被*AutoConfiguration
的类调用呢,哎呀,还真有,HttpEncodingAutoConfiguration
调用了,看了一下类的内容还帮我们注册了这样的bean,不过用了@ConditionalOnMissingBean
,所以你要是自己注册一个也没啥问题,这个自动注入的会失效。
再看看springSessionRepositoryFilter
,这个过滤器我们用来将session存进Redis,那么既然Spring Boot那么智能应该也有办法解决,只要用上spring:session:store-type
的yml配置加上@EnableRedisHttpSession
这个注解可以帮到我们,所以也不需要这个过滤器配置了。
我们再看看自定义的过滤怎么声明呢?如下示例
@Bean
public FilterRegistrationBean requestReplaceFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setOrder(2);
filterRegistrationBean.setFilter(new CustomFilter());
filterRegistrationBean.setName("customFilter");
filterRegistrationBean.setMatchAfter(false);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
做完这些之后看看你们是不是还有webapp这个目录呢,直接可以删掉了~
webConfig,AppConfig基本不受影响。
三、数据库,Redis改造
既然使用Spring Boot那也应该用上yml进行自动装配。所以我们考虑这块数据库能不能使用yml进行配置。
那么我们需要知道一个点,将与环境相关的yml与不想关的配置进行隔离区分,所以我们又定义了一个yml文件名为application-env.yml
,在application.yml
添加如下配置进行引入
spring:
profiles:
include: env
首先清理DataBaseConfig
中配置的Druid连接池的代码配置,转成yml配置放置于application-env.yml
。将Mybatis的配置放置在application.yml
。
重头戏来了,Redis配置。这里遇到一个相当大的问题,那就是后台项目Redis需要对接两个库,0号库和1号库,因为不同的项目使用不一样的库。
public class CacheConfig{
@Value("${redis.pwd}")
private String pwd;
@Value("${redis.connection.timeout}")
private Integer timeOut;
@Value("${redis.usePool}")
private Boolean usePool;
@Bean(name = "redisTemplate")
public RedisTemplate redisTemplate(@Qualifier(value = "jedisConnectionFactory") JedisConnectionFactory jedisConnectionFactory) {
return newRedisTemplate(jedisConnectionFactory);
}
@Bean(name = "redisTemplate1")
public RedisTemplate redisTemplate1(@Qualifier(value = "jedisConnectionFactory1") JedisConnectionFactory jedisConnectionFactory) {
return newRedisTemplate(jedisConnectionFactory);
}
private RedisTemplate newRedisTemplate(JedisConnectionFactory jedisConnectionFactory) {
final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(jedisConnectionFactory);
//方便查看key
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
@Primary
@Bean(name = "redisCacheManager")
public CacheManager redisCacheManager(@Qualifier(value = "redisTemplate") RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
// Sets the default expire time (in seconds)
cacheManager.setDefaultExpiration(3000);
cacheManager.setCachePrefix(new DefaultRedisCachePrefix());
cacheManager.setUsePrefix(true);
return cacheManager;
}
@Primary
@Bean(name = "redisCacheTemplate")
public CacheTemplate redisCacheTemplate(@Qualifier(value = REDIS_CACHE_MANAGER) CacheManager redisCacheManager) {
final CacheTemplate cacheTemplate = new CacheTemplate();
cacheTemplate.setCacheManager(redisCacheManager);
return cacheTemplate;
}
@Bean(name = "redisCacheManager1")
public CacheManager redisCacheManager1(@Qualifier(value = "redisTemplate1") RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
// Sets the default expire time (in seconds)
cacheManager.setDefaultExpiration(3000);
cacheManager.setCachePrefix(new DefaultRedisCachePrefix());
cacheManager.setUsePrefix(true);
return cacheManager;
}
@Bean(name = "redisCacheTemplate1")
public CacheTemplate redisCacheTemplate1(@Qualifier(value = "redisCacheManager1") CacheManager redisCacheManager) {
final CacheTemplate cacheTemplate = new CacheTemplate();
cacheTemplate.setCacheManager(redisCacheManager);
return cacheTemplate;
}
}
public class RedisConfig {
@Value("${redis.pwd}")
private String pwd;
@Value("${redis.connection.timeout}")
private Integer timeOut;
@Value("${redis.usePool}")
private Boolean usePool;
@Value("${redis.maxTotal}")
private Integer maxTotal;
@Value("${redis.maxIdle}")
private Integer maxIdle;
@Value("${redis.sentinel.master}")
private String masterName;
@Value("${redis.sentinel.nodes}")
private String sentinelHostAndPort;
@Primary
@Bean(name = "jedisConnectionFactory")
public JedisConnectionFactory jedisConnectionFactory(JedisPoolConfig jedisPoolConfig,
RedisSentinelConfiguration sentinelConfiguration) {
JedisConnectionFactory factory = new JedisConnectionFactory(sentinelConfiguration, jedisPoolConfig);
factory.setPassword(decrypt(pwd));
factory.setUsePool(usePool);
factory.setTimeout(timeOut);
return factory;
}
@Bean(name = "jedisConnectionFactory1")
public JedisConnectionFactory jedisConnectionFactory1(JedisPoolConfig jedisPoolConfig,
RedisSentinelConfiguration sentinelConfiguration) {
JedisConnectionFactory factory = new JedisConnectionFactory(sentinelConfiguration, jedisPoolConfig);
factory.setPassword(pwd);
factory.setUsePool(usePool);
factory.setTimeout(timeOut);
factory.setDatabase(1);
return factory;
}
@Bean
public RedisSentinelConfiguration sentinelConfig() {
HashMap<String, Object> source = new HashMap<>();
source.put("spring.redis.sentinel.master", masterName);
source.put("spring.redis.sentinel.nodes", sentinelHostAndPort);
return new RedisSentinelConfiguration(new MapPropertySource("sentinelProperties", source));
}
@Bean
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(maxTotal);
jedisPoolConfig.setMaxIdle(maxIdle);
return jedisPoolConfig;
}
}
我们可以看出旧版的redis配置如果想要配置两个库只需要新建两个RedisCacheTemplate
,RedisCacheManager
,JedisConnectionFactory
bean即可,但是到了Spring Boot 2中整个Redis配置都发生了变化,上述的配置不再能够奏效了,如果我们只使用一套那么自动化配置会很方便,但是我们需要两套,也就是需要两个Factory,所以我们需要作出改变!
首先,去掉Jedis,使用官方推荐的Lettuce作为Redis客户端,使用Apache的commons-pool2作为连接池 。
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.5.0</version>
</dependency>
那么redis现在该怎么配置呢?首先配置yml【注意,以下省略了顶级spring:】
redis:
lettuce:
pool:
max-active: 20 # 连接池最大连接数
min-idle: 0 # 连接池中的最小空闲连接
max-idle: 10 # 连接池中的最大空闲连接
sentinel:
master: mymaster
nodes: 节点IP,逗号分隔
password: 密码
database: 0
redis1: #redis1号库配置
sentinel:
master: mymaster
nodes: 节点IP,逗号分隔
password: 密码
database: 1
你可以发现我还自定义了一套redis1,那么这么配置后,我在代码里怎么去读取呢?首先我们要明白一件事,yml之所以可以被自动配置是因为setter,getter方法,所以想要让他被自动注入就需要自己定义一套Redis对象。
所以如下的RedisSentinelConfig
,Sentinal
均是自定义的对象,原因就是RedisProperties
中的属性我们不能直接使用,因为作为自动化配置已经指定了应该读取yml那些字段的数据。所以参照这个类的字段声明新的POJO来处理。所以免不了借鉴自动化配置相关的代码,如generateRedisSentinelConfiguration,createSentinels
方法分别参考了RedisConnectionConfiguration
中的getSentinelConfig,createSentinels
方法。
public class RedisConfig {
@Bean(name = "redisTemplate")
public RedisTemplate redisTemplate(@Qualifier(value = "factory0") LettuceConnectionFactory lettuceConnectionFactoryZero) {
return newRedisTemplate(lettuceConnectionFactoryZero);
}
@Bean(name = "redisTemplate1")
public RedisTemplate redisTemplate1(@Qualifier(value = "factory1") LettuceConnectionFactory lettuceConnectionFactoryOne) {
return newRedisTemplate(lettuceConnectionFactoryOne);
}
private RedisTemplate newRedisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
//方便查看key
redisTemplate.setKeySerializer(new StringRedisSerializer());
//方便查看key
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
@Bean
@ConfigurationProperties(prefix = "spring.redis.lettuce.pool")
public GenericObjectPoolConfig redisPool() {
return new GenericObjectPoolConfig();
}
@Bean("redisConfigZero")
@ConfigurationProperties(prefix = "spring.redis")
public RedisSentinelConfig redisConfigZero() {
return new RedisSentinelConfig();
}
@Bean("redisConfigOne")
@ConfigurationProperties(prefix = "spring.redis1")
public RedisSentinelConfig redisConfigOne() {
return new RedisSentinelConfig();
}
@Bean("redisSentinelConfigurationZero")
@Primary
public RedisSentinelConfiguration redisSentinelConfigurationZero(@Qualifier(value = "redisConfigZero") RedisSentinelConfig redisConfigZero) {
return generateRedisSentinelConfiguration(redisConfigZero);
}
@Bean("redisSentinelConfigurationOne")
public RedisSentinelConfiguration redisSentinelConfigurationOne(@Qualifier(value = "redisConfigOne") RedisSentinelConfig redisConfigOne) {
return generateRedisSentinelConfiguration(redisConfigOne);
}
@Bean("factory0")
@Primary
public LettuceConnectionFactory lettuceConnectionFactoryZero(GenericObjectPoolConfig config,
@Qualifier(value = "redisSentinelConfigurationZero") RedisSentinelConfiguration redisSentinelConfiguration1) {
LettuceClientConfiguration clientConfiguration = LettucePoolingClientConfiguration.builder()
.poolConfig(config)
.build();
return new LettuceConnectionFactory(redisSentinelConfiguration1, clientConfiguration);
}
@Bean("factory1")
public LettuceConnectionFactory lettuceConnectionFactoryOne(GenericObjectPoolConfig config,
@Qualifier(value = "redisSentinelConfigurationOne") RedisSentinelConfiguration redisSentinelConfiguration2) {
LettuceClientConfiguration clientConfiguration = LettucePoolingClientConfiguration.builder()
.poolConfig(config)
.build();
return new LettuceConnectionFactory(redisSentinelConfiguration2, clientConfiguration);
}
private RedisSentinelConfiguration generateRedisSentinelConfiguration(RedisSentinelConfig redisConfig) {
Sentinel sentinelProperties = redisConfig.getSentinel();
if (sentinelProperties != null) {
RedisSentinelConfiguration config = new RedisSentinelConfiguration();
config.master(sentinelProperties.getMaster());
config.setSentinels(createSentinels(sentinelProperties));
if (redisConfig.getPassword() != null) {
config.setPassword(RedisPassword.of(redisConfig.getPassword()));
}
config.setDatabase(redisConfig.getDatabase());
return config;
}
return null;
}
private List<RedisNode> createSentinels(Sentinel sentinel) {
List<RedisNode> nodes = new ArrayList<>();
for (String node : sentinel.getNodes()) {
try {
String[] parts = StringUtils.split(node, ":");
Assert.state(parts.length == 2, "Must be defined as 'host:port'");
nodes.add(new RedisNode(parts[0], Integer.parseInt(parts[1])));
} catch (RuntimeException ex) {
throw new IllegalStateException("Invalid redis sentinel " + "property '" + node + "'", ex);
}
}
return nodes;
}
}
至此我们配置出了两个Factory,yml自动化配置的一个坎总算跨过去了,那么接下去就好一点,但是还是很不一样的是CacheManager的构造已经完全跟之前的不一样了!所幸一番研究也不算太复杂。RedisCacheManager
使用构造器来进行组装,如下配置即可。别忘了@EnableCaching
开启缓存!
注:CacheTemplate是我们自己定义的一个缓存管理类,如果你没有这样的需求,参见RedisConfig中的RedisTemplate配置,切记那部分的声明切不可删除,否则session,使用注解进行写入的这些序列化会有问题!
@EnableCaching
public class CacheConfig {
/**
* redis作为缓存的CacheTemplate
*/
public static final String REDIS_CACHE_TEMPLATE_ZERO = "redisCacheTemplate";
private static final String REDIS_CACHE_MANAGER_ZERO = "redisCacheManager";
public static final String REDIS_CACHE_TEMPLATE_ONE = "redisCacheTemplate1";
private static final String REDIS_CACHE_MANAGER_ONE = "redisCacheManager1";
@Primary
@Bean(name = REDIS_CACHE_MANAGER_ZERO)
public CacheManager cacheManager0(@Qualifier(value = "factory0") LettuceConnectionFactory redisConnectionFactory) {
return generateRedisManager(redisConnectionFactory, new GenericJackson2JsonRedisSerializer());
}
@Primary
@Bean(name = REDIS_CACHE_TEMPLATE_ZERO)
public CacheTemplate redisCacheTemplate(@Qualifier(value = REDIS_CACHE_MANAGER_ZERO) CacheManager redisCacheManager) {
final CacheTemplate cacheTemplate = new CacheTemplate();
cacheTemplate.setCacheManager(redisCacheManager);
return cacheTemplate;
}
@Bean(name = REDIS_CACHE_MANAGER_ONE)
public CacheManager cacheManager1(@Qualifier(value = "factory1") LettuceConnectionFactory redisConnectionFactory) {
//其他项目使用GenericFastJsonRedisSerializer序列化
return generateRedisManager(redisConnectionFactory, new GenericFastJsonRedisSerializer());
}
@Bean(name = REDIS_CACHE_TEMPLATE_ONE)
public CacheTemplate redisCacheTemplate1(@Qualifier(value = "redisCacheManager1") CacheManager redisCacheManager) {
final CacheTemplate cacheTemplate = new CacheTemplate();
cacheTemplate.setCacheManager(redisCacheManager);
return cacheTemplate;
}
private CacheManager generateRedisManager(LettuceConnectionFactory redisConnectionFactory, RedisSerializer serialize) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
//默认过期时间
.entryTtl(Duration.ofSeconds(3000))
//cacheName的前缀
.computePrefixWith(cacheName -> cacheName + ":")
//序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serialize));
return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
四、启动方式配置
Spring Boot本身可以自己通过自带的Tomcat启动,但是项目要求使用外部Tomcat进行启动,首先SpringBoot需要一个Application.java类作为启动类
@SpringBootApplication
//扫描内部使用的依赖的一些bean,注入
@ImportResource("classpath*:/applicationContext.xml")
//扫描Mapper
@MapperScan(value = "包路径", annotationClass = Repository.class)
@EnableRedisHttpSession(redisNamespace = "session存储的命名空间,不设置有默认值,见源码", maxInactiveIntervalInSeconds = 28800)
public class Application extends SpringBootServletInitializer {
public static void main(final String[] args) {
SpringApplication.run(Application.class, args);
}
/**
* Note that a WebApplicationInitializer is only needed if you are building a war file and
* deploying it.
* 打成WAR包需要
* @param application
* @return
*/
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
pom文件中打包方式修改:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.0.9.RELEASE</version>
<configuration>
<!--如果是true会导致无法解压,建议jar包启动将其设置为true-->
<executable>false</executable>
//远程调试
<jvmArguments>
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005
</jvmArguments>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
五、国际化
国际化项目直接使用了大框架中的一些预定义的类,无需额外配置,故在此不做叙述。
其他坑
这种在Spring Boot中Propeties文件无法通过下面的方式拿到版本号app.version=${project.version}
正确做法:app.version=@project.version@
另一个项目
总体上和上面的而是一样的改造思路,这里只提一些不一样的改造点。我们下面称这个项目为WEB项目
首页
这个项目存在两个版本1.0,2.0并行项目,通过session共享共存,部署在不同的机器,这里我们只关注2.0版本的。WEB项目中存在web.xml,用于登陆后到达welcome.html,在欢迎页根据账号类型跳转,例如A类型账号跳转到A页面,其他账号跳转到index.html
<welcome-file-list>
<welcome-file>welcome.html</welcome-file>
</welcome-file-list>
在这个欢迎页源码其实是用了一个jsp语法做了判断跳转,那么我们知道,Spring Boot不存在web.xml,以及webapp目录,还有不欢迎jsp。所以这个项目我引入了Thymeleaf为了完全取代jsp。
接下来我们来处理上面的问题,首先我们定义一个controller,映射根路径,在这里面完成欢迎页的判断跳转。
【不要使用@RestController,只能使用@Controller否则会有问题】
@RequestMapping(value = "/", method = RequestMethod.GET)
public String welcome(HttpServletRequest request) {
String isIsp = (String) request.getSession()
.getAttribute("is_xxx_account");
if (StringUtils.equalsIgnoreCase("true", isXxx)) {
return "redirect:/oldxxx/index.html";
} else {
return "redirect:/v2/index/#";
}
}
我们这里只关注/v2/index/#
,另一个属于另一个项目版本不提。首先我们知道他映射到v2/index,这个controller定义了一些属性用于让前端读取,大致如下
@RequestMapping(value = "/v2/index", method = RequestMethod.GET)
public String index(HttpServletRequest request) {
try {
String host = request.getServerName();
request.setAttribute("frontendServerUrl","xxxxx");
request.setAttribute("iconCode", "xxxx");
return "index"
} catch (XxxxxException e) {
LOGGER.error(e.getMessage(), e);
return "redirect:/common/403.html";
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
return "redirect:/common/500.html";
}
}
接下去我们看看index.html应该放置在哪里,我去掉了webapp文件夹,Spring Boot默认的此类动态文件放置在resources/template目录下,当然也可以自定义,因为项目中已经有这个目录作为他用,故而在yml文件中配置了其他的路径
spring:
thymeleaf:
prefix: classpath:/webTemplates/ #替代JSP实现模板替换
cache: false
mode: LEGACYHTML5
之后就是jsp语法的替换了,Thymeleaf的语法可以自己学习。
踩坑预警:
那就是在JS中的使用替换变量,[[]]和[{}]前者是转义的,后者是不经过转义,我使用[[]],产生中文字符变成Unicode,且String后端传出来会被带上引号,在github上看到有人也提了这样issue才找到解决办法:https://github.com/thymeleaf/thymeleaf/issues/645
Redis 配置
WEB项目的redis只使用一个库,配置起来相对简单
spring:
redis:
jedis:
pool:
max-active: 20 # 连接池最大连接数
min-idle: 0 # 连接池中的最小空闲连接
max-idle: 10 # 连接池中的最大空闲连接
sentinel:
master: mymaster
nodes: 节点IP,逗号分隔
password: 密码
database: 0
上文那么推崇Lettuce这里为啥没有使用呢?因为项目中存在部分缓存直接使用jedis进行操作缓存,如果调整起来改动较大,为了项目的稳定性,能够少调整就尽量少调整。所以沿用了jedis客户端。如上配置即可得到一个connectionFactory
在WEB项目中的redis使用又有一个比较特殊的使用方式,他会自定义一些key的缓存时间
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
// Sets the default expire time (in seconds)
cacheManager.setDefaultExpiration(3000);
cacheManager.setCachePrefix(new DefaultRedisCachePrefix());
cacheManager.setUsePrefix(true);
//缓存失效时间设置
ImmutableMap.Builder<String, Long> builder = ImmutableMap.builder();
setExpire(builder, "cache.xxx.history", "cache.xxxx.history.expire");
cacheManager.setExpires(builder.build());
return cacheManager;
原本RedisCacheManager有一个方法setExpires进行自定义的一些有效时间的设置,但是现在RedisCacheManager构造方式有变,就需要找一个新的方式解决,很幸运我们找到了initialCacheNames
,withInitialCacheConfigurations
用于初始化一些指定缓存名的初始化配置,在默认配置里面我们可以指定过期时间。
@EnableCaching
public class CacheConfig {
public static final String REDIS_CACHE_TEMPLATE = "redisCacheTemplate";
private static final String REDIS_CACHE_MANAGER = "redisCacheManager";
@Description("redis template")
@Bean
public RedisTemplate<String, Object> redisTemplate(JedisConnectionFactory jedisConnectionFactory) {
final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(jedisConnectionFactory);
//方便查看key
redisTemplate.setKeySerializer(new StringRedisSerializer());
//方便查看key
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
@Primary
@Description("cacheTemplate that provided by redis")
@Bean(name = REDIS_CACHE_TEMPLATE)
public CacheTemplate redisCacheTemplate(@Qualifier(value = REDIS_CACHE_MANAGER) CacheManager redisCacheManager) {
final CacheTemplate cacheTemplate = new CacheTemplate();
cacheTemplate.setCacheManager(redisCacheManager);
return cacheTemplate;
}
@Primary
@Bean(name = REDIS_CACHE_MANAGER)
public CacheManager cacheManager(JedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(3000))
.computePrefixWith(cacheName -> cacheName + ":")
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
ImmutableMap<String, RedisCacheConfiguration> customExpireMap=initExpire(redisCacheConfiguration);
return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(redisCacheConfiguration)
.initialCacheNames(customExpireMap.keySet())
.withInitialCacheConfigurations(customExpireMap)
.build();
}
/**
* custom cacheName expire setting
*
* @param redisCacheConfiguration 默认配置
* @return
*/
private ImmutableMap<String, RedisCacheConfiguration> initExpire(RedisCacheConfiguration redisCacheConfiguration) {
ImmutableMap.Builder<String, RedisCacheConfiguration> builder = ImmutableMap.builder();
setExpire(redisCacheConfiguration, builder, "cache.xxxx.history", "cache.xxxx.history.expire.key.expire.time");
//.....这里加
return builder.build();
}
private void setExpire(RedisCacheConfiguration redisCacheConfiguration, ImmutableMap.Builder<String, RedisCacheConfiguration> builder,
String cacheName, String expireInMillis) {
builder.put(PropsUtil.get(cacheName),
redisCacheConfiguration.entryTtl(Duration.ofSeconds(Long.parseLong(PropsUtil.get(expireInMillis)))));
}
}
踩坑预警:
项目是1.0,2.0共享session缓存,但是项目跑起来发现1.0拿不到session,排查后发现,存储在cookie中被base64转了一次,所以取不到缓存,排查发现原来在之前版本没有设置默认值,boolean默认为false,Spring Boot升级Spring5 设置了默认值为true,会被base64
/**
* 定制session id序列化到cookie的规则
*/
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setUseHttpOnlyCookie(true);
//Spring 4.x没有设置默认值,boolean默认为false,SpringBoot升级Spring5 set了默认值为true,会被base64
cookieSerializer.setUseBase64Encoding(false);
return cookieSerializer;
}
其他坑
- 使用@RequestMapping如果路径前面带有空格,会导致找不到这个路径从而导致,被当做是静态资源,在CustomInterceptor拦截器处类型转换产生报错,具体原因需要深究源码,为什么在Spring 4就没有问题,目前悬而未决。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
//这里(HandlerMethod) handler报错
validatePermission((HandlerMethod) handler);
validateAccountPermission((HandlerMethod) handler);
return true;
}
- Spring Boot会自动配置好multipartResolver用于文件上传的处理。而项目中原本使用的是Apache的fileupload,所以就出现了冲突。
原因:multipartResolver是一个全局的文件上传处理器,配置上 multipartResolver 这个Bean之后,全局的文件上传都会经过 multipartResolver 处理(读取并解析request的 inputstream )。而 inputstream 仅能处理一次,导致处理完的 HttpServletRequest 中的 inputStream 已经没有内容。
因此后面配置使用的 commons-fileupload 的 ServletFileUpload 无法从 request 中解析出文件上传内容。