talosのプログラミング教室

Java Silver合格への道 ~staticインポート~

こんにちは。talosです。

最近Java Silverの勉強を始めました。

Java Silverでは「こんなのどこで使うんや!」って問題がよく出るのですが、そういうことは覚えにくいのでブログでまとめることにしました。

今回はstaticインポートについてです。

使い方

staticメンバにアクセスする際にはクラス名で修飾する必要があります。

public class StaticImport {

	public static void main(String[] args) {
		// TODO 自動生成されたメソッド・スタブ

		System.out.println(Math.PI);
	}

}

staticインポートを使うことで修飾する必要がなくなります。(Math.PI⇒PI)

import static java.lang.Math.PI;

public class StaticImport {

	public static void main(String[] args) {
		// TODO 自動生成されたメソッド・スタブ

		System.out.println(PI);
	}

}

import static ~という順番がよく問われるのでしっかり覚えましょう。

【Webアプリ】エラーページの設定

こんにちは。talosです。

今回はエラーページの設定の仕方を説明します。

Webアプリを作る際は必須なので、必ず設定するようにしましょう。

環境

言語:Java8
フレームワークJSF

※ 今回は環境はあまり関係ありません。

解説

web.xmlを見てみます。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
	id="WebApp_ID" version="3.1">
	<display-name>Error_Sample</display-name>
	<welcome-file-list>
		<welcome-file>index.html</welcome-file>
		<welcome-file>index.htm</welcome-file>
		<welcome-file>index.jsp</welcome-file>
		<welcome-file>default.html</welcome-file>
		<welcome-file>default.htm</welcome-file>
		<welcome-file>default.jsp</welcome-file>
	</welcome-file-list>
	<servlet>
		<servlet-name>Faces Servlet</servlet-name>
		<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>Faces Servlet</servlet-name>
		<url-pattern>/faces/*</url-pattern>
	</servlet-mapping>
	<error-page>
		<error-code>404</error-code>
		<location>/faces/404-error.xhtml</location>
	</error-page>
	<error-page>
		<error-code>500</error-code>
		<location>/faces/500-error.xhtml</location>
	</error-page>
</web-app>

エラーページの設定をしているのはこの部分です。

<error-page>
	<error-code>404</error-code>
	<location>/faces/404-error.xhtml</location>
</error-page>
<error-page>
	<error-code>500</error-code>
	<location>/faces/500-error.xhtml</location>
</error-page>

<error-page>タグの間に<error-code>タグと<location>タグを入れます。

<error-code>タグにはエラーページを設定したいエラーコードを書き、<location>タグにはページのパスを書きます。

JSFページの場合は/facesが付きます。

ここでは404と500だけですが、401や403、503などのよくあるエラーコードについても設定しておきましょう。

あとはそれぞれのエラーページを用意するだけです。
(ここでは404-error.xhtmlと500-error.xhtmlを用意しました)


スポンサーリンク



実行

Webアプリを立ち上げます。

立ち上がったら実際には存在しないページのURLを打ち込んでみます。

f:id:talosta:20201205224816p:plain

すると

f:id:talosta:20201205224836p:plain

自作した404エラーのページに移りました。


次にトップページに戻り、例外を起こすボタンを押してみます。

このボタンを押すとNullPointerExceptionを起こすようにしてあります。

f:id:talosta:20201205225015p:plain

すると

f:id:talosta:20201205225029p:plain

自作した500エラーのページに移りました。

おわりに

今回はエラーページの設定の方法を説明しました。

不明点があればコメントお願いします。

【Java・MyBatis】排他制御の実装方法

こんにちは。talosです。

WebアプリからDBを更新するとき、2人以上のユーザーが同時に更新すると、データに不整合が起こります。

f:id:talosta:20201129174305p:plain

この現象を防ぐ手法としてよく使われるのが楽観的排他制御です。

この手法では、参照したときのバージョンや更新日時が更新時に変更されていないかを確認し、変更されていた場合は更新できないようにします。

f:id:talosta:20201130213153p:plain

この手法を実装していきたいと思います。

環境

言語:Java8
フレームワークJSF
DB:MySQL
O/Rマッピングツール:MyBatis

解説

まず、以下のようなテーブルを作りました。

f:id:talosta:20201129181911p:plain

updateDateは排他制御の判定に使います。


それではsrc/db/UserSample.javaを見ていきましょう。

注目してほしい点は一か所だけです。

example.createCriteria().andIdEqualTo(user.getId()).andUpdateDateEqualTo(user.getUpdateDate());

さらに言うとここだけ見ていただければよいです。

andUpdateDateEqualTo(user.getUpdateDate())

これは

WHERE UPDATE_DATE = 参照時点でのUPDATE_DATE

と同じです。

自身がデータを参照した後に他のユーザーが更新していた場合はデータに不整合が起こるため、更新できないようにする必要があります。

更新時にはUSER_SAMPLEテーブルのUPDATE_DATEを更新した日時に更新するため、他のユーザーが更新したかどうかは参照時点での更新日時(updateDate)とDBのUPDATE_DATEが一致しているかを確認すればわかります。

そこで自身が参照した時点での更新日時(updateDate)が現在の更新日時と同じかを確かめ、同じ場合のみ更新をします。


スポンサーリンク



実行

※ レコードは1つ以上挿入してください。

サーバーを立ち上げ、00.xhtmlを開きます。

また、シークレットウインドウでも00.xhtmlを開きます。

f:id:talosta:20201129184825p:plain

開けたら両方で同じ人の編集ボタンを押します。

すると、下のような画面が開きます。

f:id:talosta:20201129185140p:plain

片方の画面で名前を編集して実行します。

f:id:talosta:20201129185318p:plain

名前が変更されました。

f:id:talosta:20201129185432p:plain

もう片方の画面でも名前を編集して実行します。

f:id:talosta:20201129185602p:plain

こちらでは排他制御が行われ、エラーメッセージが出力されました。

f:id:talosta:20201129185636p:plain

おわりに

今回は排他制御を実装しました。

不明点があればコメントお願いします。

【MyBatis】自動生成した~Exampleクラスの使い方

こんにちは。talosです。

MyBatis Generatorを使うと~Exampleクラスが生成されるのですが、それの使い方に関する資料があまりないので説明したいと思います。

前提

・MyBatis Generatorを使って自動生成済み
・SqlSessionFactory等のMyBatisを使う準備ができている

使い所

自動生成を行うとテーブル一つに対し以下のような抽象メソッドが作られます。

long countByExample(UserSampleExample example);

int deleteByExample(UserSampleExample example);

int deleteByPrimaryKey(String id);

int insert(UserSample record);

int insertSelective(UserSample record);

List<UserSample> selectByExample(UserSampleExample example);

UserSample selectByPrimaryKey(String id);

int updateByExampleSelective(@Param("record") UserSample record, @Param("example") UserSampleExample example);

int updateByExample(@Param("record") UserSample record, @Param("example") UserSampleExample example);

int updateByPrimaryKeySelective(UserSample record);

int updateByPrimaryKey(UserSample record);

この中で~Exampleクラスを使うのはexampleを引数にとるメソッドだけです。

// ①
long countByExample(UserSampleExample example);

// ②
int deleteByExample(UserSampleExample example);

// ③
List<UserSample> selectByExample(UserSampleExample example);

// ④
int updateByExampleSelective(@Param("record") UserSample record, @Param("example") UserSampleExample example);

// ⑤
int updateByExample(@Param("record") UserSample record, @Param("example") UserSampleExample example);

名前からわかる通り上から三つはそれぞれ、①該当レコードをカウントする、②該当レコードを削除する、③該当レコードを選択するSQLを実行するメソッドです。

下二つはどちらも該当レコードを更新するSQLを実行するメソッドです。

④は引数のrecordにnullの項目がある場合はその項目の更新をスキップし、値を持っている項目だけ更新します。

一方で、⑤はすべての項目を更新します。(nullがある場合はnullで更新します。NOT NULL制約がある場合は例外が起こります)


スポンサーリンク



使い方

public List<UserSample> getAllUsers() {
	List<UserSample> list = new ArrayList<>();
	try (SqlSession session = sqlSessionManager.getSqlSessionFactory().openSession()) {
		UserSampleMapper mapper = session.getMapper(UserSampleMapper.class);
		UserSampleExample example = new UserSampleExample();
		example.createCriteria().andIdIsNotNull();
		list = mapper.selectByExample(example);
	}
	return list;
}

~Exampleクラスのインスタンスを生成します。

そして、createCriteria()を呼び、続けてWHERE句を付加するメソッドを呼びます。

WHERE句を二つ以上使いたい場合は、以下のように後ろに繋げます。

example.createCriteria().andIdIsNotNull().andNameEqualTo("Tanaka");

WHERE句を付加するメソッドにはこれらがあります。

and~Between(value1, value2)
and~NotBetween(value1, value2)
and~EqualTo(value)
and~NotEqualTo(value)
and~GreaterThan(value)
and~GreaterThanOrEqualTo(value)
and~LessThan(value)
and~LessThanOrEqualTo(value)
and~In(values)
and~NotIn(values)
and~IsNull()
and~IsNotNull()
and~Like(value)
and~NotLike(value)

and~Between(value1, value2) / and~NotBetween(value1, value2)

SQLのWHERE ~ BETWEEN value1 AND value2 / WHERE ~ NOT BETWEEN value1 AND value2に相当します。

and~EqualTo(value) / and~NotEqualTo(value)

SQLのWHERE ~ = value / WHERE ~ != value(またはWHERE ~ <> value)に相当します。

and~GreaterThan(value) / and~GreaterThanOrEqualTo(value)

SQLのWHERE ~ > value / WHERE ~ >= valueに相当します。

and~LessThan(value) / and~LessThanOrEqualTo(value)

SQLのWHERE ~ < value / WHERE ~ <= valueに相当します。

and~In(values) / and~NotIn(values)

valuesはList型で、value1, value2,...を持ちます。

SQLのWHERE ~ IN (value1, value2,...) / WHERE ~ NOT IN (value1, value2,...)に相当します。

and~IsNull() / and~IsNotNull()

SQLのWHERE ~ IS NULL / WHERE ~ IS NOT NULLに相当します。

and~Like(value) / and~NotLike(value)

SQLのWHERE ~ LIKE value / WHERE ~ NOT LIKE valueに相当します。

おわりに

今回はMyBatis Generatorで自動生成した~Exampleクラスの使い方を説明しました。

不明点があればコメントお願いします。

Eclipseからmainブランチにプッシュする方法

こんにちは。talosです。

2020年10月よりGitHubのデフォルトブランチはmasterからmainになりました。

しかしEclipseはまだ対応できておらず、普通にプッシュするとmasterブランチにプッシュしてしまいます。

masterブランチに一度プッシュしてしまうとmainブランチに反映するのが結構面倒だったので、初めからmainにプッシュする方法を説明します。

GitHubリポジトリを作る

「Repositories」を開き「New」を押します。

f:id:talosta:20201130222301p:plain

「Repository name」に任意の名前を入れて「Create repository」を押します。

ここでREADMEを作ってしまうとその後が面倒になるので後で作りましょう。

f:id:talosta:20201130222311p:plain

Gitの設定をする

プッシュしたいプロジェクトを右クリックし、「チーム」⇒「プロジェクトの共有」と進みます。

f:id:talosta:20201130222427p:plain

リポジトリー・タイプはGitで次に進みます。

f:id:talosta:20201130222519p:plain

「プロジェクトの親フォルダー内のリポジトリーを使用または作成」にチェックを入れて、②のチェックボックスをクリックします。
(チェックは入れられません)

すると、「リポジトリーの作成」を押せるようになるので押します。

f:id:talosta:20201130222829p:plain

押すと勝手に②のところにチェックが入るので、「完了」を押します。

f:id:talosta:20201130222919p:plain

EclipseからGitHubにプッシュする

「ウインドウ」⇒「ビューの表示」⇒「その他」⇒「Git」⇒「Gitステージング」を開きます。

f:id:talosta:20201130223628p:plain

f:id:talosta:20201130223637p:plain

Gitステージングが開き、もしプッシュしたいリポジトリーが選択されていない場合は矢印のところから選択してください。

f:id:talosta:20201130223913p:plain

リポジトリーを選択し、プッシュするファイルを選択、コミット・メッセージを入力したら、「コミットおよびプッシュ」を押します。

一度目のプッシュ時は次の画面が開くので、URIとユーザー、パスワードを入力します。

f:id:talosta:20201130224452p:plain

URIの「talos-ta」のところはご自身のユーザー名に、「GitHub_Sample.git」のところはご自身で作った「リポジトリ名.git」に置き換えてください。

「次へ」を押すと以下の画面が開くので、宛先のブランチを「main」にします。

f:id:talosta:20201130224512p:plain

「次へ」を押すと次の画面が開くので、「完了」を押します。

f:id:talosta:20201130224704p:plain

mainブランチにプッシュできました。

f:id:talosta:20201130224817p:plain

おわりに

今回はEclipseからmainブランチにプッシュする方法を説明しました。

不明点があればコメントお願いします。

【Java】ページングでWebアプリのパフォーマンスを向上させる

こんにちは。talosです。

Webアプリのパフォーマンスを大きく左右する要因としてDB処理が挙げられます。

扱うデータが多くなればなるほど時間がかかるため、一覧表示などは工夫しないとユーザーへの負担が大きくなります。

そこで今回は、ページングを使って一覧表示をすることで、Webアプリのパフォーマンスを向上させる方法を解説します。

ページングとは

そもそもページングとはなにかというと、検索結果などが多いときに一度にすべて表示せず、1ページには検索結果の一部のみを表示して次ページボタンや前ページボタンで他のページに移ることができるようにする処理です。

一度にすべての検索結果を取得せず1ページに表示するデータのみを取得するため、ページアクセスの際にかかるDB処理の時間を短くすることができます。

f:id:talosta:20201128094439p:plain
Google検索のページング

環境

言語:Java
フレームワークJSF

解説

src/holder/Page.javaから見ていきましょう。

/**
 * 1ページ中に表示される最大件数
 */
