Last time has upgraded from Spring Boot 1.5 to Spring Boot 2.0. This time, in another project, I will write about what I was addicted to when I raised it from 1.4 to 2.0.
The main difficulty this time was
--It took a long time to isolate whether the cause of the problem was the 1.4-> 1.5 part or the 1.5-> 2.0 part. --Thymeleaf has a big influence, so testing is difficult --Around serialization / deserialization in Spring Session --Countermeasures for production deployment
It is around.
Specify the Spring Boot version in pom.xml.
pom.xml
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.3.RELEASE</version>
    <relativePath />
  </parent>
I was originally using HikariPC, so I will remove it from the dependencies.
pom.xml
    <dependency>
	<groupId>com.zaxxer</groupId>
	<artifactId>HikariCP</artifactId>
    </dependency>

The package of SpringBootServletInitializer has changed, so reimport it.

The package has changed to ʻorg.springframework.boot.web.servlet.`, so reimport it

The package has changed, so reimport it
Change ʻextends WebMvcConfigurerAdapter to ʻimplements WebMvcConfigurer
Change to @EnableWebSecurity.
https://docs.spring.io/spring-security/site/docs/current/reference/html/mvc.html

Migrate from velocity to mustache.
pom.xml
-    <dependency>
-	<groupId>org.apache.velocity</groupId>
-	<artifactId>velocity</artifactId>
-    </dependency>
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-mustache</artifactId>
+    </dependency>
Replace velocity template file * .vm with mustache template file * .mustache.
According to the mustache template format
${hoge}
↓
{{hoge}}
Replace everything like this
You can replace them all at once with shell, but if you do it with IntelliJ refactoring, the calling code will also be found and told, so if the number is small, IntelliJ refactoring may be good.

Add the following dependency
pom.xml
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-json</artifactId>
	</dependency>
Re-import with the following package
org.springframework.boot.configurationprocessor.json.JSONObject

The argument of SpringApplication.run has been changed
Modified as follows
        public static void main(String[] args) {
-               Object[] objects = { HogeApplication.class, FugaService.class };
-               SpringApplication.run(objects, args);
+               final SpringApplication application = new SpringApplication(HogeApplication.class, FugaService.class);
+               application.run(args);
        }
In addition, inherit the following class
SpringBootServletInitializer

I will fix it silently
https://spring.io/blog/2017/06/20/a-preview-on-spring-data-kay#improved-naming-for-crud-repository-methods

import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureTestDatabase;
From
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
Change package to
https://www.thymeleaf.org/doc/articles/thymeleaf3migration.html
Mechanically like this
find src/main/resources -type f -name "*.html" -print | xargs sed -i  -e 's/th:substituteby/th:replace/g'
Mechanically like this
find src/main/resources -type f -name "*.html" -print | xargs sed -i  -e 's@type=\"text/css\"@@g'
While looking at the contents, delete it.
Such a guy
<script>(window.dataLayer || (window.dataLayer = [])).push(<span th:remove="tag" th:utext="${hoge}"/>)</script>
Fix it like this
<script type="text/javascript" th:inline="javascript">/*<![CDATA[*/
     (window.dataLayer || (window.dataLayer = [])).push(/*[(${hoge})]*/)
 /*]]>*/</script>
SessionScope
Error when @SessionScope cannot be referenced when executing SpringSecurity ʻonAuthenticationSuccess Error creating bean with name 'user': Scope 'session' is not active for the current thread;`
Create the following config file
WebRequestContextListener.java
package jp.hoge;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextListener;
import javax.servlet.annotation.WebListener;
@Configuration
@WebListener
public class WebRequestContextListener extends RequestContextListener {
}
java.sql.SQLSyntaxErrorException: Table 'hoge.hibernate_sequence' doesn't exist
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:536)
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:513)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:115)
	at com.mysql.cj.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:1983)
	at com.mysql.cj.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1826)
	at com.mysql.cj.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1923)
	at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
Change GenerationType
-    @GeneratedValue(strategy=GenerationType.AUTO)
+    @GeneratedValue(strategy=GenerationType.IDENTITY)
org.springframework.dao.InvalidDataAccessResourceUsageException: error performing isolated work; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: error performing
 isolated work
        at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:242)
        at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:225)
        at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527)
        at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
        at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
        at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
        at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:135)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
        at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
        at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
        at com.sun.proxy.$Proxy127.save(Unknown Source)
        at jp.hoge.service.FugaService.execute(FugaService.java:218)
        at jp.hoge.controller.FugaController.execute(FugaController.java:101)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
