2020年5月30日 (土)

WSL2にした話

0. ついに Windows 10 version 2004 が GA

先日、ようやくWindows 10 version 2004がGAになったので入れてみた。
今回の目玉の一つはWindows Subsystem for Linux(WSL) version 2(以下「WSL2」)だろう。

僕はすでに既存のWSLでUbuntuを使っていたので、公式の移行手順をもとにUbuntuを更新を開始。

で、どうなったかというと

  • 手元の環境では移行に2時間40分ほどかかった(「数分かかることがあります」とはいったい....)
  • 手元のPCには16GBほどメモリを積んでいるんだけど、途中、vmmemというプロセスが10GBほどメモリを握りこんで動作が非常に緩慢になる

....てな感じではあったもののどうにか終了。変換が終了したら、vmmemが握っていたメモリは無事解放された。

ちなみにこのvmmemというプロセス、後で調べてみたら、どうやらこれが「WSL2が使っているマネージドVM」な模様。

1. Xクライアントを動かせるようにする

変換が無事終わったので WSLのUbuntuを起動。

僕はWindows用XサーバのVcXsrvをホスト環境に入れていて、Linuxのほうがいろいろと都合がいいPHPなんかは、WSLからPhpStormなんかを立ち上げて読み書きしているんだが、日本語入力ができないと何かと不便なので、WSLのシェルスタートアップ時にインプットメソッドのfcitxを起動するようにしている。

と、起動すると、「ディスプレイに接続できない」だの、「dbus-launchが異常終了して初期化できない」だの、思ってたのと違う状況に。

いろいろググりあげた結果、次のようにすることで解決した。

  1. ディスプレイに接続できない件は、DISPLAY環境変数の設定をlocalhostからresolv.confのアドレスに変えるQiitaの記事を参考にして解決
  2. dbus-launchが異常終了する件は、StackExchangeに同様のお悩みを抱えた方からの質問を参照して解決

2. どうしてこうなった

後追いでどうしてこうなったのか調べてみることに。

「WSLはAPI変換方式からマネージドVM方式に変わる」という話は小耳にはさんだことはあったのだけど、それがどういった影響があるのかまでは気にしていなかったのでいまさらなんだけど、ASCII.jpに懇切丁寧な解説記事があるのを発見。

記事を参照すると「WSL用の仮想スイッチが追加されている」ということなのでタスクマネージャーを見てみると、確かにvEthernet(WSL)というのが追加されている。

Task_mgr_1
一方、WSL内部でifconfigしてみたところ、ネットワークアドレスは172.27.128.0/20のようだ。プリフィックス20とは広くとってんな、というのが正直な感想。

  $ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.27.136.3 netmask 255.255.240.0 broadcast 172.27.143.255
