10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

seasar2からspring boot2系への移行

Posted at

別サイト

に書いていたのだけどサ終してしまったので移行した記事その6
2019/10/01のもの

seasar2からspring boot2へ移行

仕事でseasar2からspring bootへ移行したのでその内容を記録していく

(もちろん色々書き換えてる)

移行前の構成

  • seasar2
  • SAStruts
  • Doma2(ORM)
  • velocity(テンプレートエンジン)

これらをGradleのマルチプロジェクトでプロジェクトを分けて使用

  • build.gradle(親プロジェクト)
    • web/build.gradle(アプリケーションからアクセスする層)
    • admin/build.gradle(会社側から触る管理用ページの層)
    • core/build.gradle(データとの接続を行う層)
    • batch/build.gradle(定期バッチ処理用の層)

以上のようなもので数年ほど運用

早速対応していく

diconファイル

消しましょう、問答無用で。

build.gradleの修正

変わっているとこだけ

buildscript {
    ext {
-       seasarVersion = '2.4.48'
+      springBootVersion = '2.1.1.RELEASE'

+       profile = project.hasProperty('profile') ? project[ 'profile' ]  : 'localhost'
    }

 	repositories {
+ 		gradlePluginPortal()
 	}

 	dependencies {
+         classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
 	}
}

 plugins {
+     id 'org.springframework.boot' version '2.1.1.RELEASE'
 }

 subprojects {
+ 	apply plugin: 'io.spring.dependency-management'

 	repositories {
+ 		gradlePluginPortal()
 	}

 	dependencies {
- 		// seasar
- 		implementation "org.seasar.container:s2-framework:${seasarVersion}"
- 		implementation "org.seasar.container:s2-extension:${seasarVersion}"
- 		implementation "org.seasar.container:s2-tiger:${seasarVersion}"

+ 		// spring boot
+ 		implementation "org.springframework.boot:spring-boot-dependencies:${springBootVersion}"
+ 		implementation "org.springframework.boot:spring-boot-starter-web:${springBootVersion}"
+ 		implementation "org.springframework.boot:spring-boot-starter:${springBootVersion}"
+ 		implementation "org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}"
+ 		implementation "org.springframework.boot:spring-boot-starter-aop:${springBootVersion}"
+ 		implementation "org.springframework.data:spring-data-commons:${springBootVersion}"
+ 		implementation "org.apache.tomcat:tomcat-jdbc:9.0.13"
+ 		implementation "org.seasar.doma.boot:doma-spring-boot-starter:1.1.1"
+ 		implementation "org.springframework.boot:spring-boot-devtools:${springBootVersion}"

 		// web
- 		implementation "org.seasar.sastruts:sa-struts:1.0.4-sp9"
- 		implementation "struts:struts:1.2.9"
- 		implementation "org.apache.velocity:velocity:1.7"
- 		implementation "org.apache.velocity:velocity-tools:2.0"
+ 		implementation "org.springframework.boot:spring-boot-starter-thymeleaf:${springBootVersion}"
+ 		implementation "net.sourceforge.nekohtml:nekohtml:1.9.22"

- 		implementation "log4j:log4j:1.2.17"
+ 		implementation "org.springframework.boot:spring-boot-starter-log4j2:${springBootVersion}"

+ 		implementation "ognl:ognl:3.1.12"
+ 		implementation "commons-dbutils:commons-dbutils:1.7"

- 		testImplementation "org.seasar.aptina:aptina-unit:1.0.0"
- 		testCompile 'junit:junit'
+ 		testImplementation "org.springframework.boot:spring-boot-starter-test:${springBootVersion}"
+ 		testImplementation 'junit:junit:4.12'
  
 	}

+ 	configurations.all {
+     	exclude module: 'spring-boot-starter-logging'
+     	exclude module: "HikariCP"
+     	exclude module: "android-json"
+ 	}
}

seasar2に絡むものを消してspring bootのものを追加していく。
追加するものに関しては、プロジェクトに必要なものを選んでという感じ。
configurations.allには除外しておきたいものを入れる。
この時点で大量にエラーがでるので心が折れる。

