LoginSignup
0
1

More than 3 years have passed since last update.

スタートアップ企業のJavaサーバで見られる混乱について

Last updated at Posted at 2020-09-18

この記事では、スタートアップ企業のJavaサーバで見られる混乱を招く状況について、これらのサービスをより良い方向に向けるためのいくつかの方法を見ていきます。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

Controller Base ClassとService Base Classを使用

ベースクラス(Base Class)の紹介

Controller Base Class(コントローラベースクラス)

/** Controller Base Classes */
public class BaseController {    
    /** Injection services related */
    /** User Service */
    @Autowired
    protected UserService userService;
    ...

    /** Static constant correlation */
    /** Phone number mode */
    protected static final String PHONE_PATTERN = "/^[1]([3-9])[0-9]{9}$/";
    ...

    /** Static function related */
    /** Verify phone number */
    protected static vaildPhone(String phone) {...}
    ...
}

共通のコントローラベースクラスには、主にインジェクションサービス、静的定数、静的関数が含まれており、すべてのコントローラがこれらのリソースをコントローラベースクラスから継承し、関数で直接使用できるようになっています。

Service Base Class(サービスベースクラス)
一般的なService Base Classは以下の通りです。

/** Service Base Classes */
public class BaseService {
    /** Injection DAO related */
    /** User DAO */
    @Autowired
    protected UserDAO userDAO;
    ...

    /** Injection services related */
    /** SMS service */
    @Autowired
    protected SmsService smsService;
    ...

    /** Injection parameters related */
    /** system name */
    @Value("${example.systemName}")
    protected String systemName;
    ...

    /** Injection constant related */
    /** super user ID */
    protected static final long SUPPER_USER_ID = 0L;
    ...

    /** Service function related */
    /** Get user function */
    protected UserDO getUser(Long userId) {...}
    ...

    /** Static function related */
    /** Get user name */
    protected static String getUserName(UserDO user) {...}
    ...
}

共通のサービスベースクラスは、主にインジェクションデータアクセスオブジェクト(DAO)、インジェクションサービス、インジェクションパラメータ、静的定数、サービス関数、静的関数を含んでおり、すべてのサービスがサービスベースクラスからこれらのリソースを継承し、直接関数で使用できるようになっています。

ベースクラスの必要性

まず、Liskov Substitution Principle (LSP)を見てみましょう。

LSPによると、ベースクラス(スーパークラス)を参照するすべての場所で、そのサブクラスのオブジェクトを透過的に使用できるようにしなければなりません。

次に、ベースクラスの利点を見てみましょう。

1、ベースクラスは、サブクラスがスーパークラスのすべてのメソッドと属性を持っているので、サブクラスを作成するための作業負荷を軽減します。
2、ベースクラスは、サブクラスがスーパークラスのすべての機能を持っているので、コードの再利用性を向上させます。
3、ベースクラスは、サブクラスが独自の関数を追加できるので、コードのスケーラビリティを向上させます。

したがって、次のような結論を導き出すことができます。

1、コントローラベースクラスとサービスベースクラスは、プロジェクトのどこにも直接使用されておらず、それらのサブクラスで置き換えられることはありません。したがって、それらはLSPに準拠していません。
2、コントローラベースクラスとサービスベースクラスは、抽象インターフェース関数や仮想関数を持ちません。つまり、ベースクラスを継承する全てのサブクラスは共通の特性を持ちません。その結果、プロジェクトで使用されるものはサブクラスのままです。
3、コントローラのベースクラスやサービスのベースクラスは、再利用性にのみ焦点を当てています。つまり、サブクラスは、インジェクションDAO、インジェクションサービス、インジェクションパラメータ、スタティック定数、サービス関数、スタティック関数など、ベースクラスのリソースを便利に使うことができます。しかし、コントローラベースクラスやサービスベースクラスは、これらのリソースの必要性を無視しています。つまり、これらのリソースは、サブクラスにとって必要不可欠なものではありません。そのため、サブクラスがロードされたときにパフォーマンスを低下させてしまいます。

結論から言うと、コントローラベースクラスもサービスベースクラスも雑多なクラスに分類されます。これらは本当の意味でのベースクラスではないので、分割する必要があります。

ベースクラスを分割する方法

コントローラのベースクラスよりもサービスベースクラスの方が代表的なので、この記事ではサービスベースクラスを例に「ベースクラス」を分割する方法を説明します。

インジェクションインスタンスを実装クラスに入れる
「使うときだけクラスを導入し、不要なときは削除する」という原則に従い、使用するDAOやサービス、パラメータなどを実装クラスに注入します。