public static final int OUTPUT_NUM = 10;

/**
 * 全レコードの件数
 */
private long recordNum;

/**
 * 現在のページ
 */
private int currentPage = 1;

/**
 * 全レコードの件数から算出されるページ数(recordNum/outputNum)
 */
private long maxPage;

まずはフィールドです。

ここはコメントの通りです。

/**
 * ページングに使う変数を設定する
 * @param recordNum
 */
public void config(Long recordNum) {
	this.recordNum = recordNum;
	this.maxPage = this.recordNum / OUTPUT_NUM;

	if (this.recordNum % OUTPUT_NUM != 0
			|| this.recordNum == 0) {
		++this.maxPage;
	}
}

config()はページングに使う変数を設定するメソッドです。

maxPageは(全レコードの件数/1ページに表示する最大件数)で算出します。

ですが、例えば全レコードの件数が35件の場合、整数型だと35/10=3となるため残りの5件を表示できなくなってしまいます。

そのため、

this.recordNum % OUTPUT_NUM != 0

の場合はmaxPageをインクリメントします。

また、全レコードの件数が0の場合、maxPageは0/10=0となります。

ですが、レコードがなくても1ページは表示しなくてはなりません。

そのため、

this.recordNum == 0

の場合もmaxPageをインクリメントします。