subprojectのbuild.gradle

 +apply plugin: 'org.springframework.boot'
 
 +sourceSets {
 +    main {
 +    	java {
 +            srcDir "${rootDir}/core/src/main/java"
 +    	}
 +        resources {
 +            srcDir "${rootDir}/core/src/main/resources"
 +        }
 +    }
 +}

 dependencies {
+ 	implementation "org.springframework.boot:spring-boot-starter-security:2.1.1.RELEASE" 
+ 	implementation "org.springframework.boot:spring-boot-starter-actuator:2.1.1.RELEASE"
+ 	implementation "de.codecentric:spring-boot-admin-starter-client:2.1.1"
 }

+ springBoot {
+     mainClassName = "${project.group}.admin.Application"
+ }

+ bootRun {
+     systemProperties = ['spring.profiles.active': "${profile}"]
+ }

+ bootJar {
+ 	mainClassName = "${project.group}.admin.Application"
+     launchScript()
+ }

spring bootの設定を入れていく。
webなどはcoreを使用するのでsourceSetsを設定していく。
spring securityやspring actuatorはお好み。
使う必要がなければとくに入れる必要はない。

Application作成

 @EnableAsync
 @SpringBootApplication(exclude = {
 		ManagementWebSecurityAutoConfiguration.class,
 		SecurityAutoConfiguration.class,
 		UserDetailsServiceAutoConfiguration.class,
 		SecurityFilterAutoConfiguration.class
 		})
 @ComponentScan(basePackages = "[パッケージのroot]")
 public class Application {
 
 	static {
 		Security.setProperty("networkaddress.cache.ttl", "60");
 		Security.setProperty("networkaddress.cache.negative.ttl", "10");
 	}
 
 	public static void main(String[] args) throws Exception {
 		System.setProperty("server.servlet.context-path", "/admin");
 		SpringApplication springApplication = new SpringApplication(Application.class);
 		Properties properties = new Properties();
 		properties.setProperty("spring.profiles.active", "localhost");
 		properties.setProperty("spring.datasource.initialization-mode", "NEVER");
 		properties.setProperty("spring.resources.add-mappings", "true");
 		springApplication.setDefaultProperties(properties);
 		springApplication.run(args);
 	} 
 }

securityを入れたはいいんだけど使っているURLがsecurity側で不正扱いになってしまい、
URLをなおすのは少し厳しい状況だったため結局excludeに入れてオフる。
意味ない。。。。
キャッシュの設定やコンテキストパスの設定、その他プロパティの設定などを入れる。

各種Actionの対応

@Controller
@RequiredArgsConstructor
@RequestMapping("")

クラスへのアノテーション
@Controllerでseasar2でいうアクションを指定していく
@RequiredArgsConstructorはlombokのものだけどコンストラクタインジェクションが楽なので設定
@RequestMappingでURL設定
ちなみに@RestControllerでも基本的には一緒

-    @Resource
-    private LoginService loginService;

+    private final LoginService loginService;

DIしていたものはこんな感じで変更。private finalにすることでコンストラクタインジェクションの対象になる

-     @Resource
-     @ActionForm
-     public LoginForm loginForm;

+    @ModelAttribute
+     LoginForm setUpForm() {
+         return new LoginForm();
+     }

formの設定
やり方はいろいろありそうだけどわかりやすいのは@ModelAttribute使うものな気がする

-    public Boolean notLoginFlg;

-    @Execute(validator= false)
-    public String index() {
-        notLoginFlg = null;
-        return "index.html";
-    }

+    @RequestMapping("/")
+    public String index(ModelMap model) {
+        model.put("notLoginFlg", null);
+        return "index";
+    }

アクションのメソッド
アノテーションがかわるのとModelMapを使ってデータを格納するようにする

@Controller
@RequiredArgsConstructor
@RequestMapping("")
public class IndexAction {
    private final LoginService loginService;

    @ModelAttribute
    HogeForm setUpForm() {
        return new HogeForm();
    }