/** Udser Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** SMS service */
    @Autowired
    private SmsService smsService;

    /** System name */
    @Value("${example.systemName}")
    private String systemName;
    ...
}

静的定数を定数クラスに入れる
静的定数を対応する定数クラスにカプセル化し、必要なときに直接使用します。

/** example constant class */
public class ExampleConstants {
    /** super user ID */
    public static final long SUPPER_USER_ID = 0L;
    ...
}

サービスクラスにサービス関数を入れる
サービス機能を対応するサービスクラスにカプセル化します。他のサービスクラスを使用する場合は、このサービスクラスのインスタンスを注入し、インスタンスを通してサービス機能を呼び出すことができます。

/** User service class */
@Service
public class UserService {
    /** Ger user function */
    public UserDO getUser(Long userId) {...}
    ...
}

/** Company service class */
@Service
public class CompanyService {
    /** User service */
    @Autowired
    private UserService userService;

    /** Get the administrator */
    public UserDO getManager(Long companyId) {
        CompanyDO company = ...;
        return userService.getUser(company.getManagerId());
    }
    ...
}

ツールクラスに静的関数を入れる
静的関数を対応するツールクラスにカプセル化し、必要なときに直接使用します。

/** User Aid Class */
public class UserHelper {
    /** Get the user name */
    public static String getUserName(UserDO user) {...}
    ...
}

ビジネスコードはコントローラクラスに記述あり

現象の説明

コントローラクラスで以下のようなコードをよく見かけます。

/** User Controller Class */
@Controller
@RequestMapping("/user")
public class UserController {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function */
    @ResponseBody
    @RequestMapping(path = "/getUser", method = RequestMethod.GET)
    public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
        // Get user information
        UserDO userDO = userDAO.getUser(userId);
        if (Objects.isNull(userDO)) {
            return null;
        }

        // Copy and return the user
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userDO, userVO);
        return Result.success(userVO);
    }
    ...
}

コンパイラは、インタフェース関数がシンプルで、インタフェース関数をサービス関数にカプセル化する必要がないので、このように書いても良いと説明するかもしれませんが、実際には、インタフェース関数をサービス関数にカプセル化する必要はありません。

特殊な場合

この特殊なケースでは、コードは以下のようになります。

/** Test Controller Class */
@Controller
@RequestMapping("/test")
public class TestController {
    /** System name */
    @Value("${example.systemName}")
    private String systemName;

    /** Access function */
    @RequestMapping(path = "/access", method = RequestMethod.GET)
    public String access() {
        return String.format("You're accessing System (%s)!", systemName);
    }
}

アクセス結果は以下の通りです。

curl http://localhost:8080/test/access

System(null) にアクセスしています!

なぜsystemNameパラメータが注入されていないのかと質問されるかもしれません。さて、Springのドキュメントには次のような説明があります。

実際の@Valueアノテーションの処理はBeanPostProcessorによって実行されることに注意してください。

BeanPostProcessorインタフェースはコンテナごとにスコープされています。これはコンテナ階層を使用している場合にのみ関連します。1つのコンテナでBeanPostProcessorを定義した場合,そのコンテナ内のBeanに対してのみ、その作業を行います。1つのコンテナで定義されたBeanは、両方のコンテナが同じ階層の一部であっても、別のコンテナのBeanPostProcessorによって後処理されません。

これらの説明によれば、@ValueはBeanPostProcessorを介して処理され、WebApplicationContexApplicationContextは別々に処理されます。したがって、WebApplicationContexは、親コンテナの属性値を使用することができません。

コントローラはサービス要件を満たしていません。したがって,コントローラクラスにビジネスコードを記述することは不適切です。

三層サーバーアーキテクチャ

SpringMVCサーバーは、プレゼンテーション層、ビジネス層、パーシスタンス層からなる古典的な3層アーキテクチャを採用しており、クラスアノテーションに@Controller@Service@Repositoryを使用しています。

image.png

  • プレゼンテーション層:コントローラー層とも呼ばれています。このレイヤーは、クライアントからのリクエストを受信し、クライアントからの結果をクライアントに応答する役割を担っています。この層ではHTTPがよく使われています。
  • ビジネス層:サービス層とも呼ばれています。ビジネス関連のロジック処理を担当するレイヤーで、機能別にサービスとジョブに分かれています。
  • 永続性レイヤー:リポジトリ層とも呼ばれます。この層はデータの永続性を担当し、ビジネス層がキャッシュやデータベースにアクセスするために使用します。

そのため、コントローラークラスにビジネスコードを書くことは、SpringMVCサーバーの3層アーキテクチャ仕様に準拠していません。