/**
 * 前ページボタンを表示するか
 * @return
 */
public boolean prevPageDisplay() {
	return currentPage > 1;
}

/**
 * 次ページボタンを表示するか
 * @return
 */
public boolean nextPageDisplay() {
	return currentPage < maxPage;
}

この2つは前ページボタンと次ページボタンを表示するかどうかを制御するメソッドです。

1ページ目にいるときに前ページボタンが押されたら0ページ目へ遷移することになります。

ですが0ページ目はありません。

そのようなことを避けるためのメソッドです。

/**
 * 最初のページへ
 */
public void topPage() {
	currentPage = 1;
}

/**
 * 最後のページへ
 */
public void lastPage() {
	currentPage = (int) maxPage;
}

/**
 * 前のページへ
 */
public void prevPage() {
	if (prevPageDisplay()) {
		currentPage--;
	}
}

/**
 * 次のページへ
 */
public void nextPage() {
	if (nextPageDisplay()) {
		currentPage++;
	}

これらはページの遷移を行うメソッドです。

最初のページへ遷移するときはcurrentPageに1を代入し、最後のページに遷移するときは遷移し得る最大のページをcurrentPageを代入します。

前のページに遷移するときにはcurrentPageを-1し、次のページに遷移するときにはcurrentPageを+1します。

/**
 * 何番目のレコードから表示するか
 * @return
 */
public Integer offset() {
	return (currentPage - 1) * OUTPUT_NUM;
}

何件目から表示するかを計算をするメソッドです。

画面に一覧を表示する際、1ページ目であれば1件目から表示すれば良いですが、2ページ目では11件目から表示する必要があります。

言うまでもないですが、リストのインデックスは0からなのでcurrentPageから1引いています。


次に、src/master/UserMaster.javaを見てみましょう。

/**
 * 登録されたユーザーのリスト
 */
private ArrayList<User> userList;

{
	userList = new ArrayList<User>();
	for (int i = 0; i < 35; i++) {
		userList.add(new User("user" + i, "test"));
	}
}

本来はDBなどからレコードを取得してきますが、今回はサンプルのためここでデータを用意します。

35人のIDとパスワードをリストに登録しておきます。

/**
 * offsetからnum件のユーザーリストを返す
 * @param page
 * @return
 */
public ArrayList<User> getUserList(Page page) {
	page.config((long) userList.size());
	ArrayList<User> userList = new ArrayList<>();
	int offset = page.offset();
	int num = Page.OUTPUT_NUM;

	// recordNum % OUTPUT_NUM != 0の場合リストの外を参照する
	try {
		for (int i = offset; i < offset + num; i++) {
			userList.add(this.userList.get(i));
		}
	} catch (IndexOutOfBoundsException e) {
		e.printStackTrace();
	}
	return userList;
}

getUserList()は表示するレコードだけを返すメソッドです。

最初にpage.config()を呼んでいます。引数は全レコードの件数です。

全レコードの件数も本来であればSELECT COUNT(*)を投げて件数を取得します。

for文のところはoffset件目から10件取得してリストに追加しています。

LIMIT句で取得する件数を制限してSELECT文を投げるところを模しています。


スポンサーリンク



実行

実行するとこのような画面が開きます。

f:id:talosta:20201128171707p:plain

1~10件目が表示され、最初のページへのボタンと前ページへのボタンが非活性化されています。

次ページへのボタンを押すと、

f:id:talosta:20201128171716p:plain

11~20件目が表示されます。

最後のページへのボタンを押すと、

f:id:talosta:20201128171724p:plain

最後の5件が表示され、次ページへのボタンと最後のページへのボタンが非活性化されました。

おわりに

今回はページングの実装方法を説明しました。

不明点がありましたら、コメントお願いします。

【Java】URL直打ち対策

こんにちは。talosです。

Webアプリを作っていると、会員ページなどログイン前にはアクセスさせたくないページがあると思います。

今回はそのような場合のアクセス制御の方法を紹介したいと思います。

環境

言語:Java
フレームワークJSF

解説

source/filter/LoginFilter.javaを見てみましょう。

package filter;

import java.io.IOException;

import javax.inject.Inject;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import logic.UserLogic;

public class LoginFilter implements Filter {

	@Inject
	private UserLogic lUser;

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		// TODO 自動生成されたメソッド・スタブ

		if (!lUser.loginStatus()) {
			// ログインしていない場合はトップページにリダイレクト
			HttpServletRequest req = (HttpServletRequest) request;
			HttpServletResponse res = (HttpServletResponse) response;
			res.sendRedirect(req.getContextPath() + "/faces/00.xhtml");
		} else {
			// ログインしている場合は次のフィルタへ
			chain.doFilter(request, response);
		}
	}

}