    @RequestMapping("/")
    public String index(HttpServletRequest request, HttpServletResponse response, HogeForm hogeForm, ModelMap model) {
        model.put("notLoginFlg", null);
        return "index";
    }   
 }

結果的にこんな感じ
ちなみにHttpServletRequestとHttpServletResponseは引数に入れればデータをいれて渡してくれる
便利

こんな感じでDI設定やformやアクションの定義と書き方を変えていく

@RequiredArgsConstructorを使うとfinalのフィールドに対してコンストラクタが生成されるので
対象のフィールドをコンストラクタインジェクションされるようにできるのでコードがすっきりする
(たしかこれsearsar2だとデフォルトコンストラクタがないよって怒られてコンストラクタインジェクションができなくて諦めてた記憶がある)

publicフィールドに変数をもって画面側に渡している構成になっているアプリケーションはMedelMap等にセットするようにしなくてはいけないのでここの移行が非常にめんどくさくなる。
(自分もそうだったので大変だった)

pathを変数でとるとき

@RequestMapping(value = "/{hoge}/{huga}")
public String index(@PathVariable String hoge, @PathVariable String huga) {
    return "index";
}

@PathVariableを使うことでとれる

@Controllerだけどjsonで返したい時

@RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String index() {
    return "{}";
}

produces = MediaType.APPLICATION_JSON_VALUE
@ResponseBody
を使うことで@Controllerでもjsonを返せる。
自分は@Controllerでページ遷移してその中のajax処理も同じactionクラス内で受けていたのでこういうのが必要だった

session情報格納クラス

- @Component(instance = InstanceType.SESSION)
+ @Component
+ @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)

アノテーションをかえればOK

velocity -> thymeleaf

spring5以降velocityとの連携クラスが削除され、どうにもvelocityがうまく動かなくなったので移行
動かせそうな人は無理にやらなくてもいいと思う
htmlをてこてこ構文に合わせて直す作業なのでここは割愛
格納フォルダがwebappではなくresources/static、resources/templatesなどに変わるぽい
ここらへんはthymeleafの公式ドキュメントがあるので参照するとよい
html表現は基本的に同じことは実現できるはず
ただhtmlが多くて実際の作業の時間の半分はこれに使った、心折れた

velocityで独自のマクロ関数を使っている場合

thymeleafに同じものを作成すればなんとかなる

マクロ処理

public class ThymeleafUtils {
    public boolean hoge() {
        return true;
    }
}

どこかに同じマクロを作成します。別にそのまま前のマクロを使ってもよい。

マクロ呼び出しクラス作成

 @Component
 public class ThymeleafDialect implements IExpressionObjectDialect {
 	static final String KEY = "呼び出す時のキー名";
 
 	final Set<String> names = new HashSet<String>() {
 		{
 			add(KEY);
 		}
 	};
 
 	@Override
 	public IExpressionObjectFactory getExpressionObjectFactory() {
 		return new IExpressionObjectFactory() {
 
 			@Override
 			public Set<String> getAllExpressionObjectNames() {
 				return names;
 			}
 
 			@Override
 			public Object buildObject(IExpressionContext context, String expressionObjectName) {
 				if (KEY.equals(expressionObjectName)) { //名前が一致したなら
 					return new ThymeleafUtils(); // マクロを返す
 				}
 				return null;
 			}
 
 			@Override
 			public boolean isCacheable(String expressionObjectName) {
 				return true;
 			}
 		};
 	}
 
 	@Override
 	public String getName() {
 		return "ThymeleafUtilsDialect";
 	}
 
 }

IExpressionObjectDialectを使って特定のキーを使用した場合に、マクロを設定したThymeleafUtilsを使うようにしていく

テンプレートエンジンへの登録

 @Configuration
 public class ThymeleafConfig {
 	@Autowired
 	private ThymeleafDialect thymeleafDialect;
 
 	@Bean
 	public TemplateEngine templateEngine(TemplateEngine templateEngine) {
 		templateEngine.addDialect(thymeleafDialect);
 		return templateEngine;
 	}
 }

