utamaro’s blog

誰かの役に立つ情報を発信するブログ

環境ごとに分けたtsconfigを作成する

環境ごとに分けたtsconfigを設定する

想定するのはdevelop環境とprod環境の2つの設定を分けて管理します。

dev環境では、sourceMap: trueで、prod環境ではsourceMap: falseとします。

製品として出すのに元のソースを出すのはかっこ悪いので。。。

devとprodの設定を作成する

tsconfig.dev.json

{
  "compilerOptions": {
    "sourceMap": true,
    "target": "es5",  // TSはECMAScript 5に変換
    "module": "es2015",  // TSのモジュールはES Modulesとして出力
    "moduleResolution": "node",  // node_modules からライブラリを読み込む
  }
}

tsconfig.prod.json

{
  "compilerOptions": {
    "sourceMap": false,
    "target": "es5",  // TSはECMAScript 5に変換
    "module": "es2015",  // TSのモジュールはES Modulesとして出力
    "moduleResolution": "node",  // node_modules からライブラリを読み込む
  }
}

webpackのdev設定を作成する

これについてはwebpack-mergeを使用して分けます。

一つのファイルで実現する方法が思いつきませんでした。

webpack.config.dev.jsを作成します。

// 開発用のコンフィグファイル

const merge = require('webpack-merge');
const baseConfig = require('./webpack.config.base');
// ↑ baseとなるconfigを読み込みます。

// webpack-base.config.jsに同様の設定値がある場合、こちらが優先される。
// baseの設定ファイルをマージします。マージにはwebpack-mergeというライブラリを使ってます。
const devConfig = merge(baseConfig, {
    devtool: 'inline-source-map',
});
devConfig.module.rules.push({
    // 拡張子 .ts の場合
    test: /\.ts$/,
    // TypeScript をコンパイルする
    loader: "ts-loader",
    options: {
        configFile: 'tsconfig.dev.json'
    }
})

module.exports = devConfig;

ここで大事なのは、devConfig.module.rulespushしていることです。

ruleを上書きしないようにしています。

次に、webpack.config.base.jsを作成します。

const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const jsRoot = './js';

function makePath(...paths) {
    return path.resolve(__dirname, paths.join('/'));
}

const baseConfig = {
    // エントリーポイントの設定
    entry: {
        'reset': makePath(jsRoot, 'reset.js'),
    },
    // 出力の設定
    output: {
        // 出力するファイル名
        filename: '[name].bundle.js',
        // 出力パス
        path: `${__dirname}/../static/js`,
        publicPath: '/static/js/'
    },
    module: {
        rules: [
          // いろいろな設定を追加する。
        ]
    },
    resolve: {
        extensions: [".ts", ".js"]
    },
    target: 'electron-main',
    plugins: [
        new CleanWebpackPlugin({
            cleanOnceBeforeBuildPatterns: `${__dirname}/static/js/*`
        }),
    ]
};

module.exports = baseConfig;

package.jsonのscriptを作成する

これについてはwebpackを使っている方にとってはよく見るスクリプトかと思います。

{
  "scripts": {
    "webpack-dev": "webpack --config ./webpack.config.dev.js --mode development --progress --display-error-details",
    "webpack-production": "webpack --config ./webpack.config.production.js --mode production --progress --display-error-details"
  }
}

実行する

npm run webpack-devで実行すると、.ts.jsで出力されます。

当初はtscを使うのかと思ってました。

mysqlclientを使ったらdyldエラーが起きた

from MySQLdb import _mysqlが書かれたpythonファイルを実行したら以下のエラーが発生しました。

Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import MySQLdb as _mysql
  File "/lib/python3.8/site-packages/MySQLdb/__init__.py", line 18, in <module>
    from . import _mysql
ImportError: dlopen(/lib/python3.8/site-packages/MySQLdb/_mysql.cpython-38-darwin.so, 2): Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib
  Referenced from: /usr/local/opt/mysql/lib/libmysqlclient.21.dylib
  Reason: image not found

このエラーによってプログラミング時間として用意していた夜の3時間と、朝の2時間が溶けました。

なんでこんなエラーが起きたのかわからなかったですし、初めての経験だったのであたふたしました。

原因について

brewでインストールしたopensslをswitchすることで解決したので、brewに関係があると思います。

エラー内容にもある通り、opensslが問題だったのかと思います。

今回はswitchで解決したので、依存関係の問題だったのでしょう。

ちなみに、エラーが起きたときにインストールしていたopensslについては以下のバージョンがインストールされていました。

  • openssl/
  • openssl@1.1/

このときwhichコマンドを使って参照先を確認したところ、以下が出力されました。

/usr/local/opt/openssl@1.1/bin/openssl