ポイントは4点です。


ポイント

1.Filterをimplementsしている
2.doFilter()メソッドをオーバーライドしている
3.ログインしていない場合はトップページにリダイレクトする
4.ログインしている場合はchain.doFilter()メソッドを呼ぶ

InjectしているUserLogicクラスは自作のクラスです。

loginStatus()メソッドではログイン状態かどうかを判定しています。

falseが返ってきた場合はログインしていないため、適当なURLにリダイレクトしています。

一方で、trueが返ってきた場合はログインしているため、次のフィルタに移ります。


次に、WebContent/WEB-INF/web.xmlを見てみましょう(一部抜粋)。

<filter>
	<filter-name>Filter</filter-name>
	<filter-class>filter.LoginFilter</filter-class>
</filter>
<filter-mapping>
	<filter-name>Filter</filter-name>
	<url-pattern>/faces/01.xhtml</url-pattern>
</filter-mapping>

まず<filter>タグの中から見ていきます。

<filter-name>にはフィルタの名前を指定します。

<filter-class>には先程見たFilterをimplementsしたクラスのパスを指定します。

次に<filter-mapping>タグの中を見ていきます。

<filter-name>の中は<filter>タグの中の<filter-name>と同じ名前とします。

<url-pattern>には、<filter-class>で指定したクラスの中で行っている処理を適用するページのパスを指定します。