テンプレートエンジンに上で作った設定を差し込む
これでThymeleaf上で

${#キー名.hoge()}

とかで使えるはず

AOP

インターセプター処理を移行していく

public class HogeInterceptor extends AbstractInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        return invocation.proceed();
    }
 }

seasar2だとcustomizer.diconに定義した上でこんな感じだと思う

移行後は以下の感じ。Java内にインターセプトする条件等も書いていく
以下の例だとactionパッケージに関わる処理を通る際にインターセプトします
Orderはなくてもよいけど複数のインターセプターを入れて順番を制御したい場合には使うの推奨

@Order(10)
@Component
@Aspect
public class HogeInterceptor {
    @Around("execution(* *..*Action.*(..))")
    public Object invoke(ProceedingJoinPoint jp) throws Throwable {
        MethodSignature signature = (MethodSignature) jp.getSignature();
        Method method = signature.getMethod();
        return jp.proceed();
    } 
 }

MethodとかClass>はよく使うけどさくっと取れる

SingletonS2Container

- SingletonS2Container.getComponent("hogehoge");
+ ApplicationContextProvider.getApplicationContext().getBean(HogeHoge.class);

SingletonS2Containerがなくなるのでspringのほうのコンテナから情報を取るように変える

jarで呼び出す時

javaにmainクラスを置いて呼び出す形式で元のがこれだとすると

CLASSPATH=$CLASSPATH:$ROOTDIR/bin/batch.jar
java -cp $CLASSPATH hogehoge.batch.job.BatchJob

移行後はこんな感じ

ENVIRONMENT_NAME=dev
java -jar -Dspring.profiles.active=$ENVIRONMENT_NAME -Dserver.port=9000 -Dbatch.execute=BatchJob $ROOTDIR/bin/batch.jar

profileのとこは変数とかでもらうようにして切り替えたりとかとか

主にバッチ処理をshから呼び出す時にjavaコマンドで呼び出していたので対応していく
java側は以下のようにApplicationRunnerを使う実装になる

public class BatchJob implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
    }  

    @Configuration
    public static class BatchConfig {
        @Bean
        @ConditionalOnProperty(value = { "batch.execute" }, havingValue = "BatchJob")
        public BatchJob batchJob() {
            return new BatchJob();
        }
    }
}

dao

daoクラスはdoma-genで自動生成しているがspring bootでは@ConfigAutowireableをつける必要があるので追加

+ @ConfigAutowireable
 @Dao
 public interface HogeDao {
 }

自動生成時のテンプレートをいじれば以降は自動化できるようになりそう

Helperなどのコンポーネント

@Componentをつけます
@RequiredArgsConstructorもつけると@Autowiredもいらないのでスッキリする

サービスクラスについては@Serviceをつける

// 元のやつ
public class HogeHelper {
    @Resource
    private HogeDao hogeDao;
}

// 移行後
@Component
@RequiredArgsConstructor
public class HogeHelper {
    private final HogeDao hogeDao;
}
 
// service
@Service
@RequiredArgsConstructor
public class HogeService {
    private final HogeHelper hogeHelper;
}

接続先DBの切り替え

seasar2とは結構違うので最初はちょっと行き詰まった
元々はdaoパッケージにおいている処理にインターセプターを挟んでDataSourceFactoryにdataSource名をセットするというようにやってた
DataSourceFactoryがなくなるから同じようにやるために対象DBを保持するホルダーを作成して接続しに行く時にそのホルダーから取り出す、という方式にした

domaの設定側でこのホルダーを参照するように変えていく

切り替えのインターセプター

@Component
@Aspect
public class HogeInterceptor {
    @Around("execution(* *..*Dao.*(..))")
    public Object invoke(ProceedingJoinPoint jp) throws Throwable {
        Schema schema = SchemaHolder.getSchema() == null ? Schema.DEFAULT : SchemaHolder.getSchema();
        // ここで切り替えたいDBのdataSourceNameをとってくる
        String dataSourceName = "hogehogeDataSource";
        try {
            SchemaHolder.setSchema(dataSourceName);
            return jp.proceed();
        } finally {
            // 戻す
            SchemaHolder.setSchema(schema);
        }
    }
 }