Hibernate's Generator mappings have changed.
Corrected by adding the following settings to application.properties
application.properties
spring.jpa.hibernate.use-new-id-generator-mappings=false
<SpringProfile> in logback-spring.xml fails to load the profile correctly
For executableJar, you can define it as -Dspring-boot.run.profiles = environment name.
In this project, we will deploy war on tomcat, so
Define it in ʻapplication.properties like spring-boot.run.profiles = environment name`.
Actually, since the profile is divided when the war file is generated in pom.xml, it is defined as follows.
application.properties
spring-boot.run.profiles=${spring.profiles.active}
pom.xml
<profiles>
		<profile>
			<id>local</id>
			<properties>
				<spring.profiles.active>local</spring.profiles.active>
			</properties>
		</profile>
		<profile>
			<id>stg</id>
			<properties>
				<spring.profiles.active>stg</spring.profiles.active>
			</properties>
		</profile>
</profiles>
Profile at build time
mvn package -Pstg
With that feeling, it is embedded in ʻapplication.properties`.
NullPointerExepotion occurs when trying to access a member variable of User However, even if you look at the contents of Redis, you can not read it because it is serialized ...
@SessionScopeOnce, create a Serializer that converts to JSON and registers it in Redis like the following
HttpSessionConfig.java
@ConditionalOnProperty(name = "spring.session.store-type", havingValue = "redis", matchIfMissing = false)
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 20 * 24 * 60 * 60) //default=1800 -> 20days
@Configuration
public class HttpSessionConfig implements BeanClassLoaderAware {
   @Autowired
   private LettuceConnectionFactory lettuceConnectionFactory;
   private ClassLoader classLoader;
   @Bean
   public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
	final ObjectMapper mapper = new ObjectMapper()
			.registerModules(SecurityJackson2Modules.getModules(classLoader))
			.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
	return new GenericJackson2JsonRedisSerializer(mapper);
  }
      //For Elasticache
      @Bean
      public static ConfigureRedisAction configureRedisAction() {
          return ConfigureRedisAction.NO_OP;
      }
	@Override
	public void setBeanClassLoader(final ClassLoader classLoader) {
		this.classLoader = classLoader;
	}
}
Then, when transitioning to Controller after login is completed, it was set in Redis with all fields null.
After logging in, the Controller determines whether ʻid in the ʻUser object is null, and if it is null, repacks it.
After logging in, I can handle the session without any problems.
Actually I wanted to be able to have session information in JSON format, but since the structure of the User class was a complicated nested structure, I gave up due to time constraints ... Too much information in the User class ...
It seems that the SerialVersionUID handled by serialization / deserialization has changed internally with the version upgrade of SpringSession, so when deploying the upgraded code, users who are already logged in cannot deserialize the session information and get stuck.
So, put in the correspondence when deserializing.
https://sdqali.in/blog/2016/11/02/handling-deserialization-errors-in-spring-redis-sessions/
The article is a little old and the dependent libraries have changed, so I changed it as follows.
HttpSessionConfig.java
- public class HttpSessionConfig {
+ public class HttpSessionConfig extends RedisHttpSessionConfiguration {
+	@Autowired
+	RedisTemplate<Object, Object> redisTemplate;
+ 	@Bean
+	@Override
+	public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
+		return super.springSessionRepositoryFilter(new SafeDeserializationRepository<>(sessionRepository, redisTemplate));
+	}
SafeDeserializationRepository.java
public class SafeDeserializationRepository<S extends Session> implements SessionRepository<S> {
    private final SessionRepository<S> delegate;
    private final RedisTemplate<Object, Object> redisTemplate;
     private static final String BOUNDED_HASH_KEY_PREFIX = "spring:session:sessions:";
     public SafeDeserializationRepository(SessionRepository<S> delegate,
            RedisTemplate<Object, Object> redisTemplate) {
        this.delegate = delegate;
        this.redisTemplate = redisTemplate;
    }
     @Override
    public S createSession() {
        return delegate.createSession();
    }
     @Override
    public void save(S session) {
        delegate.save(session);
    }
     @Override
    public S findById(String id) {
        try {
            return delegate.findById(id);
        } catch(SerializationException e) {
            log.info("Deleting non-deserializable session with key {}", id);
            redisTemplate.delete(BOUNDED_HASH_KEY_PREFIX + id);
            return null;
        }
    }
     @Override
    public void deleteById(String id) {
        delegate.deleteById(id);
    }
}
This will allow existing users to log out once, re-login to save new Session data in Redis, and then refer to it.
In general, the above support has made it work in a production environment. (I think there are others)
Also, originally, Tomcat should be 8.5 series or higher, but 8.0 seems to be no problem, so I forgot to upgrade Tomcat.
If you skip some major versions of SpringBoot, it will be very difficult, so let's upgrade frequently ...! !!
Recommended Posts