永続性レイヤーのコードはサービスクラスへ

機能面では、サービスクラスの中にパーシスタンス層のコードを書いても良いと思います。そのため、多くのユーザーがこのコーディング方法を受け入れています。

主な問題点

1、ビジネス層とパーシスタンス層が混在しており、SpringMVCサーバの3層アーキテクチャ仕様に準拠していないこと。
2、ステートメントや主キーがビジネスロジックで組み立てられているため、ビジネスロジックの複雑さが増すこと。
3、サードパーティ製のミドルウェアがビジネスロジックで直接使用されているため、サードパーティ製のパーシステンスミドルウェアの置き換えが難しいこと。
4、また、同じオブジェクトの永続化レイヤのコードが様々なビジネスロジックに散らばってしまい、オブジェクト指向プログラミングの原則に反すること。
5、このコーディング方法でユニットテストケースを書いてしまうと、パーシスタンス層のインタフェース機能を直接テストすることが出来ないこと。

データベースのコードはサービスで書き込まれる

ここでは、データベース永続化ミドルウェアHibernateの直接問い合わせを例に説明します。

現象の説明

/** User Service Class */
@Service
public class UserService {
    /** Session factory */
    @Autowired
    private SessionFactory sessionFactory;

    /** Get user function based on job number */
    public UserVO getUserByEmpId(String empId) {
        // Assemble HQL statement
        String hql = "from t_user where emp_id = '" + empId + "'";

        // Perform database query
        Query query = sessionFactory.getCurrentSession().createQuery(hql);
        List<UserDO> userList = query.list();
        if (CollectionUtils.isEmpty(userList)) {
            return null;
        }

        // Convert and return user
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userList.get(0), userVO);
        return userVO;
    }
}

おすすめの解決策

/** User DAO CLass */
@Repository
public class UserDAO {
     /** Session factory */
    @Autowired
    private SessionFactory sessionFactory;

    /** Get user function based on job number */
    public UserDO getUserByEmpId(String empId) {
        // Assemble HQLstatement
        String hql = "from t_user where emp_id = '" + empId + "'";

        // Perform database query
        Query query = sessionFactory.getCurrentSession().createQuery(hql);
        List<UserDO> userList = query.list();
        if (CollectionUtils.isEmpty(userList)) {
            return null;
        }

        // Return user information
        return userList.get(0);
    }
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function based on job number */
    public UserVO getUserByEmpId(String empId) {
        // Query user based on job number
        UserDO userDO = userDAO.getUserByEmpId(empId);
        if (Objects.isNull(userDO)) {
            return null;
        }

        // Convert and return user
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userDO, userVO);
        return userVO;
    }
}

プラグインについて
AliGeneratorは、アリババが開発したMyBatis Generatorベースのツールで、DAO(データアクセスオブジェクト)層のコードを自動生成します。AliGeneratorで生成されたコードでは、複雑なクエリを実行する際に、ビジネスコード内でクエリ条件を組み立てる必要があります。そのため、ビジネスコードが特に肥大化してしまいます。

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function */
    public UserVO getUser(String companyId, String empId) {
        // Query database
        UserParam userParam = new UserParam();
        userParam.createCriteria().andCompanyIdEqualTo(companyId)
            .andEmpIdEqualTo(empId)
            .andStatusEqualTo(UserStatus.ENABLE.getValue());
        List<UserDO> userList = userDAO.selectByParam(userParam);
        if (CollectionUtils.isEmpty(userList)) {
            return null;
        }

        // Convert and return users
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userList.get(0), userVO);
        return userVO;
    }
}

個人的には、プラグインを使って DAO レイヤーのコードを生成するのは好きではありません。代わりに、マッピングにオリジナルの MyBatis XML を使用する方が好きです。

  • プラグインは、プロジェクトに不適合なコードをインポートする可能性があります。
  • 単純なクエリを実行するには、複雑なコードの完全なセットをインポートする必要があります。
  • 複雑なクエリでは、条件を組み立てるためのコードが複雑で直感的ではありません。XMLで直接SQL文を書く方が良いでしょう。
  • テーブルを変更した後、コードを再生成して上書きする必要があり、その間に誤ってユーザー定義関数(UDF)を削除してしまう可能性があります。

プラグインの使用を選択する場合は、プラグインがもたらす利便性を享受しつつ、プラグインのデメリットも受け入れるべきです。

Redisのコードはサービスクラスへ