ホルダー用のクラス

public class SchemaHolder {
    @AllArgsConstructor
    @Getter
    public static enum Schema {
        DEFAULT("defaultDataSource"),
        HOGEHOGE("hogehogeDataSource"),
        ;
        private final String name;
    }
 
    private static final ThreadLocal<Schema> schemaHolder = new ThreadLocal<>();
 
    public static void setSchema(Schema schema) {
        schemaHolder.set(schema);
    }
 
    public static void setSchema(String name) {
        for (Schema schema : Schema.values()) {
            if (schema.getName().equals(name)) {
                setSchema(schema);
                break;
            }
        }
    }
 
    public static Schema getSchema() {
        return schemaHolder.get();
    }
 
    public static void clear() {
        schemaHolder.remove();
    }
 }

doma2

domaの場合org.seasar.doma.jdbc.Configをimplementsして設定を定義しているのでそこを書き換える

mybatisだと不要らしいのだけどdomaの場合はtransactionに参加させるために
TransactionAwareDataSourceProxyでDataSourceをラップする必要がある

tomcatやdatasourceの設定はymlに書きそれを読み込んで設定する形にした

DB切り替えは先程のホルダーを使って行う

@Configuration
@EnableTransactionManagement
public class DaoAppConfig implements Config {
    @Override
    @Bean
    public DataSource getDataSource() {
        if (dataSource != null) {
            return dataSource;
        }
        AbstractRoutingDataSource abstractRoutingDataSource = new AbstractRoutingDataSource() {
            @Override
            protected Object determineCurrentLookupKey() {
                if (SchemaHolder.getSchema() == null) {
                    return Schema.DEFAULT.getName();
                }
                return SchemaHolder.getSchema().getName();
            }
        };

        Map<Object, Object> dataSources = Map.ofEntries(
            entry(Schema.DEFAULT.getName(), defaultDataSource()),
            entry(Schema.HOGEHOGE.getName(), hogehogeDataSource()),
        );
        abstractRoutingDataSource.setTargetDataSources(dataSources);
        abstractRoutingDataSource.setDefaultTargetDataSource(defaultDataSource());
        dataSource = abstractRoutingDataSource;
        return dataSource;
    }

    @Bean
    public DataSource defaultDataSource() {
        return new TransactionAwareDataSourceProxy(this.tomcatJdbcConnectionPool(defaultDbc(), dbcProp()));
    }