inet6 fe80::215:5dff:fe7a:7ea1 prefixlen 64 scopeid 0x20<link>
ether 00:15:5d:7a:7e:a1 txqueuelen 1000 (イーサネット)
RX packets 2699 bytes 421846 (421.8 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 102 bytes 7288 (7.2 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

 


  lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (ローカルループバック)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

ただ、「再起動するとアドレスは変わるので一定にならない」そうなので、この値はあくまで参考値ということになる。

早い話、Windowsホスト環境とWSL内部はvEthernetのアドレスを接点にNATされている、ということのようなので、件のQiitaの記事で書かれている/etc/resolv.confのnameserverのアドレスを指定するやり方で問題ない、ということのようだ。

一方StackExchangeで紹介されているdbus-launchだが、DESCRIPTIONの冒頭には

dbus-launch コマンドは、シェルスクリプトから dbus デーモンのセッションバスインスタンスを開始するために使用されます。
通常は、ユーザーのログイン スクリプトから呼び出されます。デーモン自体とは異なり、dbus-launch は終了するので、
バックティックや $() コンストラクトを使用して dbus-launch から情報を読み取ることができます。

 

引数を指定しない場合、dbus-launch はセッションバスインスタンスを起動し、そのインスタンスのアドレスと PID を
標準出力に出力します。

....とある。WSL内で実際に実行してみると、シェル変数定義を二つ返す。

  $ dbus-launch
DBUS_SESSION_BUS_ADDRESS=unix:abstract=/tmp/dbus-Xq7M2S9YAw,guid=f6f9e17a76a4f8db225fc4bd5ed2165b
DBUS_SESSION_BUS_PID=389

manpagesの記載によれば、Xセッションの開始時にこれらの変数定義がないと新しいセッションを開始するようになっていて、事前定義するなどでdbus-launchによる自動起動を回避できるようだ。

fcitxがdbusを使っているので、dbus-launchの起動方法を工夫することはできるんだろうけど。

3. ところで使用感は

ざっくりこんなところ。

  • DockerをWSL2バックエンドに変えたら、明らかに起動が速くなったうえ、ホスト環境へのパフォーマンス影響も体感的には相当改善された、気がする
  • 従来のLXSSカーネルモジュールでの動作と比べ、Ubuntu Shellの起動は明らかに遅くなった
  • メモリをもりもり食うようになった
    ※WSL2の起動で2GBほど、Dockerを加えると+1.8GB、これにコンテナが乗るとさらに増える、といった感じ

ところで、WSL2で使うことになったアドレス範囲を内部ネットワークですでに使っている場合、どうなるんだろう?

» 続きを読む

2020年5月21日 (木)

今年もまたこのイベントがやってきた(通算47回目)

イベント is 何

言わずと知れた誕生日というやつである。

たまにはさらしてみようか、例の奴を。

2020年5月 6日 (水)

Spring Batchで改行がめちゃくちゃなCSVと戦う

0. まだまだSpring Batchと格闘中です

先日実際に起きたお話。

取引先からダウンロードしたとあるCSVをSpring Batchで書いた取込処理にかけたところ、ファイルの読み込みで死ぬケースが起きたので調べてみたところ、CSVのカラムに改行が入っていたことでFlatFileItemReaderがお亡くなりになっていた。
要はこんな感じである。

Csv_with_messy_line_breaks

実リリース前のテストで発覚したのでよかったものの、これはあかん、ということで、どうするべきかを考えることに。

1. 今回のお題目

  • 改行を含む可能性があるのは、特定のカラムだけ
  • 改行コードはバラバラ(CRLF、LF、CRのいずれもありうる)、かつエスケープされていない
  • データのカラムはダブルクォーテーションで囲まれている
  • 1行目はヘッダ行なので読み飛ばす必要がある
  • ....をチャンクモデルでどう扱うかを考えるわけだが、最大の問題は2番目のかつエスケープされていないという箇所。これがもとで、標準のDelimitedLineTokenizerArrayIndexOutOfBoundsExceptionでお亡くなりになってしまう。

    ちゃんとエスケープされているなら大丈夫だそうなんだが。
    TERASOLUNA Batch Frameworkのファイルアクセスの項を参照。

    そういう時にはCSVを読み込むカスタムReaderを作るべしということだそうなので、実際にやってみた。

    2. ItemReader / ItemProcessor / ItemWriter

    読み込みのときに問題があるので、まずItemReaderから。
    先ほどのQiitaの記事ほぼそのままで恐縮だが、使い慣れていたOpenCSVをパーサにしているのと、Builderを追加しているのが違うくらい。

      class CsvItemReader<T>() : AbstractItemCountingItemStreamItemReader<T>(),
          ResourceAwareItemReaderItemStream<T>, InitializingBean {
    
        var charset: Charset = Charset.defaultCharset()
        var linesToSkip: Int = 0;
        var delimiter: Char = ','
        var quotedChar: Char = '"'
        var escapeChar: Char = '"'
    
        private lateinit var resourceToRead: Resource
        private lateinit var headers: Array<String>
        private lateinit var fieldSetMapper: FieldSetMapper<T>
    
        private var noInput: Boolean = false
        private lateinit var csvReader: CSVReader
    
        init {
          setName(this.javaClass.simpleName)
        }
    
        override fun doOpen() {
          Assert.notNull(resourceToRead, "Resource to read is required")
    
          // 例外をスローするとバッチにブレーキがかかる
          noInput = true
          if (!resourceToRead.exists()) {
            throw IllegalStateException("Input resource does not exist : $resourceToRead")
          }
          if (!resourceToRead.isReadable) {
            throw IllegalStateException("Input resource must be readable : $resourceToRead")
          }
    
          // ここからOpenCSVの初期化
          // CSVParser
          val csvParserBuilder = CSVParserBuilder().withSeparator(delimiter)
              .withQuoteChar(quotedChar)
              .withStrictQuotes(true)
          // 同じ値を書き込むと怒られるので、不一致の場合のみにする
          if (quotedChar != escapeChar) {
            csvParserBuilder.withEscapeChar(escapeChar)
          }
    
          csvReader = CSVReaderBuilder(FileReader(resourceToRead.file, charset))
              .withCSVParser(csvParserBuilder.build())
              .withSkipLines(linesToSkip)
              .build()
    
          noInput = false
        }
    
        override fun doRead(): T? {
          // 読める状態にない、あるいは読んだ内容が空だったときは null を渡すと空データとして処理される
          if (noInput) {
            return null
          }
    
          if (csvReader == null) {
            throw ReaderNotOpenException("CSVReader is not initialized")
          }
    
          // OpenCSVで行を読む
          val line: Array<out String> = csvReader.readNext() ?: return null
    
          // FieldSetMapperに読んだ行を渡してPOJOにマップさせる
          val fs: FieldSet = DefaultFieldSet(line, headers)
          return fieldSetMapper.mapFieldSet(fs)
        }
    
        override fun doClose() {
          // 終了処理で呼ばれる
          // 各種パーサはここで閉じておくべし
          csvReader.close()
        }
    
        override fun setResource(resource: Resource) {
          // ResourceAwareItemReaderItemStream から。
          // これを実装しておくと、 MultiResourceItemReader の委譲先にすることができるようになるっぽい
          this.resourceToRead = resource
        }
    
        override fun afterPropertiesSet() {
          Assert.notNull(this.headers, "header is required")
          Assert.notNull(this.fieldSetMapper, "FieldSetMapper is required")
        }
    
        fun setHeaders(headers: Array<String>) {
          this.headers = headers
        }
    
        fun setFieldSetMapper(fieldSetMapper: FieldSetMapper<T>) {
          this.fieldSetMapper = fieldSetMapper
        }
    
        /**
         * 上記CsvItemReaderのビルダ。
         */
        class Builder<T>() {
          private val reader: CsvItemReader<T> = CsvItemReader()
    
          fun build(): CsvItemReader<T> {
            return reader
          }
    
          fun withResource(resource: Resource): Builder<T> {
            reader.setResource(resource)
            return this
          }
    
          fun withFieldSetMapper(fieldSetMapper: FieldSetMapper<T>): Builder<T> {
            reader.fieldSetMapper = fieldSetMapper
            return this
          }
    
          fun withHeaders(headers: Array<String>): Builder<T> {
            reader.headers = headers
            return this
          }
    
          fun withCharset(charset: Charset): Builder<T> {
            reader.charset = charset
            return this
          }
    
          fun withLinesToSkip(linesToSkip: Int): Builder<T> {
            reader.linesToSkip = linesToSkip
            return this
          }
    
          fun withDelimiterChar(delimiter: Char): Builder<T> {
            reader.delimiter = delimiter
            return this
          }
    
          fun withQuotedChar(quotedChar: Char): Builder<T> {
            reader.quotedChar = quotedChar
            return this
          }
    
          fun withEscapeChar(escapeChar: Char): Builder<T> {
            reader.escapeChar = escapeChar
            return this
          }
        }
      }

    ItemReaderとセットで使うFieldSetMapperは単純。たぶん、みればわかるレベル。

      @Component
      class CsvUserMapper: FieldSetMapper<CsvUser> {
        override fun mapFieldSet(fieldSet: FieldSet): CsvUser {
          // fieldSet の値を順番に抜いてはめて返すだけ
          return CsvUser(fieldSet.readString(0), fieldSet.readString(1))
        }
      }

    そのほか、中間処理をうけもつItemProcessorと書き込みを受け持つItemWriterも、話を単純にするためごく単純にしてみた。

    • ItemProcessor
      @Component
      class CsvImporterProcessor : ItemProcessor<CsvUser, AppUser> {
        override fun process(item: CsvUser): AppUser? {
          // 単にインスタンスを組み替えるだけ
          return AppUser(item.username, item.description)
        }
      }
    • ItemWriter
      @Component
      class CsvItemWriter(private val appUserRepository: AppUserRepository): ItemWriter<AppUser> {
        override fun write(items: MutableList<out AppUser>) {
          // こちらも右から左に永続化するだけ
          // AppUserRepository は AppUser の JpaRepository
          appUserRepository.saveAll(items)
        }
      }

    この状態でStepをSpring Beanとして構成してやる。

      @Bean
      fun csvItemReader(csvUserMapper: CsvUserMapper): ItemReader<CsvUser> {
        return CsvItemReader.Builder<CsvUser>()
                .withCharset(StandardCharsets.UTF_8)
                .withResource(ClassPathResource("/csv/userdata.csv")) // クラスパス内にあるファイルを指定している
                .withFieldSetMapper(csvUserMapper)
                .withLinesToSkip(1) // 1行飛ばす
                .withHeaders(arrayOf("username", "description"))  // ヘッダをマップするメンバーの定義
                .withDelimiterChar(',') // 区切り記号
                .withQuotedChar('"')    // 囲み文字
                .build()
      }
    
      @Bean
      fun step1(csvItemReader: ItemReader<CsvUser>, csvItemWriter: CsvItemWriter, csvImporterProcessor: CsvImporterProcessor): Step {
        return stepBuilderFactory.get("csvItemReaderStep")
                .chunk<CsvUser, AppUser>(10)
                .reader(csvItemReader)
                .processor(csvImporterProcessor)
                .writer(csvItemWriter)
                .build()
      }

    こうしてやることで、ようやく改行を含むカラムをちゃんと読めるようになった。
    DBに書き込んだ結果がこちら。

    Insert_result_1

    3. ソースコード

    https://github.com/f97one/LineBreakAwareCsvImporterDemoをご参照ください。

    2020年5月 4日 (月)

    WicketとSpringを悪魔合体させる その2

    0. ではがったいさせるぞ(通算二度目)

    WicketとSpringを悪魔合体させるアプローチとして、前回の記事では

    1. WicketをベースにSpringのDIを個別に組み込む
    2. Spring BootをベースにフロントをWicketにする

    のうち前者にチャレンジしてみたが、今回は後者をやってみる。

    1. 準備

    Spring Bootをベースにするので、まずは普通にSpring InitializrでSpring Bootなプロジェクトを作る。
    この時のポイントは、テンプレートエンジンは入れないこと。Apache WicketはViewよりの機能を提供するので、画面組立はWicketにさせるためだ。

    以下、pom.xmlのdependenciesを抜粋。

      <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-kotlin</artifactId>
    </dependency>
    <dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-reflect</artifactId>
    </dependency>
    <dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib-jdk8</artifactId>
    </dependency>
    <!-- Apache Wicket -->
    <dependency>
    <groupId>org.apache.wicket</groupId>
    <artifactId>wicket-spring</artifactId>
    <version>${wicket.version}</version>
    </dependency>
    <dependency>
    <groupId>org.apache.wicket</groupId>
    <artifactId>wicket-core</artifactId>
    <version>${wicket.version}</version>
    </dependency>
    <dependency>
    <groupId>org.apache.wicket</groupId>
    <artifactId>wicket-ioc</artifactId>
    <version>${wicket.version}</version>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
    <exclusion>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    </exclusion>
    </exclusions>
    </dependency>
    </dependencies>

    2. 普通のSpring Bootアプリケーションからいじるところ

    まず、Spring BootにすることでKotlinのコードはsrc/main/kotlinに、ページに紐づくViewのHTML等それ以外のファイルはsrc/main/resourcesに、それぞれ配置されるようになるが、WicketのQuick Startで作ったプロジェクトではJavaのコードもページに紐づくHTMLも同じ場所に置かれるようになっている。

    ここは趣味の問題と言えるけど、Wicketのしきたりに従うなら、pom.xmlのbuildセクションをいじってHTMLをsrc/main/kotlin以下に置けるようにするといい。

      <build>
    <resources>
    <resource>
    <filtering>false</filtering>
    <directory>src/main/resources</directory>
    </resource>
    <resource>
    <filtering>false</filtering>
    <directory>src/main/kotlin</directory>
    <includes>
    <include>**</include>
    </includes>
    <excludes>
    <exclude>**/*.kt</exclude>
    </excludes>
    </resource>
    </resources>
    <testResources>
    <testResource>
    <filtering>false</filtering>
    <directory>src/test/resources</directory>
    </testResource>
    <testResource>
    <filtering>false</filtering>
    <directory>src/test/kotlin</directory>
    <includes>
    <include>**</include>
    </includes>
    <excludes>
    <exclude>**/*.kt</exclude>
    </excludes>
    </testResource>
    </testResources>
    <!-- 以下略 -->
    </build>

    またWicketには、クラシックなwarで使われるweb.xmlで設定している設定が必要になる。
    Create a Wicket Quickstartで作ることができるmvn archetype:generateの結果には、以下のようなweb.xmlが含まれているはず。

    <?xml version="1.0" encoding="ISO-8859-1"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/web-app_3_1.xsd"
      version="3.1">
    
      <display-name>test-wicket-app1</display-name>
    
      <!--
        There are three means to configure Wickets configuration mode and they
        are tested in the order given.
    
        1) A system property: -Dwicket.configuration
        2) servlet specific <init-param>
        3) context specific <context-param>
    
        The value might be either "development" (reloading when templates change) or
        "deployment". If no configuration is found, "development" is the default. -->
    
      <filter>
        <filter-name>wicket.test-wicket-app1</filter-name>
        <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
        <init-param>
          <param-name>applicationClassName</param-name>
          <param-value>net.formula97.webapps.WicketbootlinApplication</param-value>
        </init-param>
      </filter>
    
      <filter-mapping>
        <filter-name>wicket.test-wicket-app1</filter-name>
        <url-pattern>/*</url-pattern>
      </filter-mapping>
    </web-app>

    Spring BootにすることでJava Configで書くことができるようになるので、これをもとにServletContextInitializerの実装クラスをConfigurationにして書いてやる。

      @Configuration
      class ServletInitializer: ServletContextInitializer {
        override fun onStartup(servletContext: ServletContext) {
          // web.xml の設定をもとに書く
          // WicketFilter は Apache Wicket の ServletFilter
          val registration: FilterRegistration = servletContext.addFilter("wicket.wicketbootlin", WicketFilter::class.java)
          registration.setInitParameter(WicketFilter.APP_FACT_PARAM, SpringWebApplicationFactory::class.java.name)
    
          // メインクラスのFQCNをここで指定する
          registration.setInitParameter("applicationClassName", WicketbootlinApplication::class.java.name)
          // いわゆるサーブレットフィルタ部分
          registration.setInitParameter(WicketFilter.FILTER_MAPPING_PARAM, "/*")
          registration.addMappingForUrlPatterns(null, false, "/*")
    
          // 起動モード指定
          // spring.profiles.active の値に応じて development と deployment を切り替えるとかもアリだろう
          registration.setInitParameter("configuration", "development")
        }
      }

    最後にエントリーポイントになるWebApplicationだが、Spring Bootのエントリーポイントは、作った直後ではこうなっているはず。

      @SpringBootApplication
    class WicketbootlinApplication
    companion object { @JvmStatic fun main(args: Array) { runApplication(*args) } }

    なので、エントリーポイントのクラスをWebApplicationの継承クラスにして、getHomePage()とinit()を実装する。

      @SpringBootApplication
      class WicketbootlinApplication: WebApplication() {
        @Autowired
        private lateinit var applicationContext: ApplicationContext
    
        companion object {
          @JvmStatic
          fun main(args: Array) {
            runApplication(*args)
          }
        }
    
        override fun getHomePage(): Class {
          return HomePage::class.java
        }
    
        override fun init() {
          super.init()
    
          // レスポンスとマークアップ時のエンコーディングをUTF-8にする
          requestCycleSettings.responseRequestEncoding = CharEncoding.UTF_8
          markupSettings.defaultMarkupEncoding = CharEncoding.UTF_8
    
          // ComponentScanの結果を反映
          // これで Wicket から Spring による DIコンポーネントを使うことができるようになる
           componentInstantiationListeners.add(SpringComponentInjector(this, applicationContext))
    
          // todo ページルーティングを書く
          mountPage("/", HomePage::class.java)
        }
      }

    これでフロントがWicketのSpring Bootアプリケーションになったので、

      @Service
      interface EnterpriseMessage {
        fun getMessage(): String
        fun getVersionCode(): String
      }

    こんなServiceを

      class HomePage(params: PageParameters): WebPage(params) {
        // Page クラスは Spring の配下にあるわけではないので、
        // Autowired ではなく org.apache.wicket.spring.injection.annot.SpringBean を使う
        @SpringBean
        private lateinit var enterpriseMessage: EnterpriseMessage
    init { // ページに値を張る add(Label("message", Model.of(enterpriseMessage.getMessage()))) add(Label("versionCode", Model.of(enterpriseMessage.getVersionCode()))) } }

    とやることでPageクラスから利用できるようになる。
    ページに値を張った結果はこんな感じ。
    Injected

    あとは、ビジネスロジックやデータアクセスに関する部分は、Springのもつ強力な機能を活用すればいいだろう。

    2020年5月 3日 (日)

    WicketとSpringを悪魔合体させる その1

    0. ではがったいさせるぞ

    データアクセスがからむユニットテストをやるには、やはりDIの力を借りるのが手っ取り早い。

    CDIでもいいんだけれど、今回はSpringを使ってみることにする。
    アプローチとしては

    1. WicketをベースにSpringのDIを個別に組み込む
    2. Spring BootをベースにフロントをWicketにする

    の二つがあるが、今回は前者にチャレンジしてみる。

    1. 準備

    何はなくともpom.xmlの依存設定だ。wicket-springを依存に追加する。
    そのほか、SpringのDIをつかうので、Spring Contextとjavax.annotation-apiも加える。

      <dependency>
        <groupId>org.apache.wicket</groupId>
        <artifactId>wicket-spring</artifactId>
        <version>${wicket.version}</version>
      </dependency>
      <dependency>
        <groupId>org.springframework</groupId>
        <artifactid>spring-context</artifactId>
        <version>5.1.10.RELEASE</version>
      </dependency>
      <dependency>
        <groupId>javax.annotation</groupId>
        <artifactid>javax.annotation-api</artifactId>
        <version>1.3.2</version>
      </dependency>

    2. 各所の実装

    話を簡単にするため、今回はView側にDIでねじ込まれた値を張るだけにしてみる。こんな感じ。

    • HomePage.html
      <!DOCTYPE html>
      <html xmlns:wicket="http://wicket.apache.org" lang="ja">
      <head>
          <meta charset="utf-8" />
          <title></title>
      </head>
      <body>
      <h1>
          <span wicket:id="message">hello</span>
      </h1>
      <p>
          java.version = <span wicket:id="versionCode">8</span> .
      </p>
      </body>
      </html>
    • HomePage.kt
      class HomePage(parameters: PageParameters): WebPage(parameters) {
        // Autowired ではなく org.apache.wicket.spring.injection.annot.SpringBean を使う
        @SpringBean
        private lateinit var enterpriseMessage: EnterpriseMessage
    
        init {
          add(Label("message", Model.of(enterpriseMessage.message)))
          add(Label("versionCode", Model.of(enterpriseMessage.versionCode)))
        }
      }

    エントリーポイントになるWebApplicationの継承クラスは、DIしたいパッケージにコンポーネントスキャンをかけてSpringの管理下に置く処理を書く。

      class WicketApplication(): WebApplication {
        override fun init() {
          super.init()
    
          val ctx = AnnotationConfigApplicationContext()
          // コンポーネントスキャン対象を指定
          ctx.scan("net.formula97.webapps.beans")
          ctx.refresh()
          // Wicket-Springの機能でSpringの管理下に置く
          getComponentInstantiationListeners().add(SpringComponentInjector(this, ctx))
        }
      }

    Spring管理下に置かれたクラスは、今回は定数クラスっぽい扱いにしてみた。

      @ManagedBean
      class EnterpriseMessage {
        val message: String = "Welcome to the Spring-Integrated world!"
        val versionCode: String = System.getProperty("java.version")
      }

    こうしてやることで、マネージドビーンをnewすることなく(そもそもKotlinだとインスタンスを作るのにnewというキーワードは使わないのだが)使うことができるようになる。

    2020年4月28日 (火)

    Spring + Thymeleafで検索 + ページング + ソートを同時にやる件

    0. よく見るアレを自力で書く

    画面上半分に検索用の入力値を入れる部分があり、下半分に検索結果を表示するテーブルとページネーションバーが置かれている、ギョーミーアプリではよく見るアレを自力で書いたことがなかったので、その時のメモを。
    もう何番煎じなのかわからないけど、多分に自分用メモである。

    1. 動きの仕様的なもの

    ざっとこんな感じか。

    • 検索項目はPOSTで受け付ける
    • GETにクエリパラメータをぶら下げても検索できる
    • ページング処理はGETで行う

    2. 各所の要点的なもの

    今回は、総務省が公開している全国地方公共団体コードのExcelファイルをもとに、自治体コードと自治体名を検索表示するSpring Bootアプリケーションをネタにしてみた。

    なお、ソースコード全体はこちら

    2-1. 検索について

    検索項目はPOSTで受け付けるがGETにクエリパラメータをぶら下げても検索できるということなので、Controllerはこうなる。

        @PostMapping("/search")
        fun search(model: Model, @PageableDefault pageable: Pageable, condition: Condition?): String {
            // クエリパラメータを組み立てる
            val b: StringBuilder = StringBuilder("redirect:/?page=${pageable.pageNumber}")
    
            if (condition != null) {
                if (StringUtils.hasLength(condition.cityCode)) {
                    b.append("&cityCode=${condition.encodedCityCode()}")
                }
                if (StringUtils.hasLength(condition.cityName)) {
                    b.append("&cityName=${condition.encodedCityName()}")
                }
            }
    
            return b.toString()
        }

    ちと汚いが、POSTで受け付けたフォームデータをクエリパラメータに組みなおしてGETエンドポイントにリダイレクトしている。いわゆるPRG(POST-REDIRECT-GET)パターンである。

    肝心要の検索処理はGETエンドポイント側で行う。

        @GetMapping("/")
        fun show(model: Model, @PageableDefault pageable: Pageable,
                 @RequestParam(value = "cityCode", required = false) cityCode: String?,
                 @RequestParam(value = "cityName", required = false) cityName: String?,
                 @RequestParam(value = "sortItem", required = false) sortItem: String?,
                 @RequestParam(value = "direction", required = false) direction: String?,
                 @RequestParam(value = "sortDirection", required = false) sortDirection: String?): String {
    
            // 以下、クエリパラメータを一つずつ解析して検索にかける
            // :
            // :
            // 以下略
    
            return "/sorted"
        }

    @RequestParamアノテーションで受け付けるクエリパラメータを定義するのだけど、required = falseをつけて渡されなくてもエラーとしないようにする。

    また、ソートに関するパラメータもここで受け付ける。

    2-2. ページングに関して

    Springには、もともとページングに関する処理が入っていて、Repositoryにページ要求を渡すとページ情報付きで検索結果をもらうことができる。この辺りはSpring Web ページネーションあたりでググればいくらでも記事がヒットする。

    なので、Repositoryはこうなる。

    @Repository
    interface CityRepository: JpaRepository<City, String> {
    
        fun findCitiesByCityCodeIsLike(@Param("cityCode") cityCode: String, pageable: Pageable): Page<City>
        // その他いろいろ
        // :
      }

    引数のPageableが検索時に使うページ要求、戻り値のPageがページデータになる。

    画面のテンプレートエンジンには、戻り値のPageをそのまま張ればページネーションとして機能する。
    のだが、今回はあえてそれ専用のPOJOを用意した。

    View側になるテンプレートには、ページネーションバーに受け付けられるすべてのクエリパラメータを固めるようThymeleafのリンクURL式を書いてやる。このあたりはほぼhttps://blog1.mammb.com/entry/2018/06/04/225310そのまんまを拝借させてもらった。

    2-3. ソートに関して

    ソートもページネーションの応用でいける。

        <table class="table table-bordered">
            <thead>
            <tr>
                <th class="text-center">
                    <a href="#" th:if="${sortItem} eq 'cityCode'"
                       th:href="@{${url}(page=(${page.number} - 1), cityCode=${condition.cityCode}, cityName=${condition.cityName}, sortItem='cityCode', direction=${direction})}">
                        団体コード<span th:if="${sortItem} eq 'cityCode'"><span th:if="${direction} eq 'ASC'">▽</span><span th:if="${direction} eq 'DESC'">△</span></span>
                    </a>
                    <a href="#" th:unless="${sortItem} eq 'cityCode'"
                       th:href="@{${url}(page=(${page.number} - 1), cityCode=${condition.cityCode}, cityName=${condition.cityName}, sortItem='cityCode', direction='ASC')}">
                        団体コード
                    </a>
                </th>
            </tr>
            </thead>
        :

    前述のソートとあわせると、Controllerはこうなる。

        @GetMapping("/")
        fun show(model: Model, @PageableDefault pageable: Pageable,
                 @RequestParam(value = "cityCode", required = false) cityCode: String?,
                 @RequestParam(value = "cityName", required = false) cityName: String?,
                 @RequestParam(value = "sortItem", required = false) sortItem: String?,
                 @RequestParam(value = "direction", required = false) direction: String?,
                 @RequestParam(value = "sortDirection", required = false) sortDirection: String?): String {
    
            // ソートとページ要求は割と密な関係なので、条件を同時に整理する
            var sort: Sort? = null
            if (StringUtils.hasLength(sortDirection)) {
                val sd = if (sortDirection == Sort.Direction.ASC.name) Sort.Direction.ASC.name else Sort.Direction.DESC.name
                val si: String
                if (StringUtils.hasLength(sortItem)) {
                    si = sortItem!!
                    model.addAttribute("sortItem", si)
                } else {
                    si = "cityCode"
                }
                sort = Sort.by(Sort.Direction.fromString(sd), si)
    
                model.addAttribute("sortDirection", sd)
                model.addAttribute("direction", sd)
            } else if (StringUtils.hasLength(sortItem) && StringUtils.hasLength(direction)) {
                val dir = if (direction == Sort.Direction.DESC.name) Sort.Direction.ASC.name else Sort.Direction.DESC.name
                val searchDir = if (direction == Sort.Direction.DESC.name) Sort.Direction.DESC else Sort.Direction.ASC
                model.addAttribute("sortItem", sortItem)
                model.addAttribute("direction", dir)
                model.addAttribute("sortDirection", direction)
                val si = if (sortItem == "prefName") "pref.prefName" else sortItem
                sort = Sort.by(searchDir, si)
            }
    
            // ページ要求を作り直して検索
            val condition = Condition(cityCode, cityName)
            val p: Pageable = if (sort == null) {
                pageable
            } else {
                PageRequest.of(pageable.pageNumber, pageable.pageSize, sort)
            }
            val cities: Page<City> = citySearchService.searchCity(condition, p)
            val page = Pagination(cities)
            // クエリパラメータで渡された検索条件を画面のModelに張りなおす
            model.addAttribute("cityList", cities)
            model.addAttribute("page", page)
            model.addAttribute("condition", condition)
    
            return "/sorted"
        }

    3. 最後に

    受け付けるクエリパラメータを公開しておけば、トラブル対応もしやすいんじゃなかろうか。
    まぁ、クエリパラメータがURLにいっぱいぶら下がっていると「URLが汚くなるから全部POSTにしたい」という人も中にはいそう(※あくまで個人の見解です)だけど。

    それはそうと、ページネーションはサーバサイドではあまりコードを書かなくても実現できるけど、テンプレートエンジン側は結構めんどいことをやらないといけないのがつらい....。

    2020年4月 3日 (金)

    Spring Batchで複数ファイルをItemReaderに使う

    0. Spring Batchと格闘中です

    現職では、自力でバッチアプリを書いていて、なぜか思いのほかJavaと格闘している。

    利用シーンとしては、CSV提供されるマスタデータを複数のテーブルに格納する、というありがちな奴なんだけど、このCSVが一本ではなく複数ある。

    ということなので、最初はタスクレットモデルで複数ファイルを一気に扱う方式で実装していたけれど、メモリ使用量とかの問題でチャンクモデルで実装しなおすことに。

    と、ここで問題が。
    普通のFlatFileItemReaderでは複数のファイルを同時に処理できない。

    そこでMultiResourceItemReaderの出番です。

    1. MultiResourceItemReader

    Reads items from multiple resources sequentially (複数のリソースから項目を順番に読み取る) とあるとおり、処理を移譲されたItemReaderに順番にResourceを渡す。

    ソース全体はhttps://github.com/f97one/MultiFileItemReaderBatchExを参照いただくとして、肝になりそうな部分を抜粋してみる。

    ここでは、同じ書式の複数のファイルから二つのテーブルにデータを振り分ける処理を書いてみた。

    ItemReaderまわりのBean
      @Bean
      fun directoryOrgReader(orgReader: FlatFileItemReader<OrgFile>): MultiResourceItemReader<OrgFile> {
          // Resourceを配列として返す
          // 渡したくないファイルがあるならここでフィルタしておく
          val dir = Path.of("C:", "work", "readtest")
          val dirFiles = Files.list(dir).collect(Collectors.toList())
          val csvResList = mutableListOf<Resource>()
          for (p in dirFiles) {
              csvResList.add(FileSystemResource(p))  // プロジェクト外のファイルなので FileSystemResource を使う
          }
    
          val reader = MultiResourceItemReader<OrgFile>()
          reader.setResources(csvResList.toTypedArray())
          reader.setDelegate(orgReader)  // ここに入っているBeanで実際に処理される
          return reader
      }
    
      @Bean
      fun orgReader(): FlatFileItemReader<OrgFile> {
          val mapper = DefaultLineMapper<OrgFile>()
          val delimitedLineTokenizer = DelimitedLineTokenizer()
          delimitedLineTokenizer.setNames("id", "subject", "itemType")
          mapper.setLineTokenizer(delimitedLineTokenizer)
          val fieldSetMapper = BeanWrapperFieldSetMapper<OrgFile>()
          fieldSetMapper.setTargetType(OrgFile::class.java)
          mapper.setFieldSetMapper(fieldSetMapper)
    
          // 本来のFlatFileItemReaderには処理するResourceを指定する必要があるが、
          // delegateしてくるMultiResourceItemReaderからResourceを供給されるので
          // ここには何も書かなくてよい
          return FlatFileItemReaderBuilder<OrgFile>()
                  .name("orgFileReader")
                  .linesToSkip(1)
                  .encoding("windows-31j")
                  .lineMapper(mapper)
                  .build()
      }
    実行ステップ定義
      @Bean
      fun step1(directoryOrgReader: MultiResourceItemReader<OrgFile>, orgItemWriter: MultiTblItemWriter): Step {
          // ここではItemProcessorは定義していないが、必要に応じて追加しよう
          return stepBuilderFactory.get("step1")
                  .chunk<OrgFile, OrgFile>(10)
                  .reader(directoryOrgReader)  // MultiResourceItemReaderのほうをreaderにする
                  .writer(orgItemWriter)       // MultiTableItemWriterはItemWriterを実装したクラス
                  .build()
      }

    ItemWriterについてはよくあるやつなので、GitHubのほうを観てもらえば雰囲気はわかると思う。

    え? 読み込ませるファイルに順序がある? そんなときはComapratorを実装すればいい。

      @Bean
      fun directoryOrgReader(orgReader: FlatFileItemReader<OrgFile>): MultiResourceItemReader<OrgFile> {
          // Resourceを配列として返す
          // 渡したくないファイルがあるならここでフィルタしておく
          val dir = Path.of("C:", "work", "readtest")
          val dirFiles = Files.list(dir).collect(Collectors.toList())
          val csvResList = mutableListOf<Resource>()
          for (p in dirFiles) {
              csvResList.add(FileSystemResource(p))  // プロジェクト外のファイルなので FileSystemResource を使う
          }
    
          val reader = MultiResourceItemReader<OrgFile>()
          reader.setResources(csvResList.toTypedArray())
          reader.setDelegate(orgReader)  // ここに入っているBeanで実際に処理される
          reader.setComparator { o1, o2 ->
              // o1, o2 とも nullable な Resource のインスタンスなので、
              // getFile() なり getFileName() なりして比較結果を
              // Int で返してやろう
              return 0
          }
          return reader
      }

    2020年1月 9日 (木)

    CSRを楽に作る

    0. CSR is 何

    SSL証明書の署名要求(Certificate Signing Request)のこと。[出典]

    仕事でSSL証明書の更新をやることになったんだけど、その時に必要なデータを作るのが微妙にめんどかった。

    1. 一発作成できるようにしてみた

    やることはおおむね以下のとおり。

    1. 秘密鍵の生成
    2. 秘密鍵をもとにしたCSRの作成
    3. CSRをしかるべき証明機関に送って署名してもらう
    4. 対になる秘密鍵と署名済み公開鍵(あれば中間証明書も)をサーバに仕込む

    最後の「秘密鍵と署名済み公開鍵をサーバに仕込む」は、まぁ読んで字のごとくなんだが、opensslで秘密鍵とCSRを生成するのを年1回しかやらないと色々忘れるので、処理をシェルスクリプトに固めてみた。
    ちなみに対象環境はWSLのUbuntu。

    使い方は以下のとおり。
    ソースを見てもらえばわかるけど、それぞれデフォルト値が設定されている。

    create_csr.sh [-d domain] [-p passphrase] [-s subject] [-y year]
      -d domain     ファイル名にするドメイン名
      -p passphrase 秘密鍵生成に使うPEMパスフレーズ
      -s subject    証明書のDN
      -y year       発行年
    

    こいつを実行することで、カレントディレクトリに発行年のディレクトリを作成して秘密鍵とCSRを作成、CSRの中身をクリップボードへ放り込む。

    あとは、クリップボードの中身を証明機関のWeb画面にペーストすれば署名要求は完了、という算段になっている。

    2. 改造について

    改造するとしたら、2~6行目のデフォルト値の部分、最終行のクリップボードへ送る処理、あたりか。

    特に最終行はOSごとに処理が異なるので、変更は必須じゃなかろうか。

    2019年12月15日 (日)

    俺氏、ついにWindowsでgo-oci8を使うことに成功する

    0. 結果的にいろいろ理解不足だった

    前回記事では進捗ダメです状態で終わっていたのだけど、あれからいろいろいじり倒してどうにか形にすることができた。

    まず前回記事の最後にある

      > go get github.com/mattn/go-oci8
      # github.com/mattn/go-oci8
      In file included from GOPATH\src\github.com\mattn\go-oci8\cHelpers.go:3:0:
      ./oci8.go.h:1:17: fatal error: oci.h: No such file or directory
      compilation terminated.
    

    について。これはoci8.pcのインクルードパスの書き方に問題があったようで、InstantClient側の階層を一つ下げるとともに、WindowsのパスセパレータであるバックスラッシュをUnix系OSで使われるスラッシュに置き換えることで事なきを得た。

      orasdk=C:/bin/instantclient_19_3/sdk
      gcc=C:/bin/TDM-GCC-64
    
      oralib=${orasdk}/lib/msvc
      orainclude=${orasdk}/include
    
      gcclib=${gcc}/lib
      gccinclude=${gcc}/include
    
      glib_genmarshal=glib-genmarshal
      gobject_query=gobject-query
      glib_mkenums=glib-mkenums
    
      Name: oci8
      Description: oci8 library
      Libs: -L${gcclib} -L${oralib} -loci
      Libs.private:
      Cflags: -I${orainclude} -I${gccinclude}
      Version: 19.3.0
    

    1. 最終的にどうしたのか

    どうやら、最初から以下のようにしておけばよかった、ということらしい。

    1. GCCをMinGW GCCの最新版にする
    2. MinGWと同じツールチェインで作られているGTK+由来のpkg-configを追加する
    3. oci8.pcをUnix形式のパスセパレータ表記で作成する

    1-1. GCCをMinGW GCCの最新版にする

    推奨品という触れ込みのTDM-GCCだが、上記のoci8.pcgo buildしてみると、

      C:\bin\instantclient_19_3\sdk\lib\msvc\oci.lib
      error adding symbols: File in wrong format
      collect2.exe: error: ld returned 1 exit status
    

    と出る。

    ググると同じところで躓いていた人がおり、スレッドを追っていくことで原因が「GCCのバージョンが古いこと」だということにようやく気が付いた。

    なので、SourceForgeのMinGW-w64 - for 32 and 64 bit WindowsにあるMinGW-w64のインストーラを使ってMinGWのツールチェインをインストールした。
    今回はversion 8.1.0を使い、フレーバーはインストーラのデフォルトと思しきPOSIX/sehにした。

    1-2. MinGWと同じツールチェインで作られているGTK+由来のpkg-configを追加する

    MinGWにはokg-configは含まれないので、前回記事で入れたgettext、glib、pkg-configをMinGWのインストール先に上書きで統合するだけ。

    1-3. oci8.pcをUnix形式のパスセパレータ表記で作成する

    パスセパレータについては前述のとおり変更済みなので、GCCを変更したことに対応させる。
    最終的にoci8.pcはこうなった。

      orasdk=C:/bin/instantclient_19_3/sdk
      gcc="C:/Program Files/mingw-w64/x86_64-8.1.0-posix-seh-rt_v6-rev0/mingw64"
    
      oralib=${orasdk}/lib/msvc
      orainclude=${orasdk}/include
    
      gcclib=${gcc}/lib
      gccinclude=${gcc}/include
    
      glib_genmarshal=glib-genmarshal
      gobject_query=gobject-query
      glib_mkenums=glib-mkenums
    
      Name: oci8
      Description: oci8 library
      Libs: -L${gcclib} -L${oralib} -loci
      Libs.private:
      Cflags: -I${orainclude} -I${gccinclude}
      Version: 19.3.0
    

    この状態でgo installしてみると、特にエラーアウトプットはなし。どうやらいけたらしい。
    Elwhjakuyaaahoa

    で、動作確認をしてみる。
    _example/dbms_outputは、発行したSQLの戻りに入っている文字列「hello」を表示するものなので、最終的なアウトプットに「hello」と出ていればいいはず。

      C:\Users\f97one\GOPATH\src\github.com\mattn\go-oci8\_example\dbms_output>set GO_OCI8_CONNECT_STRING=system/Orcl19cAdmin@//localhost:1521/ORCLCDB
      C:\Users\f97one\GOPATH\src\github.com\mattn\go-oci8\_example\dbms_output>go run -x main.go
      WORK=C:\Users\f97one\AppData\Local\Temp\go-build013517802
      mkdir -p $WORK\b001\
      cat >$WORK\b001\importcfg.link << 'EOF' # internal
      packagefile command-line-arguments=C:\Users\f97one\AppData\Local\go-build\58\5863c3505d965e6d20b9f8f592c519e220e5eed4b41183b13033d2e2374a1571-d
      packagefile database/sql=C:\Users\f97one\AppData\Local\go-build\62\628feb6160d48c5ea359bb61135be38aa5057fe7edbe4e99838bfb2c6089bbde-d
      packagefile fmt=c:\go\pkg\windows_amd64\fmt.a
      packagefile github.com/mattn/go-oci8=C:\Users\f97one\GOPATH\pkg\windows_amd64\github.com\mattn\go-oci8.a
      packagefile log=c:\go\pkg\windows_amd64\log.a
      packagefile os=c:\go\pkg\windows_amd64\os.a
      packagefile runtime=c:\go\pkg\windows_amd64\runtime.a
      packagefile context=c:\go\pkg\windows_amd64\context.a
      packagefile database/sql/driver=C:\Users\f97one\AppData\Local\go-build\bb\bbc0505de003812bb8e208b6f9c39c7ee40a4e3ade95c0d9d2db378042112a86-d
      packagefile errors=c:\go\pkg\windows_amd64\errors.a
      packagefile io=c:\go\pkg\windows_amd64\io.a
      packagefile reflect=c:\go\pkg\windows_amd64\reflect.a
      packagefile sort=c:\go\pkg\windows_amd64\sort.a
      packagefile strconv=c:\go\pkg\windows_amd64\strconv.a
      packagefile sync=c:\go\pkg\windows_amd64\sync.a
      packagefile sync/atomic=c:\go\pkg\windows_amd64\sync\atomic.a
      packagefile time=c:\go\pkg\windows_amd64\time.a
      packagefile unicode=c:\go\pkg\windows_amd64\unicode.a
      packagefile unicode/utf8=c:\go\pkg\windows_amd64\unicode\utf8.a
      packagefile internal/fmtsort=c:\go\pkg\windows_amd64\internal\fmtsort.a
      packagefile math=c:\go\pkg\windows_amd64\math.a
      packagefile bytes=c:\go\pkg\windows_amd64\bytes.a
      packagefile encoding/binary=c:\go\pkg\windows_amd64\encoding\binary.a
      packagefile io/ioutil=c:\go\pkg\windows_amd64\io\ioutil.a
      packagefile regexp=C:\Users\f97one\AppData\Local\go-build\d2\d2c97385147b6b2df852f8314ea0bc365ea5fd428c22cc28a7751caff46481e2-d
      packagefile strings=c:\go\pkg\windows_amd64\strings.a
      packagefile runtime/cgo=C:\Users\f97one\AppData\Local\go-build\05\050b0c52184a06b9e406b4908e746f8690cbeab9c2d8c629ec5358b2648f5d6f-d
      packagefile syscall=c:\go\pkg\windows_amd64\syscall.a
      packagefile internal/oserror=c:\go\pkg\windows_amd64\internal\oserror.a
      packagefile internal/poll=c:\go\pkg\windows_amd64\internal\poll.a
      packagefile internal/syscall/windows=c:\go\pkg\windows_amd64\internal\syscall\windows.a
      packagefile internal/testlog=c:\go\pkg\windows_amd64\internal\testlog.a
      packagefile unicode/utf16=c:\go\pkg\windows_amd64\unicode\utf16.a
      packagefile internal/bytealg=c:\go\pkg\windows_amd64\internal\bytealg.a
      packagefile internal/cpu=c:\go\pkg\windows_amd64\internal\cpu.a
      packagefile runtime/internal/atomic=c:\go\pkg\windows_amd64\runtime\internal\atomic.a
      packagefile runtime/internal/math=c:\go\pkg\windows_amd64\runtime\internal\math.a
      packagefile runtime/internal/sys=c:\go\pkg\windows_amd64\runtime\internal\sys.a
      packagefile internal/reflectlite=c:\go\pkg\windows_amd64\internal\reflectlite.a
      packagefile math/bits=c:\go\pkg\windows_amd64\math\bits.a
      packagefile internal/race=c:\go\pkg\windows_amd64\internal\race.a
      packagefile internal/syscall/windows/registry=c:\go\pkg\windows_amd64\internal\syscall\windows\registry.a
      packagefile path/filepath=c:\go\pkg\windows_amd64\path\filepath.a
      packagefile regexp/syntax=C:\Users\f97one\AppData\Local\go-build\09\09ed04a320a2025b1e0a177d0508fba26180bcc7e9a93f5b6b9a0215a3250c5a-d
      packagefile internal/syscall/windows/sysdll=c:\go\pkg\windows_amd64\internal\syscall\windows\sysdll.a
      EOF
      mkdir -p $WORK\b001\exe\
      cd .
      "c:\\go\\pkg\\tool\\windows_amd64\\link.exe" -o "C:\\Users\\f97one\\AppData\\Local\\Temp\\go-build013517802\\b001\\exe\\main.exe" -importcfg "C:\\Users\\f97one\\AppData\\Local\\Temp\\go-build013517802\\b001\\importcfg.link" -s -w -buildmode=exe -buildid=ZWzZXxPk1jGh5zBAmzMZ/JNrpGUoogY-8JjSab9MF/eSW2eCfLhSZIeYr8x4zF/ZWzZXxPk1jGh5zBAmzMZ -extld=gcc "C:\\Users\\f97one\\AppData\\Local\\go-build\\58\\5863c3505d965e6d20b9f8f592c519e220e5eed4b41183b13033d2e2374a1571-d"
      $WORK\b001\exe\main.exe
      hello
    
      C:\Users\f97one\GOPATH\src\github.com\mattn\go-oci8\_example\dbms_output>
    

    今度は大丈夫そうである。

    2. 終わりに

    蓋を開けてみれば、GCCさえちゃんとしていれば何の苦労もなかった、ということなんだが、MSYS2のpacmanが挙動不審になるのを皮切りに、いろいろ大周りをする羽目になった。

    2019年12月14日 (土)

    Windowsでmatn/go-oci8を使おうとしてとても苦労している話

    0. 唐突にOracle Databaseを使おうと思い立った

    Oracle Databaseを使ったシステムなど、お仕事くらいでしか使うことはなかったんだけど、Oracle Cloud Free Tierとして使えるDBのPaaSがOracle Databaseだけ、というので、かなり仕方なく使う、というネガティブな話だったりする。

    Javaや.NETには公式のデータベースドライバが出ているので、それらを使えば簡単にDBを利用できるけど、せっかくなので今回はGoでやってみることにした。

    なお、結論から書くとこの話、実現できてないorz

    1. 環境

    ビルドには以下が必要。

    • C/C++コンパイラ
    • pkg-config
    • プラットフォームとバージョンに応じたOracle InstantClient

    このほか、当然のことながらGitやGoが必要。

    1-1. MSYS2

    僕のWindows PC(Windows 10 pro version 1909)にはSphinxでPDFをビルドするため、MSYS由来のGNU Makeとshが入っていたので、GCCを入れるためpacmanを起動。
    しかしながら、なぜか何もフィードバックを出さずpacmanが途中で終了してしまい、パッケージのインストールもアップデートもできずハマる。

    おかしい、version 1809のころはふつうにpacmanが動いていたはずなのに。

    これでは何もできないので、MSYS2はあきらめることに。

    1-2. Cygwin

    ならば、ということでMSYS2をアンインストールしてCygwin x64を入れてみた。

    Baseパッケージグループのほか、makeやGCCをCygwin Installerで一とおりインストールし、C:\cygwin64\binなどをWindowsのPath環境変数に追加していった。

    Cygwin shellではなくDOS窓からgo get github.com/mattn/go-oci8とやったところ、

      # runtime/cgo
      gcc_libinit_windows.c: 関数 ‘x_cgo_sys_thread_create’ 内:
      gcc_libinit_windows.c:57:12: エラー: implicit declaration of function ‘_beginthread’; did you mean ‘OpenThread’? [-Werror=implicit-function-declaration]
        thandle = _beginthread(func, 0, arg);
                  ^~~~~~~~~~~~
                  OpenThread
      cc1: all warnings being treated as errors
      go: failed to remove work dir: GetFileInformationByHandle C:\cygwin64\tmp\go-build601027150\NUL: Incorrect function.
    

    と出てビルドできず。後でググると本体のGitHub WikiInstallFromSource

    Go does not support the Cygwin toolchain.

    などど書かれていた。なん....だと....?

    1-3. WSL

    最近のエディションにはWSL(Windows Subsystem for Linux)という、読んで字のごとくLinux互換環境を提供するサブシステムが標準搭載されている。デーモンを常駐させられない以外はほぼLinuxそのままなので、こいつで頑張ってみることに。

    僕は普段UbuntuのWSLを使っているけど、GCCもpkg-configもapt installで導入できるのでお手軽だ。

    ということでapt installでGCCやらpkg-configやらをインストールしてgo getしてみる。
    結果、エラー表示は出ず。

    いけた、のか? ということで付属のexampleで動作確認。

      $ cd $GOPATH/src/github.com/mattn/go-oci8/_example/nls
      $ GO_OCI8_CONNECT_STRING=system/Orcl19cAdmin@//localhost:1521/ORCLCDB go run main.go
      ORA-12571: TNS:packet writer failure
    

    パケット到達不能、だと....? まぁ、Oracle Databaseは親環境のDockerで動かしているとはいえ、WSLからDockerを制御できているので、親環境とは通信できているはずだが。

    てなわけで、この方法もダメということに。

    1-4. TDM-GCC

    Goの公式としてはTDM-GCCを推奨しているようなので、Cygwin GCCをアンインストールしてこちらに切り替えてみた。

    パッケージマネージャは付属していないので、pkg-configを自力でセットアップしなければならない。やり方自体はStackOverflowの記事を参考にやってみた。

    一応、直リンクを置いておく。

    準備自体はGistを残してくれている方がいたので、それを使わせてもらうことに。

      > go get github.com/mattn/go-oci8
      # pkg-config --cflags  -- oci8
      pkg-config: exit status 3221225595
    

    3221225595をWindowsの電卓に通すと0xC000007Bになるのだが、pkg-configが正常起動できていない、ということのようだ。どうやらABIが違うようなので、win64ではなくwin32に変えることで正常起動するようになった。

    で、再挑戦。

      > go get github.com/mattn/go-oci8
      # github.com/mattn/go-oci8
      In file included from GOPATH\src\github.com\mattn\go-oci8\cHelpers.go:3:0:
      ./oci8.go.h:1:17: fatal error: oci.h: No such file or directory
      compilation terminated.
    

    ....pkg-configがインクルードパスを正常に処理できていないようだ。もちろん、oci.hの場所は-Iオプションの行で指定しているんだが。

    おわりに

    最後のTDM-GCCパターンではあと一歩なような気もする。

    そんな気はするんだが、MySQLやPostgreSQLのドライバだったり、GoではなくJavaを選択していた場合だと、もっと簡単にデータアクセスを実装できていたであろう、と考えると、「なぜ僕はこんな苦行を....?」などと考えてしまう。

    «転職した

    2020年5月
              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            

    最近のコメント

    最近のトラックバック

    無料ブログはココログ