説明

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;
    /** Redistemplate */
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    /** User primary key mode */
    private static final String USER_KEY_PATTERN = "hash::user::%s";

    /** Save user function */
    public void saveUser(UserVO user) {
        // Convert user information
        UserDO userDO = transUser(user);

        // Save Redis user
        String userKey = MessageFormat.format(USER_KEY_PATTERN, userDO.getId());
        Map<String, String> fieldMap = new HashMap<>(8);
        fieldMap.put(UserDO.CONST_NAME, user.getName());
        fieldMap.put(UserDO.CONST_SEX, String.valueOf(user.getSex()));
        fieldMap.put(UserDO.CONST_AGE, String.valueOf(user.getAge()));
        redisTemplate.opsForHash().putAll(userKey, fieldMap);

        // Save database user
        userDAO.save(userDO);
    }
}

おすすめの解決策

/** User Redis Class */
@Repository
public class UserRedis {
    /** Redistemplate */
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    /** Primary key mode */
    private static final String KEY_PATTERN = "hash::user::%s";

    /** Save user function */
    public UserDO save(UserDO user) {
        String key = MessageFormat.format(KEY_PATTERN, userDO.getId());
        Map<String, String> fieldMap = new HashMap<>(8);
        fieldMap.put(UserDO.CONST_NAME, user.getName());
        fieldMap.put(UserDO.CONST_SEX, String.valueOf(user.getSex()));
        fieldMap.put(UserDO.CONST_AGE, String.valueOf(user.getAge()));
        redisTemplate.opsForHash().putAll(key, fieldMap);
    }
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;
    /** User Redis */
    @Autowired
    private UserRedis userRedis;

    /** Save user function */
    public void saveUser(UserVO user) {
        // 转化用户信息
        UserDO userDO = transUser(user);

        // Save Redis user
        userRedis.save(userDO);

        // Save database user
        userDAO.save(userDO);
    }
}

Redisのオブジェクト関連の操作インターフェースをDAOクラスにカプセル化します。これは、SpringMVCサーバのオブジェクト指向プログラミングの原則と3階層アーキテクチャの仕様に準拠しており、コードの管理・保守を容易にします。

データベースモデルクラスはインターフェースに晒されている

症状の説明

/** User DAO Class */
@Repository
public class UserDAO {
    /** Get user function */
    public UserDO getUser(Long userId) {...}
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function */
    public UserDO getUser(Long userId) {
        return userDAO.getUser(userId);
    }
}

/** User Controller Class */
@Controller
@RequestMapping("/user")
public class UserController {
    /** User service */
    @Autowired
    private UserService userService;

    /** Get user function */
    @RequestMapping(path = "/getUser", method = RequestMethod.GET)
    public Result<UserDO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
        UserDO user = userService.getUser(userId);
        return Result.success(user);
    }
}

先行コードはSpringMVCサーバの3層アーキテクチャに準拠しているようです。唯一の問題は、データベースモデルUserDOが外部インターフェースに直接さらされていることです。

既存の問題と解決策

既存の問題点
1、データベースのテーブル設計が間接的に露出しているため、競合製品の分析に便利です。
2、データベースクエリにフィールド制限が課されていない場合、インターフェースデータの量が膨大になり、ユーザーの貴重なトラフィックを無駄にします。
3、データベースクエリにフィールド制限が課されていない場合、機密性の高いフィールドがインターフェースに簡単に露出してしまい、データセキュリティの問題が生じます。
4、データベースモデルクラスがインターフェースの要件を満たすことができない場合、データベースモデルクラスに他のフィールドを追加する必要があり、その結果、データベースモデルクラスとデータベースのフィールドの間にミスマッチが発生します。
5、インターフェースのドキュメントが適切にメンテナンスされていない場合、コードを読んでも、データベースモデルクラス内のどのフィールドがインターフェースによって使用されているかを識別するのに役立ちません。そのため、コードの保守性が悪くなります。

ソリューション
1、管理システムの観点からは、データベースモデルクラスはインターフェースモデルクラスから完全に独立していなければなりません。
2、プロジェクトの構造上、開発者はデータベースモデルクラスをインターフェースに公開しないようにしなければなりません。

プロジェクト構築のための3つの方法

以下では、開発者がデータベースモデルクラスをインターフェースに公開することを効果的に防ぐために、より科学的にJavaプロジェクトを構築する方法を説明します。

方法1:共有モデルでプロジェクトを構築する
すべてのモデル・クラスを 1 つのモデル・プロジェクト(example-model)に配置します。他のプロジェクト(example-repository、example-service、example-website など)は、すべて example-model に依存しています。関係図は以下のようになります。

image.png

image.png

リスク
プレゼンテーション層プロジェクト(example-webapp)は、ビジネス層プロジェクト(example-service)の任意のサービス機能を呼び出すことができますし、ビジネス層をまたいでパーシスタンス層プロジェクト(example-repository)のDAO機能を直接呼び出すこともできます。