そして、openssl@1.1には当然ながらlibssl.1.0.0.dylibはありません。

なので、Library not loaded:と言われたのだと思います。

解決方法

使用しているバージョンに誤りがあると考えたので、以下のコマンドを実行しました。

brew switch openssl 1.0.2r

これだけで解決しました。

悩んでいた時間が5時間で、解決が1分未満という。。。

なんといいますか、気分が上がったあとに、疲れて萎えました。

stackoverflowの以下の回答を見て解決方法を思いつきました。ありがとうございます。

https://stackoverflow.com/questions/59006602/dyld-library-not-loaded-usr-local-opt-openssl-lib-libssl-1-0-0-dylib#answer-59184347

最後に

ちなみにですが、コマンドを実行する前に以下のコマンドを実行していました。

brew switch openssl 1.0

結果は

Error: openssl does not have a version "1.0" in the Cellar.
openssl installed versions: 1.0.2q, 1.0.2r

そんなバージョンは無いよ!と。 1.0.2q1.0.2rがインストールされてるよ!と。

なんという親切さ。

ありがとうございました。

webpack4とelectronでfsモジュールが見つからないエラーの解決方法について

webpack4とelectronでfsモジュールが見つからない問題

webpack4とelectronを使って開発している場合にfsモジュールが見つからないエラーが出てきました。

まずfsモジュールについてですが、これはファイルシステムを使用する際に利用するものです。

fs.readFile()といったように使用するものです。

webpack4でビルドを実行した際に以下のようなエラーとして出力されます。

ERROR in ./node_modules/electron/index.js
Module not found: Error: Can't resolve 'fs' in './node_modules/electron'
resolve 'fs' in './node_modules/electron'
  Parsed request is a module
  using description file: ./node_modules/electron/package.json (relative path: .)
    Field 'browser' doesn't contain a valid alias configuration

この解決方法について試したことを記事にします。

調査1

まず最初に以下のような対応を見つけました。

module.exports = {
  node: {
    fs: "empty"
  }
}

webpack.config.jsに以上の設定を追加することでエラーが消えるという対応です。

この対処の場合、nodefsモジュールをからのオブジェクトで置き換えることになります。

https://webpack.js.org/configuration/node/#node

この問題点はいざfsモジュールを使用した際にからのオブジェクトになっているので利用できなくなるということです。

いざ使う際に別のモジュールを取り込んで開発した場合、また別の問題が出てくる可能性があります。 そして作業が止まります。

使えるものは最初から使えたほうが良いでしょう。

調査2

次に以下のような対応を見つけました。

module.exports = {
  target: 'node',
}

webpack.config.jsに以上の設定を追加するとエラーが消えるというものです。

targetnodeとすることでエラーが消えます。

これはnodejsを使った環境で使用する場合に設定する値です。

サーバーサイドプログラミングにおいてはこれを設定しても良いと思いますが、electronにおいては間違いかと思います。

electronはあくまでもフロントエンドのアプリケーションなので。

ということでこの対応方法は設定しないほうが良いでしょう。

解決方法

たどり着いた解決方法です。

参考になった記事がこちらです。

https://blog.mbaas.nifcloud.com/entry/2019/04/10/103228

この記事中の以下の部分がヒントになりました。

さらに Module not found: Error: Can't resolve 'fs' というエラーも出ます。 これはビルド環境はNode.jsながら、対象がWebブラウザであるために発生するエラーです

ということで、webpack.config.jsに以下の設定を追加します。

module.exports = {
  target: 'electron-main',
}

つまり、electronようにコンパイルするようにするということです。

設定値については以下のページに載っていました。

https://webpack.js.org/configuration/target/#string

ちなみにですが、webを設定しても解決するのではと考えたのですが、解決しませんでした。

最後に

エラー内容で検索したら解決方法は出るのですが、なぜその解決方法なのかも調べたほうが良いと思いました。

  1. エラー内容で検索する
  2. 解決方法を見つける
  3. 解決方法で不明な点を調べる
  4. 現状を解決するのに適切な解決方法かを検討する
  5. 解決方法を取り込む

何時間も詰まった場合は以下のようにする。

  1. 現状使っているライブラリのドキュメントを読み込む

以上です。

jwtを検証するアノテーションを作って認証済みか判定する

jwtを検証するアノテーションを作って認証済みか判定する

jwtをcookieに追加してAPIの実行を管理したいと思いました。 そのときにやった内容です。

ざっと説明すると、ログインしていたらjwtがcookieに入っていて、そのcookieを検証することで認証を管理しています。 少々めんどくさい実装になってしまったので改善が必要ですが、アノテーションの自作や、interceptor、cookieの取り扱いなど勉強になることが多々ありました。

@VerifyJwtというアノテーションを付けることで検証が可能になります。