    @Bean
    public DataSource hogehogeDataSource() {
        return new TransactionAwareDataSourceProxy(this.tomcatJdbcConnectionPool(hogehogeDbc(), dbcProp()));
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.default")
    protected DataBaseConnectionSetting defaultDbc() {
        return new PostgresqlConnectionSetting();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.hogehoge")
    protected DataBaseConnectionSetting hogehogeDbc() {
        return new PostgresqlConnectionSetting();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.tomcat")
    protected DataBaseConnectionSettingProperties dbcProp() {
        return new PostgresqlConnectionSettingProperties();
    }

    private org.apache.tomcat.jdbc.pool.DataSource tomcatJdbcConnectionPool(DataBaseConnectionSetting dbc, DataBaseConnectionSettingProperties dbcProp) {
        org.apache.tomcat.jdbc.pool.DataSource ds = new org.apache.tomcat.jdbc.pool.DataSource();

        // 接続先情報等を設定
        ds.setDriverClassName(dbc.getDriver());
        ds.setUsername(dbc.getUser());
        ds.setPassword(dbc.getPassword());
        ds.setUrl(dbc.getUrl());
        return ds;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        PlatformTransactionManager[] platformTransactionManagers = new PlatformTransactionManager[] {
            new DataSourceTransactionManager(defaultDataSource()),
            new DataSourceTransactionManager(hogehogeDataSource()),
        };
        return new ChainedTransactionManager(platformTransactionManagers);
    }
}

DB設定のメソッドインタフェース

public interface DataBaseConnectionSettingProperties {
 	Integer getMaxActive();
 	Integer getInitialSize();
 	Integer getMaxIdle();
 	Integer getMinIdle();
 	Integer getMaxAge();
 	Boolean getFairQueue();
 	Integer getMaxWait(); 
 	Boolean getTestOnBorrow();
 	Boolean getTestOnReturn();
 	String getValidationQuery();
 	Integer getValidationQueryTimeout();
 	Integer getValidationInterval();
 	Boolean getTestWhileIdle();
 	Integer getTimeBetweenEvictionRunsMillis();
 	Integer getMinEvictableIdleTimeMillis();
 	Boolean getRemoveAbandoned();
 	Integer getRemoveAbandonedTimeout();
 	String getConnectionProperties();
 }

DB設定のプロパティクラス

 @Data
 public class PostgresqlConnectionSettingProperties implements DataBaseConnectionSettingProperties {
 	private Integer maxActive;
 	private Integer initialSize;
 	private Integer maxIdle;
 	private Integer minIdle;
 	private Integer maxAge;
 	private Boolean fairQueue;
 	private Integer maxWait;
 	private Boolean testOnBorrow;
 	private Boolean testOnReturn;
 	private String validationQuery;
 	private Integer validationQueryTimeout;
 	private Integer validationInterval;
 	private Boolean testWhileIdle;
 	private Integer timeBetweenEvictionRunsMillis;
 	private Integer minEvictableIdleTimeMillis;
 	private Boolean removeAbandoned;
 	private Integer removeAbandonedTimeout;
 	private String connectionProperties;
 }

DB設定の接続インタフェース

 public interface DataBaseConnectionSetting {
 	String getUrl();
 	String getUser();
 	String getPassword();
 	String getDriver();
 	String getDriverClassName();
 	Integer getMaxActive();
 	Integer getInitialSize();
 	Integer getMaxIdle();
 	Integer getMinIdle();
 }

DB設定の接続クラス

 @Data
 public class PostgresqlConnectionSetting implements DataBaseConnectionSetting {
 	private String driver = "org.postgresql.Driver";
 	private Integer port = 5432;
 	private String domain = "127.0.0.1";
 	private String schema;
 	private String user;
 	private String password;
 	private String dbname = "postgresql";
 	private Integer maxActive = null;
 	private Integer initialSize = null;
 	private Integer maxIdle = null;
 	private Integer minIdle = null;

 	@Override
 	public String getUrl() {
 		return String.format("jdbc:%s://%s:%s/%s", this.dbname, this.domain, this.port, this.schema);
 	}
 
 	@Override
 	public String getDriverClassName() {
 		return driver;
 	}
 
 }

application.yml

アプリケーションの設定を記述

起動時のprofileによってapplication-[profile名].ymlの設定も読み込んで上書きするので
デフォルトの設定をapplication.ymlに書きプロファイルごとに変えたい設定をそれぞれに書く。
actuatorやspring boot adminを使用することにしたのでその旨も記述する。

application.yml

 spring.thymeleaf.mode: HTML 
 spring.datasource.initialization-mode: NEVER
 spring.datasource.type: [org.seasar.doma.jdbc.Configをimplementsしたクラス] 
 spring.main.allow-bean-definition-overriding: true

server.connection-timeout: 600000

spring.datasource.tomcat:
   maxActive: 10
   initialSize: 10
   maxIdle: 2
   minIdle: 1
 
 spring:
   datasource:
     default:
       domain: localhost
       user: user
       password: password
       schema: default
     hogehoge:
       domain: localhost
       user: user
       password: password
       schema: hogehoge

 management:
   endpoints:
     web:
       base-path: /actuator
       exposure:
         exclude: '*'
   endpoint:
     shutdown:
       enabled: false

一応これで基本の設定とかdoma2に読み込ませるDB設定、actuatorの設定がされる

application-dev.yml

 spring:
   datasource:
     default:
       domain: 255.255.255.255
       user: user
       password: password
       schema: default
     hogehoge:
       domain: 255.255.255.255
       user: user
       password: password
       schema: hogehoge

profileで上書きしたいときはこんなファイルを作り書き換える
profile=devだったときのみ上書かれる

transaction

トランザクションの境界は@Transactionalを使用する
すべてのエラーでロールバックしたいときはrollbackForを設定する

@Transactional(rollbackFor = Exception.class)
public List<> findList() {
    return hogeHelper.findList();
}

で、ここまでやればあとはエラーを潰したり、動かしてみておかしいところを対応していけば移行できそう

以下、行き詰まったことや追加であとからやっていったものをメモ書きしておく

コンストラクタインジェクションで依存が循環してエラーになる

コンストラクタインジェクションの場合、起動時にDIを行いそのメンバに対してもDIを行うのでソースが相互依存していたり依存が循環しているとアプリケーションの起動時にエラーします。

依存性を解消してしまうのがよいけどスパゲッティだったり触れない場合もあるのでその場合はフィールドインジェクションかセッターインジェクションなどを使う

@Component
public class HogeHelper {
    private HogeDao hogeDao;

    @Autowired
    public void hogeDao(HogeDao hogeDao) {
        this.hogeDao = hogeDao;
    }
}

log4j -> log4j2

actuatorを使う際にログレベルをいじろうとしたらログ設定にいれているものが出てこなくて調べたらどうもlog4j2とかlogbackにしないとでなそう、とのことなのでサクッと移行。

log4j.xml

 <?xml version="1.0" encoding="UTF-8" ?>
 <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
 <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
 	<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
 		<param name="threshold" value="info" />
 		<layout class="org.apache.log4j.PatternLayout">
 			<param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />
 		</layout>
 	</appender>

 	<appender name="FILE" class="org.apache.log4j.FileAppender">
 		<param name="File" value="/tmp/logs/hoge.log" />
 		<layout class="org.apache.log4j.PatternLayout">
 			<param name="ConversionPattern" value="%m%n" />
 		</layout>
 	</appender>

 	<logger name="org.apache.commons" additivity="false">
 		<level value="WARN" />
 		<appender-ref ref="STDOUT" />
 	</logger>

 	<logger name="file_log" additivity="false">
 		<level value="INFO" />
 		<appender-ref ref="FILE" />
 	</logger>

 	<root>
 		<priority value="INFO" />
 		<appender-ref ref="STDOUT" />
 	</root>

 </log4j:configuration>

log4j.xmlがこんな風に定義していたとしたら下記のように移行できる

log4j2.xml

 <?xml version="1.0" encoding="UTF-8"?>
 <Configuration status="off">
     <Appenders>
     	<Console name="STDOUT" target="SYSTEM_OUT">
     		<PatternLayout pattern="%d [%t] %-5p %c - %m%n"/>
     	</Console>
     	<File name="FILE" fileName="/tmp/logs/hoge.log">
     		<PatternLayout pattern="%m%n"/>
     	</File>
     </Appenders>
 
     <Loggers>
     	<Logger name="org.apache.commons" level="WARN" additivity="false">
     		<AppenderRef ref="STDOUT"/>
     	</Logger>
     	<Logger name="file_log" level="INFO" additivity="false">
     		<AppenderRef ref="FILE"/>
     	</Logger>
     	<Root level="INFO">
     		<AppenderRef ref="STDOUT"/>
     	</Root>
     </Loggers>
 
 </Configuration>

Application起動後の処理

アプリケーションが起動したら行いたい処理があったためそちらも実装した

@EventListener(ApplicationReadyEvent.class)

を指定することで起動準備完了時に呼ばれる

@SpringBootApplication
@ComponentScan(basePackages = "hoge")
public class Application {
    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(Application.class);
        springApplication.run(args);
    }
 
    @EventListener(ApplicationReadyEvent.class)
    public void doAfterStartup() {
        // 起動時にやりたい処理をする
        // ここに来た時点でDI等は行われているはずなのでDao操作する処理の呼び出しとかもできる
    }
 }

exceptionのハンドラー

GlobalException的な共通のエラー処理を使いたい場合は作成する

@Controller
@RequiredArgsConstructor
@ControllerAdvice(annotations = { RestController.class })
public class ApiExceptionHandler { 
    @ExceptionHandler(Throwable.class)
    public Object handlerException(HttpServletRequest request, HttpServletResponse response, Throwable t) {
        // Throwableなので全部ひろってしまう
    
        // 種類で処理をわけたいとき
        if (t instanceof NullPointerException) {
        } else {
        }

        // 共通処理とか書く
        return null;
    }
 
    @ExceptionHandler(NullPointerException.class)
    public Object nullPointerException(HttpServletRequest request, HttpServletResponse response, Throwable t) {
        // 特定のエラー拾いたい時
        return new RedirectView("http://localhost:8080/nullpointer");
    }
 }

ここまですべてやったらだいたい移行できたので独自で入れてるものなどを動くように調整していく

自分がやった中であったのはXMLBeansがspring5以降連携クラスが消されてしまったので
XLSMapperに差し替えて処理を書き換えたなどがあった
心折れた

動くようになればactuatorとかrestdocsとかspring-boot-adminとか導入できるようになり独自でやっていたものが消せて色々管理しやすくなった

かかった期間としては日次の作業の傍らだったのもあり自分1人でやって2ヶ月弱ぐらい
最初はコンパイルすら通らない状態になるから孤独な戦いだけどいつか光が開けると思って我慢の戦い

ソースが完全に変わってしまうので付随するjenkinsやらシェルやらも直すことになるし
gitで先行しているコードにマージしていくのも気にしながら都度本流ソースを取り込んで解消しながらやらないと最終マージがつらそう
コミットが多いプロジェクトや多くの人で作業しているプロジェクトだと難度が上がりそう

作成したjarのサーバ反映

おまけでcentosのサーバに反映したメモを書いていきます

稼働フォルダ作成

sudo mkdir -p /var/local/app
sudo chown -R user:user /var/local/app

フォルダはどこでもいいです。起動ユーザで操作できる場所なら問題ないです。

作成したフォルダにbootJarで作成したjarを置く

./gradlew web:bootJar -x test
# web/build/libs/web.jarができているのでspcとかであげる

init.d作成

sudo ln -s /var/local/app/web.jar /etc/init.d/web

これをやると/etc/init.d/webが起動スクリプト(バイナリ)になります
build.gradleでbootJarにlaunchScript()を入れているのがポイントです

conf作成(jarと同じ場所)

cat web.conf
JAVA_OPTS="-Dspring.profiles.active=dev"

jarと同じ名前のconfを同フォルダに置きます
色々書いてますがデフォルトでも動くはするので特にコンフィグ変えないよって場合はなしでもいける

sudo権限

sudo visudo
user ALL=(root) NOPASSWD:/etc/init.d/web *

起動しやすいようにvisudoに権限を追加します

起動

sudo /etc/init.d/web start

ログローテート

sudo vi /etc/logrotate.d/syslog
/var/log/web.log

/var/log配下にログを吐くのでそれのローテート設定を入れます
/var/log以外にもlog4j2等の設定で好きな場所にも吐けます

感想とか

最初はもっとかかると思ってはじめた作業だったので思ったより早くできてよかったなー
というのが最初の感覚
seasar2自体は個人的には使いやすくてよかったのですが開発が終わってしまっているというのが大きくて、いよいよ移行せざるを得ないな、となりやることに。。。
とはいえ使えるものもあったのでseasar2に依存しない便利系のutil系のライブラリとかはぶっちゃけ移植してきて使ったりはしています

ざっと移行して早速いいなと思ったのはrestdocsで、
ユニットテスト書く
->テストデータ作る
->テストする
->ドキュメントが作られる
という流れが簡単にできるようになって、APIドキュメントの質が担保しやすくなったなあ、と。

10
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?