方法2:分離されたモデルでプロジェクトを構築
API プロジェクト(example-api)を別途構築し、外部インタフェースとそのモデル VO クラスを抽象化する。ビジネス層プロジェクト(example-service)はこれらのインタフェースを実装し、プレゼンテーション層プロジェクト(example-webapp)にサービスを提供します。プレゼンテーション層プロジェクト(example-webapp)は、API プロジェクト(example-api)で定義されたサービスインタフェースのみを呼び出します。

image.png

image.png

リスク
プレゼンテーション層プロジェクト(example-webapp)は、ビジネス層プロジェクト(example-service)の内部サービス機能とパーシステンス層プロジェクト(example-repository)のDAO機能をまだ呼び出すことができます。このような状況を避けるために、管理システムは、プレゼンテーション層プロジェクト(example-webapp)がAPIプロジェクト(example-api)によって定義されたサービスインターフェース関数のみを呼び出すことができるようにする必要があります。

方法3: サービス指向プロジェクトを構築する
ビジネス層プロジェクト(example-service)と永続化層プロジェクト(example-repository)をDubboプロジェクト(example-dubbo)を使ってサービスにパッケージ化します。ビジネスレイヤプロジェクト(example-webapp)または他のビジネスプロジェクト(other-service)に対して、APIプロジェクト(example-api)で定義されたインターフェース機能を提供します。

image.png

image.png

注意: Dubboプロジェクト(example-dubbo)は、APIプロジェクト(example-api)で定義されたサービス・インターフェースのみをリリースします。これにより、データベースモデルが公開されないことを保証します。ビジネスレイヤープロジェクト(例-webapp)や他のビジネスプロジェクト(他のサービス)は、APIプロジェクト(例-api)にのみ依存し、APIプロジェクトで定義されているサービスインターフェースのみを呼び出すことができます。

あまり推奨されない提案

ユーザーによっては、次のような配慮があるかもしれません。インタフェースモデルと永続層モデルが分離されていることを考えると、インタフェースモデルにデータ問い合わせモデルのVOクラスが定義されている場合、永続層モデルにもデータ問い合わせモデルのDOクラスが定義されている必要があることになります。また、インタフェースモデルにデータ戻りモデルのVOクラスが定義されている場合は、永続層モデルにもデータ戻りモデルのDOクラスが定義されている必要があります。しかし、これはプロジェクトの初期段階での迅速な反復開発にはあまり適していません。また、次のような疑問も出てきます。永続層のデータモデルをインターフェイスを通して公開せずに、インターフェイスのデータモデルを永続層に使わせることは可能なのでしょうか?

このメソッドは、SpringMVCサーバの3層アーキテクチャの独立性に影響を与えるため、受け入れられません。しかし、この方法はデータベースモデルクラスを公開しないので、迅速な反復開発には許容できます。したがって、あまりお勧めできない提案です。

/** User DAO Class */
@Repository
public class UserDAO {
    /** Calculate user function */
    public Long countByParameter(QueryUserParameterVO parameter) {...}
    /** Query user function */
    public List<UserVO> queryByParameter(QueryUserParameterVO parameter) {...}
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Query user function */
    public PageData<UserVO> queryUser(QueryUserParameterVO parameter) {
        Long totalCount = userDAO.countByParameter(parameter);
        List<UserVO> userList = null;
        if (Objects.nonNull(totalCount) && totalCount.compareTo(0L) > 0) {
            userList = userDAO.queryByParameter(parameter);
        }
        return new PageData<>(totalCount, userList);
    }
}

/** User Controller Class */
@Controller
@RequestMapping("/user")
public class UserController {
    /** User service */
    @Autowired
    private UserService userService;

    /** Query user function (with the page index parameters of startIndex and pageSize) */
    @RequestMapping(path = "/queryUser", method = RequestMethod.POST)
    public Result<PageData<UserVO>> queryUser(@Valid @RequestBody QueryUserParameterVO parameter) {
        PageData<UserVO> pageData = userService.queryUser(parameter);
        return Result.success(pageData);
    }
}

結論

Javaをどのように活用すべきかについては人それぞれの意見があり、もちろんこの記事では私の個人的な意見のみを述べています。しかし、私にとっては、私が以前働いていたいくつかのスタートアップ企業での経験に基づいて、自分の考えを表現することが重要だと考えました。なぜなら、私の理解では、これらのカオスな構成が修正されていれば、システム全体がより良いものになると考えているからです。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

0
1
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
0
1