WebMvcConfigurerの設定を行う

WebMvcConfigurerの設定を行います。 interceptorの適用範囲を定義しています。 この定義によってアノテーションを適用する範囲を決めています。

import com.xxx.xxx.settings.interceptor.LoginRequiredInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginRequiredInterceptor loginRequiredInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginRequiredInterceptor)
                .addPathPatterns("/admin/**") // 適用対象のパス(パターン)を指定する
                .excludePathPatterns("/error")
                .excludePathPatterns("/static/**"); // 除外するパス(パターン)を指定する
    }
}

この設定によって/admin/**に対してアノテーションを有効にしています。 また、エラー時、静的ファイル要求時にinterceptorが無効になるように設定しています。 静的ファイルで無効にする方法として、interceptor内での実装方法もあるがこちらのほうが楽に設定できます。 また、有効時、無効時のパスがまとまるので見通しが良くなります。

LoginRequiredInterceptorの実装を行う

先程作成したWebMvcConfigで使用したloginRequiredInterceptorを作成します。 preHandleを実装してアノテーションのついている場合の処理を作成します。

このクラス内では以下のクラスの実装が必要です。

  • com.xxx.xxx.apps.admin.user.model.UserModel
  • com.xxx.xxx.apps.admin.user.service.UserService
  • com.xxx.xxx.jwt.model.JwtUserClaimModel
  • com.xxx.xxx.jwt.service.JwtService
  • com.xxx.xxx.settings.annotation.VerifyJwt
import com.xxx.xxx.apps.admin.user.model.UserModel;
import com.xxx.xxx.apps.admin.user.service.UserService;
import com.xxx.xxx.jwt.model.JwtUserClaimModel;
import com.xxx.xxx.jwt.service.JwtService;
import com.xxx.xxx.settings.annotation.VerifyJwt;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Objects;

@Slf4j
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtService jwtService;

    @Autowired
    private UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Method method = ((HandlerMethod) handler).getMethod();
        VerifyJwt verifyJwtAnnotation = AnnotationUtils.findAnnotation(method, VerifyJwt.class);

        if (Objects.nonNull(verifyJwtAnnotation)) {
            log.info("do login required.");
            // ログインが必要で、jwtの検証が必要な場合の処理を行う
            String token = jwtService.takeJwt(request);
            if (Objects.nonNull(token)) {
                // tokenの検証を行う
                JwtUserClaimModel claimModel = jwtService.verify(token, JwtUserClaimModel.class);
                if (Objects.isNull(claimModel)) {
                    // トークンの検証が失敗した場合の処理。
                    if (StringUtils.isBlank(verifyJwtAnnotation.failureUrl())) {
                        return true;
                    } else {
                        response.sendRedirect(verifyJwtAnnotation.failureUrl());
                        return true;
                    }
                } else {
                    UserModel userModel = userService.fetchUser(claimModel.getUserId());
                    request.setAttribute("user", userModel);
                    if (StringUtils.isBlank(verifyJwtAnnotation.successUrl())) {
                        // 例えば、/adminが成功時に表示する画面としたときに、ログイン済みの状態で/adminにリクエストした場合、
                        // 成功時の画面に/adminを設定しているとリダイレクトし続けてしまう。
                        return true;
                    } else {
                        // 成功時のurlが設定されていればリダイレクトを行う。
                        response.sendRedirect(verifyJwtAnnotation.successUrl());
                        return true;
                    }
                }
            } else {
                if (StringUtils.isBlank(verifyJwtAnnotation.failureUrl())) {
                    return true;
                } else {
                    response.sendRedirect(verifyJwtAnnotation.failureUrl());
                    return true;
                }
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

正直ネストが深くなりすぎてる感があるので、だめだなぁと思ってます。

UserModelの実装を行う

toStringしたときにpasswordが表示されないようにすること。 これが意外と大事で、ログを出していたりすると忘れることがよくあります。

import lombok.Data;
import lombok.ToString;

import java.time.LocalDateTime;

@Data
public class UserModel {
    private int id;
    private String email;
    @ToString.Exclude
    private String password;
    private boolean isActive;
    private boolean isAdmin;
    private boolean isGeneral;
    private LocalDateTime modifiedAt;
    private LocalDateTime createdAt;
}

UserServiceの実装を行う

mapperの実装については省略します。 userIdemailを利用してデータの存在確認を行います。 今回の場合は、jwt内にuserIdを入れており、そのIDを使用してデータを取得しています。 本来はここでnullチェックが必要ですが、省略します。

import com.xxx.xxx.apps.admin.user.mapper.UserMapper;
import com.xxx.xxx.apps.admin.user.model.UserModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public UserModel fetchUser(long userId) {
        UserModel userModel = userMapper.selectUserById(userId);
        return userModel;
    }
}

JwtUserClaimModelを実装する

jwtに含めるデータを定義します。

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class JwtUserClaimModel {
    private long userId;
    private boolean isActive;
    private boolean isAdmin;
    private boolean isGeneral;
}

JwtServiceを実装する

このServiceを利用してjwtの生成、削除、検証、cookieへの追加が可能となります。 Cookieの追加についてはjwtとは別の機能なので、utilクラスを用意するのが良いと思います。

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.xxx.xxx.settings.config.CommonPropertyConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.TimeZone;

@Slf4j
@Service
public class JwtService {

    @Autowired
    private CommonPropertyConfig commonPropertyConfig;

    public String takeJwt(HttpServletRequest httpServletRequest) {
        Cookie[] cookies = httpServletRequest.getCookies();
        if (Objects.nonNull(cookies)) {
            for (Cookie cookie : cookies) {
                if (commonPropertyConfig.getJwtCookieKey().equals(cookie.getName())) {
                    String token = cookie.getValue();
                    log.debug("jwt found in cookies. token=[{}]", token);
                    return token;
                }
            }
        }
        log.debug("jwt token not found.");
        return null;
    }

    public String create(Object headerClaimModel) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(commonPropertyConfig.getJwtSecretKey());
            Map<String, Object> headerClaims = new HashMap<>();
            headerClaims.put("appName", headerClaimModel);
            Calendar calendar = Calendar.getInstance();
            calendar.add(Calendar.HOUR, 6);
            calendar.setTimeZone(TimeZone.getTimeZone("UTC"));
            Date expiredAt = calendar.getTime();
            String token = JWT.create()
                    .withIssuer(commonPropertyConfig.getJwtIssuser())
                    .withHeader(headerClaims)
                    .withExpiresAt(expiredAt)
                    .sign(algorithm);
            log.debug(token);
            return token;
        } catch (JWTCreationException exception){
            //Invalid Signing configuration / Couldn't convert Claims.
            throw exception;
        }
    }

    public <T> T verify(@NotNull String token, Class<T> claimClass) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(commonPropertyConfig.getJwtSecretKey());
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(commonPropertyConfig.getJwtIssuser())
                    .build(); //Reusable verifier instance
            DecodedJWT jwt = verifier.verify(token);
            Claim claim = jwt.getHeaderClaim("appName");
            T claimModel = claim.as(claimClass);
            return claimModel;
        } catch (JWTDecodeException exception){
            //Invalid token
            log.warn("Invalid token. token=[{}]", token);
        } catch (JWTVerificationException exception){
            //Invalid signature/claims
            log.info("Invalid signature/claims token=[{}]", token);
        }
        return null;
    }
}

VerifyJwtアノテーションを実装する

successUrlとfailureUrlを用意したアノテーションを作成します。 ランタイムで動作し、メソッドに対して利用できます。

import javax.validation.constraints.NotBlank;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface VerifyJwt {
    @NotBlank String successUrl() default "";
    @NotBlank String failureUrl() default "";
}

controllerクラスでアノテーションを利用する

すべての準備が整いました。実際に利用します。

    @VerifyJwt(successUrl = "/admin")
    @RequestMapping(path = "/login")
    public String loginView(HttpServletRequest request, HttpServletResponse response) throws IOException {
        return "admin/login";
    }

    @RequestMapping(path = "/login", method = RequestMethod.POST)
    public String postLogin(@Validated LoginRequestParam param,
                            HttpServletRequest httpServletRequest,
                            HttpServletResponse httpServletResponse,
                            BindingResult bindingResult) {
        // bindingResultのエラーチェックを行うこと。今回は省いている。
        LoginDto dto = LoginDto.builder()
                .email(param.getEmail())
                .password(param.getPassword())
                .build();
        // アカウントの認証を行う。
        UserModel userModel = loginUserService.userExists(dto);
        if (Objects.nonNull(userModel)) {
            // ユーザーが存在するため、tokenを生成する
            JwtUserClaimModel claimModel = JwtUserClaimModel.builder()
                    .userId(userModel.getId())
                    .isActive(userModel.isActive())
                    .isAdmin(userModel.isAdmin())
                    .isGeneral(userModel.isGeneral())
                    .build();
            String token = jwtService.create(claimModel);
            Cookie cookie = new Cookie(commonPropertyConfig.getJwtCookieKey(), token);
            cookie.setMaxAge(commonPropertyConfig.getJwtCookieMaxAge());
            cookie.setPath("/");
            if ("https".equals(httpServletRequest.getScheme())) {
                cookie.setSecure(true);
            }
            cookie.setHttpOnly(true);
            httpServletResponse.addCookie(cookie);
            return "redirect:";
        } else {
            // 管理ページでのアカウント登録は行わないため、登録画面へのリダイレクトは行わない。
        }
        return "admin/login";
    }

    @VerifyJwt(failureUrl = "/admin/login")
    @RequestMapping()
    public String homeView(HttpServletRequest request) {
        // resources/templates/login.htmlが開かれます。
        return "admin/home";
    }

問題点

@VerifyJwtすごく便利!

と作ったときは思いました。

最終的にこのアノテーションは使っていません。

なぜかというと、アノテーションを付け忘れてバグとなるからです。 APIを作成するごとにつける必要があって面倒なのです。

本来はアノテーションを使用せずに同様の機能を作成するのが良かったのだと思います。

機会があれば改良して、もっと使いやすいものを作成したいと思います。

テレビニュースで情報収集しなくなった理由

僕はテレビを使って情報収集するのをやめました。

やめた理由はいくつかあるのですが、一番の決めては「情報の検証が面倒」だからです。

テレビニュースの収入源はCMの広告費です。

テレビの合間に流れる邪魔な広告が大部分の収入源となっています。

テレビ局は視聴率を稼ぎ、より広告枠の価値を上げることによって収益に反映させる構造です。

とすると、テレビが扱う情報は確実な情報である必要がなくなります。 面白さや、インパクトを重視した見た目や、お涙頂戴など人の感情に訴えかけるように編集しています。

京都アニメーション実名報道事件とか、あれって炎上商法じゃないですかね? 炎上させることでニュースを見させ、視聴率を稼ぐ。 実名報道はニュースを見させる、関心を呼ぶための餌として使用する(偏見でしょうか?)

視聴率をとれないニュースは報道しないし、テレビ局にとって不利になることは報道しないんです。 それをすると視聴率が稼げないから。 それをすると広告主が離れていくから。 それをするとお金が入ってこなくなるから。

テレビ局が発信する情報は、調べるきっかけとはなりますが確かな情報とは言いづらいです。 根底が視聴率となっているため、若干の誇張表現が混じっている可能性を否定できません。

中学生の頃に「ネットで情報を調べるときは3つ以上の一致する情報を見つけること」と習いました。 テレビも同じではないでしょうか? テレビで流れているニュースの裏付けをして、その情報が正しいかどうか確認する必要があるでしょう。

でも、調べるのってかなり面倒なんですよ…

耳から聞いた内容をすべて覚えていられるわけではないです。 かといってすべてをメモするのも難しい。

間違った情報が出た際に「〜の箇所は正しくはXXでした。お詫びして訂正致します」とサラッと言われると聞き逃すよね。 「いつのどの場面よ!」ってなります。しかも一回しか言わないし。

常日頃からそんなことをしてたら疲弊してしまいます。 倒れますよ。

ネット上で調べるのならば適宜メモしつつ、わからない箇所は再検索できます。 情報を整理しつつ、重要な単語はコピペすることも可能です。 耳で聞いてメモするという作業がなくなるため、調べ物をするのに手間がかからなくなるんです。

これがテレビを使って情報収集するのをやめた理由です。

まとめると、「メモするのが面倒だからネットで調べ物する」という理由です。

JavaのOptionalを戻り値に使用することについて消極的な理由

僕は戻り値にOptionalを使用することに消極的です。

戻り値に使用する理由に対して以下の理由をよく聞きます。

  • メソッドを利用した際にnull値を返す可能性を明示する。
  • nullを考慮したコーディングを強制させる。

これらをやる理由が無いと考えているからです。

メソッドを利用した際にnull値を返す可能性を明示する。

これ本当に明示できているのでしょうか? Optionalだから明示できているのでしょうか? Objectでもnullが入る可能性があるのだから、Objectでもnullを返す可能性は考えられるのでは無いでしょうか?

以下の条件で使い分けるのだとするなら、Optionalを使用する利点が出てくると思います。

  • Optionalを返すメソッドではnullを返す可能性がある。
  • Optionalを返さないメソッドはnullを返す可能性が無い。

ただし、戻り値から値を取り出す場合にコーディングに差異が出てきます。

idと一致するオプションが無い場合Optionalを返すメソッドを例とします。

public Optional<OptionDetail> fetch(Integer id) {
    if (id == 1) {
        OptionDetail optionDetail = new OptionDetail().setId(id).setName("SAMPLE");
        return Optional.of(optionDetail);
    }
    return Optional.ofNullable(null);
}

このとき呼び出し元は以下のように実装します。

OptionDetail optionDetailA = fetchDetailA(1)
        .orElseThrow(() -> {
            System.out.println("log message");
            throw new IllegalArgumentException("invalid id.");
        });
optionDetailA.getId();

データがない場合はログを出して、例外を出して処理を終えます。 .get()orElse(T)を利用しないのは、nullというのは、データがない状態を示しているからです。 データが無いのに、その場合の値を置き換えるとかおかしいですよね? .get()を使用した場合、値がnullだった場合にNoSuchElementExceptionが発生します。

次にOptionalを使用しない場合です。

public OptionDetail fetchDetailB(Integer id) {
    if (id == 1) {
        OptionDetail optionDetail = new OptionDetail().setId(id).setName("SAMPLE");
        return optionDetail;
    }
    return null;
}

このとき呼び出し元は以下のように実装します。

OptionDetail optionDetailB = fetchDetailB(1);
if (Objects.isNull(optionDetailB)) {
    System.out.println("log message");
    throw new IllegalArgumentException("invalid id.");
}
optionDetailB.getId();

見ればわかると思うのですが、対して違いが無いんです。

個人的には戻り値が全部Optional<T>となっているよりObjectとなっていた方が良いと思ったり、でもいちいちif文を書くの面倒だなとも思います。

そもそもを語るのなら、nullを返すメソッドはNullPointerExceptionを発生させる可能性があるため避けるべきです。 それを考慮した場合のメソッドは以下のようになるでしょう。

public OptionDetail fetchDetailC(Integer id) throws IllegalArgumentException {
    if (id == 1) {
        OptionDetail optionDetail = new OptionDetail().setId(id).setName("SAMPLE");
        return optionDetail;
    }
    throw new IllegalArgumentException("invalid id.");
}

データがない場合に例外を出します。 「存在しないデータを取得しようとした」という意味です。

利用側では以下の実装になります。

OptionDetail optionDetailC;
try {
    optionDetailC = fetchDetailC(1);
} catch (IllegalArgumentException e) {
    System.out.println("log message");
    throw e;
}
optionDetailC.getId();

Optionalの場合も同じ様な実装になります。

OptionDetail optionDetailD;
try {
    optionDetailD = fetchDetailD(1).get();
} catch (IllegalArgumentException e) {
    System.out.println("log message");
    throw e;
}
optionDetailD.getId();

どちらも同じ様な実装になるので、どっちでもいいんですよ。

ただ、Optionalの場合はorElseorElseThrowを利用できないので利用する必要性がありません。 また、fetchしてから一度.get()しないとなりません。 なにかデータを取得するメソッドを実行しているのに、なぜ何かを取得するメソッド(.get())を再度実行するのでしょうか?

IDEによってはisPresent()を利用してくださいと警告がでて混乱する可能性があるので使わないほうが良いでしょう。

これを考えると、この場合はOptionalを使わない方がマシです。

結論

  • Optionalの利用云々によってコーディングは対して違いがない。
  • nullを返さない実装の場合はOptionalは処理を煩雑にするため使用しない。
  • そもそもnullを返す実装を疑い、メソッドの利便性を考えたときにOptionalを使うメリットは小さい

nullを考慮したコーディングを強制させる。

本当にnullを考慮したコーディングを強制させるのでしょうか?

僕はそんなことは無いんじゃないかと思ってます。

.get()した場合try-catchを書かなくてもコンパイルは通ります。 そのままデプロイした場合NoSuchElementExceptionが発生する可能性があります。 それはNullPointerExceptionと何が違うのでしょうか。

Optionalを返すということはnullを返す可能性があると考えられるんだ!」

「だから.get()ではくorElse()orElseThrow()を使用しろ!」

orElseを利用するなら、なぜデフォルト値をメソッド側が返さないのでしょうか? orElseThrowを利用するなら、なぜメソッド側でIllegalArgumentExceptionなどの例外を出さないのでしょうか?

それはOptionalを利用するメリットかもしれませんが、Optionalを使わなければならない理由にはなりません。

だからOptionalについての議論が起こったりするんでしょうね。

nullを返すことに理由がある場合、なぜnullを返すのかjavadocにかかれているはずです。 そうなるとnullを返す可能性があるからと言ってjavadocすら確認しないということは無いでしょう。 そこに重要な情報が書かれていた場合のことを考えると、戻り値だけを見てコーディングするのは危険です。

(処理内容をコードから読み解く必要性はないです。必要がある場合はメソッド名が明確ではありません。)

また、nullを返すことに理由がない場合はそのメソッドの見直しが必要です。 理由のないnull返却は開発者を混乱させます。

最終的には利用するメソッドがどんな条件でnullや例外を出すのか確認して実装することになると思います。

そうなった場合、コーディング時のメソッドの使い方を確認するコストに差異はありません。 そして、それを確認したときOptionalを使用していなくても、エンジニアはそれに適した実装をします。

結論

  • nullを考慮したコーディングを強制するわけではない。
  • Optionalを利用してもしなくてもコーディング時にやることに変わりはない。

最後に

以上の理由から僕はOptionalを利用することに消極的です。

Optionalはメソッドを利用する際の処理を煩雑にしますし、実装方法によっては開発者を混乱させます。

また、javaの場合古い書き方に慣れている人のほうが絶対数が多く、Optionalの実装になれていない可能性があります。 チーム内で使い方の議論をして時間を浪費するぐらいなら使わない方が良いです。

そんなことよりも、よりよい機能について提案したり、リファクタリング作業や、業務の効率化にリソースを使ったほうが良いです。

Optionalの利用について議論をしていたり、考えている方の参考になれば幸いです。

試行錯誤しながら転職に成功したときのはなし

はじめに

転職活動をする際に自分の入りたい企業を探して、アタックをすると思います。 アタックするのは1回しかチャンスがなく、落ちた直後に再アタックしても落ちる可能性が高いです。

そんなときに、1回のチャンスをどのように成功させるのか。 僕の実体験から記事にしたいと思います。

※ いろんな事情から会社名については隠したいと思います。

僕の転職活動は少しばかり変わっていて、以下のことをしています。

  1. GreenとBizreachのみを使用する
  2. スカウトしか見ない(Greenの場合は気になるか、メールです)
  3. こちらから企業を探したりはしない

以上です。 向き不向きがあると思うので参考までに。

理由としては、やりたいことが決まっていなくて、開発だけしたいエンジニアだったからです。

こういう考えの方は結構多いのではないでしょうか?

多くの企業の中からメリット・デメリットをまとめて応募するのは探すのに多大なコストが掛かります。 そのコストを軽減しつつ、積極的に採用活動をしている企業の中から面白そうな事業をしているのを探すというやり方です。

プロフィール

僕はウェブエンジニアです。

3年の業務経験のあるサーバーサイド開発エンジニアです。

専門はjava言語で、pythonrubymysql、フロント開発ができます。

休日はプログラムを書いたりしていて外に出ることは稀です。 出るとしたら買い出しのときか、公園で筋トレする場合だけです。

僕のIQは110~120ぐらいなので、世間的に一般的なレベルだと思います。

書類選考で2社落ちた

職務経歴書が悪かったのか、履歴書が悪かったのか、もしくはどちらも論外だったのかはわかりません。

書類選考を通過できるようになるまでのステップについて、試したことをまとめます。

1社目: 職務経歴書、履歴書を自分で考えて書く。落ちた。
2社目: 職務経歴書の問題点について考えて改善したもとを提出した。落ちた
3社目: 職務経歴書は及第点と仮定して、履歴書を改善した。通過した。

3社と面談して、書類結果をもらうまでに3週間ぐらいかかってます。

とても面倒でした。

面談日の調整で3日ぐらいの候補日を提案して、返事をもらえるのが次の日です。 それを1週間続けて3社の予定を入れました。

結論としては、PDCAサイクルを回すことで効率に対策が可能ということです。

  1. plan: 書類通過するまでの計画を練る
  2. do: 計画を実行する
  3. check: 実行した結果を評価する
  4. action: 評価結果から改善事項をみつける

職務経歴書を書きます。

落ちた職務経歴書を見直しました。

すると

職務経歴書に自分の経歴書が書かれていなかったのです。 どういうことかというと、自分の強みや仕事をするモチベーションなどを書いていたのです。

例としては↓のような感じです。(もう少し具体的に書いてましたが、雰囲気はこんな感じです)

私はXを3年やってきて、趣味でもYをやってきました。
開発時には再利用性や運用を考えながら開発しています。

これを書いていたときは、書類選考で落ちました。

1社目はなんで落ちたのかわからなくて、2社目のときに同じものを面談時に出して指摘があるか聞きました。

職務経歴書です。質問ありますか?」

「職歴とか会社で何をやったのかは書いてあるけど、読まないとわからん。」

ここで「なるほどな」と。 具体的な回答はもらっていないですが、おそらく↓かと思います。

「その人がいつ、どこで、どのようなことをやってきたのかがまとまっている、見やすい資料が欲しい。」

採用担当者からしたら、自分の気持ちとか、考え方とかいらないのです。 何ができるか、何をやってきたのかが知りたいのだと思います。

ちなみに検証のために、一回修正して出しました。 その後、修正した職務経歴書で書類選考は通ったので、考え的にはあっていたのだと思います。 とすると、2社目は履歴書で落ちたことになります。

履歴書を書きます。

1社目は、「開発者がやっている取り組みに対して共感して開発に関わりたいと考えたから」という理由です。 職務経歴書がダメダメだったので、この理由があっているのかわかりませんでした。

2社目でも同じ理由で出したところ、落ちました。 2社目は職務経歴書を修正していたので、職務経歴書については及第点と仮定します。 すると履歴書が間違っていることになります。

ということで、履歴書を修正して3社目にアタックです。

3社目では、「企業の目的に共感した」という理由で応募しました。 なぜ共感したかという具体的な理由は必要です。

例えば、

Xという目標に共感して応募しました。
私は過去Yということを体験しており、Xができれば私のような経験をする人が少なくなる(以下略)

雑ですが、こんなです。

どこかで拾ってきたような文章や、無理に敬語で書く必要も無いと思います。 それで落ちませんでしたし。

※ ビジネスメールを書くレベルで良いと思います。

ようやく書類選考に通過しました。

企業のビジョンに共感していることと、共感した理由が自分の言葉で書かれていることが重要なのだと思います。

おそらく、「ゲームを作りたいので入りたいです。売上がXで潰れづらいので応募しました。」だと落ちると思います。

「小学生のころからプレイしており、思い出があります。特に〜の場面がとても熱くなりました。僕は昔から〜が好きで(以下略)」だと通過するかもしれません。

(書類選考で3社…。先は長い…)

まとめると

書類選考では以下のことを気をつけます。

  1. 職務経歴書はいつ、どこで、何をやったのか、何ができるかを見やすくまとめること。
  2. 履歴書は企業の目的に共感したという理由にすること。
  3. 履歴書に共感した理由を具体的に書くこと。

これを意識して作成したところ、書類選考に通るようになりました。

書類選考だけで2社落ちるとか、転職とは大変です。。。 何が大変って、面談だけはやっているので時間作って行かないとならないということです。

面接に慣れるために2社で練習する

どんなことにも対策というのは必要です。

面接の時にどんなことを聞かれるのか、どんな返しをすると良いかなどです。 2社はそれを調べるために使いました。

どちらも2次選考で落ちてしまいました。 というのも、練習ということで気持ちが入らなくて、別に行きたくもない会社というのがバレたのだと思います。

結果としては以下のことができればよいのだと思います。

コミュニケーション能力

不思議なことに技術的な能力を聞かれることはありませんでした。 これは実務で3年経験があるからだと思います。

ということで、実際に働いたときに社員との連携が可能かどうかをみているようでした。 「何かを作る際に必要なことは?」や「開発時に一番気をつけないといけないことは?」などを聞かれたりします。 あと「好きな言語は?」というのもありました。

これに対して論理的な回答ができるかどうかだと思います。 あと、笑顔を作るというのと、冗談を交えて笑い声を混ぜるということです。 評価するのは目の前の人なので、良い印象を与えたほうが得です。なので笑顔と笑い声です。 笑顔で話す人って話しやすいな~と思うことがあったので、実践しました。

また、面談時と面接時に必ず聞かれた質問がありました。

  1. 自己紹介をお願いします。
  2. 弊社の選考を受けた理由は?
  3. 転職する際の企業選びの基準は?

3番については1社目で聞かれました。 2社目で回答を修正して、3も絡めたところ質問はありませんでした。 これについては履歴書に書いた内容をいうだけで良いと思います。

不思議と「他の会社と比較してどう?」は聞かれませんでした。 ちなみに僕ならこのように答えると思います。

「ぶっちゃけると、他の会社とは比較していません。世の中に多くの会社があり、そのすべてを比較検討することは時間的に難しいからです。そのなかでスカウトをしてまで話を聞きたいと言っていただいたことが何よりも嬉しかったので面談しました。そのときに、〜について聞いて、自分のやりたいことに一致していたので選考に進みました。」

こう答えると「ちなみにやりたいことってのは?」と聞かれるので、その企業の理念に絡めてやりたいことを答えます。 曖昧な箇所を一箇所残して、回答すると質問を誘導できます。 これおすすめです。

まずは結論から話して、なぜその結論なのかを話すというのが大事です。 ちなみに、僕は敬語が苦手です。

一人称として「僕は」とか言いますし、「めっちゃ嬉しいです」とか「〜ができたらやばいですよねw」とか言ってました。 「やばい」っていうのは流石に「具体的にどうやばい?」って聞かれましたが(汗

ここで論理的に回答できると加点させるはずです。

いざ本番へ

本番として、スカウトの来た2社に行きたいと考えたました。

結論はどちらも内定をいただくことができました。 とてもありがたいことです。

練習時に得た経験を反映させた結果だと思います。 書類選考は難なくクリアして、面接も突破しました。

どちらの企業も僕がやりたいことをやっていたので、どちらを選んでも楽しく開発できそうでした。 最終的には年齢がまだ若いので、経験を積むということを考えて、より経験を積める会社を選択しました。

以上です。

転職時の参考になると嬉しいです。