今回はログインしていない状態で01.xhtmlにアクセスしたときに制限したいため、/faces/01.xhtmlとしています。
(faceletsタグを使用していない場合は/facesは不要です。)

もしこの後に他のフィルタを適用したい場合は、続けて<filter>と<filter-mapping>のセットを用意してあげればよいです。

また、フォルダ内のページすべてにフィルタを適用したい場合は/faces/folder/*のようにします。


スポンサーリンク



実行

実際に動かしてみます。

まず、
ID:test
password:test
でログインしてください。

ログアウトボタンが表示されていればログインに成功しています。

f:id:talosta:20201125222021p:plain

この状態でアドレスバーにhttp://localhost:8080/WebFilter_Sample/faces/01.xhtmlと打ち込むと、

f:id:talosta:20201125222323p:plain

01.xhtml(ログアウトボタンが表示されているページ)が開けると思います。


次に、一度ログアウトして、

f:id:talosta:20201125222504p:plain

アドレスバーにhttp://localhost:8080/WebFilter_Sample/faces/01.xhtmlと打ち込むと、

f:id:talosta:20201125222548p:plain

01.xhtmlは開けず、最初のページから動かないと思います。

おわりに

今回はServletの@WebFilterアノテーションを使用して、URLの直打ち対策を行ってみました。

不明点がありましたら、コメントお願いします。