前阵子部门要求我们的项目接入新框架,使用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类,继承ConfiguarationSupportWebConfig,项目的个性化配置的AppConfig,以及RedisConfig,CacheConfig,DataSourceConfig这些数据库缓存相关的配置。

步骤:

  1. 依赖调整;
  2. WebInitializer实现类替代,转yml、新配置或者注解实现;
  3. 数据库,redis改造
  4. 启动方式改造
  5. 国际化

改造进程

一、依赖调整

去除所有的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对象。

所以如下的RedisSentinelConfigSentinal均是自定义的对象,原因就是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构造方式有变,就需要找一个新的方式解决,很幸运我们找到了initialCacheNameswithInitialCacheConfigurations用于初始化一些指定缓存名的初始化配置,在默认配置里面我们可以指定过期时间。

@EnableCaching
public class CacheConfig {
 
    /**
     * redis作为缓存的CacheTemplate
     */
    public static final String REDIS_CACHE_TEMPLATE = "redisCacheTemplate";
    private static final String REDIS_CACHE_MANAGER = "redisCacheManager";
 
    /**
     * 官方redis template 在session这些上有使用,不可或缺
     *
     * @param jedisConnectionFactory
     * @return
     */
    @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;
   }

其他坑

  1. 使用@RequestMapping如果路径前面带有空格,会导致找不到这个路径从而导致,被当做是静态资源,在CustomInterceptor拦截器处类型转换产生报错,具体原因需要深究源码,为什么在Spring 4就没有问题,目前悬而未决。
@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
//这里(HandlerMethod) handler报错
        validatePermission((HandlerMethod) handler);
        validateAccountPermission((HandlerMethod) handler);
        return true;
    }
  1. Spring Boot会自动配置好multipartResolver用于文件上传的处理。而项目中原本使用的是Apache的fileupload,所以就出现了冲突。

原因:multipartResolver是一个全局的文件上传处理器,配置上 multipartResolver 这个Bean之后,全局的文件上传都会经过 multipartResolver 处理(读取并解析request的 inputstream )。而 inputstream 仅能处理一次,导致处理完的 HttpServletRequest 中的 inputStream 已经没有内容。

因此后面配置使用的 commons-fileupload 的 ServletFileUpload 无法从 request 中解析出文件上传内容。