Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その11 )( ログイン画面の作成5 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その10 )( ログイン画面の作成4 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 「次回から自動的にログインする」機能の作成
参照したサイト・書籍
Spring Security 3.1
- 作者: Robert Winch
- 出版社/メーカー: Packt Publishing
- 発売日: 2012/12/26
- メディア: ペーパーバック
- この商品を含むブログを見る
- 「Chapter 6: Remember-me Services」を参照しました。
Remember-Me Authentication
http://docs.spring.io/spring-security/site/docs/3.2.7.RELEASE/reference/htmlsingle/#remember-me- Spring Security の公式マニュアルです。
Spring Boot Security Application
http://kielczewski.eu/2014/12/spring-boot-security-application/- Spring Boot + Spring Security で Remember-me 機能を実装する方法を参考にしました。
How to achieve conditional resource import in a Spring XML context?
http://stackoverflow.com/questions/3035630/how-to-achieve-conditional-resource-import-in-a-spring-xml-context- applicationContext.xml の中の設定を spring.profiles.active の値により変える方法を調査した時に参照しました。
目次
- はじめに
- 1.0.x-make-rememberme ブランチの作成
- ログイン画面に「次回から自動的にログインする」チェックボックスを追加する
- UserInfoUserDetails クラス、loginsuccess.html、UserInfoDao クラス、UserInfoService クラスの変更
- WebSecurityConfig クラスの変更
- 動作確認
- commit、Push、Pull Request、マージ
- メモ書き
手順
はじめに
Spring Security の Remember-me 機能を実装します。
Remember-me 機能とは、ログインしていない状態で認証が必要なページにアクセスした時にログイン画面へリダイレクトせず自動ログインする機能ですが、ログイン画面にアクセスした時には単にログイン画面を表示します ( 自動ログインはしません )。
Remember-me 機能には TokenBased と PersistentTokenBased の2つの方法がありますが、今回対応するのは TokenBased の方です。TokenBased は ID、パスワードをハッシュ化した文字列を remember-me Cookie にセットしてブラウザへ返すため、ID やパスワードをブラウザに返すのを避けたい場合には PersistentTokenBased で実装する必要があります。remember-me Cookie にセットする文字列については 5.2. Simple Hash-Based Token Approach に記載されています。
1.0.x-make-rememberme ブランチの作成
- IntelliJ IDEA で 1.0.x-make-rememberme ブランチを作成します。
ログイン画面に「次回から自動的にログインする」チェックボックスを追加する
src/main/resources/templates の下の login.html を リンク先の内容 に変更します。以下の画像の画面になります。
UserInfoUserDetails クラス、loginsuccess.html、UserInfoDao クラス、UserInfoService クラスの変更
UserInfoUserDetails クラスの getUsername メソッドが返す値が Remember-me 機能が生成する Token の username にセットされ、これが自動ログイン時の ID に使用されます。現在は user_info.username の値を返していますが、今回作成しているログイン画面では user_info.mail_address を ID としているので、getUsername メソッドで user_info.mail_address の値を返すように変更します。
この変更に伴い他にもいくつか変更する必要があるクラスが出てきますので、一緒に変更します。
src/main/java/ksbysample/webapp/lending/security の下の UserInfoUserDetails.java を リンク先の内容 に変更します。
src/main/resources/templates の下の loginsuccess.html を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/dao の下の UserInfoDao.java を リンク先の内容 を変更します。
src/main/resources/META-INF/ksbysample/webapp/lending/dao/UserInfoDao の下の initCntBadcredentialsByUsername.sql を選択後、SHIFT+F6 を押して Rename ダイアログを表示し、initCntBadcredentialsByMailAddress.sql にファイル名を変更します。変更後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/service の下の UserInfoService.java を リンク先の内容 に変更します。
WebSecurityConfig クラスの変更
動作確認
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
まずは remember-me Cookie がない状態で認証が必要なページにアクセスしてもログイン画面に戻ることを確認します。
ブラウザを起動して http://localhost:8080/loginsuccess にアクセスします。「ログイン成功!」の画面は表示されずログイン画面が表示されます。
ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。「ログイン成功!」の画面が表示されます。
ブラウザを1度終了させてから起動し直します。起動した後、http://localhost:8080/loginsuccess にアクセスするとログイン画面が表示されます。
次は remember-me Cookie がある状態で認証が必要なページにアクセスすると、ログイン画面に戻らずそのページが表示されることを確認します。
ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、今度は「次回から自動的にログインする」をチェックして「ログイン」ボタンをクリックします。「ログイン成功!」の画面が表示されます。
再度ブラウザを1度終了させてから起動し直します。起動した後、http://localhost:8080/loginsuccess にアクセスすると今度は「ログイン成功!」の画面が表示されます。
最後にログアウト機能で remember-me Cookie を削除すると、最初と同じように認証が必要なページにアクセスしてもログイン画面に戻ることを確認します。
http://localhost:8080/logout にアクセスしてログアウトしてログイン画面に戻ります。
再度ブラウザを1度終了させてから起動し直します。起動した後、http://localhost:8080/loginsuccess にアクセスすると今度はログアウトしたことにより remember-me Cookie が削除されているのでログイン画面が表示されます。
Ctrl+F2 を押して Tomcat を停止します。
commit、Push、Pull Request、マージ
commit します。「Code Analysis」ダイアログが出ますが、無視して「Commit」ボタンをクリックします。
GitHub へ Push、1.0.x-make-rememberme -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-make-rememberme ブランチを削除、をします。
メモ書き
applicationContext.xml の id="txAdvice" の timeout が 3秒だと Debug 時にタイムアウトしてしまう
今回 Remember-me 機能を実装している時にうまく動かないところがあってデバッグ実行して動作を確認したのですが、applicationContext.xml でトランザクションのタイムアウトを 3秒に設定していたため、デバッグ中にタイムアウトが発生してまともにデバッグできませんでした。。。
タイムアウトすると画面に以下のようなエラーメッセージが表示されます。
spring.profiles.active = product の時は 3秒の設定とし、develop の時はタイムアウトなしにしたいので、設定する方法を調べました。
src/main/java/ksbysample/webapp/lending の下の Application.java を リンク先の内容 に変更します。
src/main/resources の下の applicationContext.xml のファイル名を applicationContext-product.xml に変更します。
applicationContext-product.xml をコピーして applicationContext-unittest.xml を作成します。中は同じです。
作成された applicationContext-unittest.xml が自動で開きますが、画面上部に「Application context not configured for this file」のメッセージが表示されます。右上に表示されている「Configure application context」リンクをクリックします。
「Choose Application Context」ウィンドウが表示されるので、「Spring Application Context」メニューを選択します。選択後少し時間が経つと画面上部の「Application context not configured for this file」のメッセージが消えます。
applicationContext-product.xml をコピーして applicationContext-develop.xml を作成します。作成後、リンク先の内容 に変更します。
作成された applicationContext-develop.xml が自動で開くので、applicationContext-unittest.xml の時と同様に右上に表示されている「Configure application context」リンクをクリックした後、「Choose Application Context」ウィンドウから「Spring Application Context」メニューを選択します。
メインメニューから「File」->「Project Structure...」を選択して「Project Structure」ダイアログを表示します。画面左側で「Project Settings」->「Modules」を選択し、画面中央で「ksbysample-webapp-lending」->「Spring 」を選択すると、画面右側に applicationContext-product.xml, applicationContext-unittest.xml, applicationContext-develop.xml が登録されていることが確認できます。
「OK」ボタンをクリックして「Project Structure」ダイアログを閉じます。
Tomcat が正常に起動することを確認します。Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
Tomcat が正常に起動し、Run View に出力されているログから applicationContext-develop.xml が使用されていることが確認できます。
Ctrl+F2 を押して Tomcat を停止します。
今回はこのまま commit、GitHub へ Push します。
ソースコード
login.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title>ログイン画面</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/> <!-- Bootstrap 3.3.4 --> <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <!-- Font Awesome Icons --> <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css"/> <!-- Ionicons --> <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css"/> <!-- Theme style --> <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css"/> <!-- AdminLTE Skins. Choose a skin from the css/skins folder instead of downloading all of them to reduce the load. --> <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <style> <!-- .content-wrapper { background-color: #fffafa; padding-top: 50px; } .form-group { margin-bottom: 5px; } --> </style> </head> <!-- ADD THE CLASS layout-top-nav TO REMOVE THE SIDEBAR. --> <body class="skin-blue layout-top-nav"> <div class="wrapper"> <!-- Full Width Column --> <div class="content-wrapper"> <div class="container"> <!-- Main content --> <section class="content"> <div class="row"> <div class="col-xs-12 col-md-push-3 col-md-6"> <div class="form-wrap"> <div class="text-center"><h1>ksbysample-lending</h1></div> <br/> <form role="form" action="#" th:action="@{/login}" method="post" id="login-form" autocomplete="off"> <div class="form-group" th:if="${session['SPRING_SECURITY_LAST_EXCEPTION']} != null"> <p class="form-control-static text-danger" th:text="${session['SPRING_SECURITY_LAST_EXCEPTION'].message}"></p> </div> <div class="form-group"> <label for="id" class="sr-only">ID</label> <input type="text" name="id" id="id" class="form-control" placeholder="ID を入力して下さい" th:value="${session['SPRING_SECURITY_LAST_EXCEPTION']} != null ? ${session['SPRING_SECURITY_LAST_EXCEPTION'].authentication.principal} : ''"/> </div> <div class="form-group"> <label for="password" class="sr-only">Password</label> <input type="password" name="password" id="password" class="form-control" placeholder="Password を入力して下さい"/> </div> <div class="form-group text-center"> <div class="checkbox"> <label><input type="checkbox" name="remember-me" id="remember-me" value="true"/>次回から自動的にログインする</label> </div> </div> <button id="btn-login" class="btn btn-primary btn-block">ログイン</button> </form> </div> </div> </div> </section> <!-- /.content --> </div> <!-- /.container --> </div> </div> <!-- ./wrapper --> <!-- jQuery 2.1.4 --> <script src="/js/jQuery-2.1.4.min.js" type="text/javascript"></script> <!-- Bootstrap 3.3.2 JS --> <script src="/js/bootstrap.min.js" type="text/javascript"></script> <!-- AdminLTE App --> <script src="/js/app.min.js" type="text/javascript"></script> <script type="text/javascript"> <!-- $(document).ready(function() { $('#id').focus().select(); }); --> </script> </body> </html>
<div class="form-group text-center"> ... <label><input type="checkbox" name="remember-me" id="remember-me" value="true"/>次回から自動的にログインする</label> ... </div>
の部分を追加します。- チェックボックスの name 属性の値は
remember-me
にします。この名前は org.springframework.security.config.annotation.web.configurers パッケージの下の RememberMeConfigurer.java の中でprivate String rememberMeParameter = "remember-me";
と定義されています。
UserInfoUserDetails.java
package ksbysample.webapp.lending.security; import ksbysample.webapp.lending.entity.UserInfo; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.time.LocalDateTime; import java.util.Collection; import java.util.Set; public class UserInfoUserDetails implements UserDetails { private UserInfo userInfo; private final Set<? extends GrantedAuthority> authorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled; public UserInfoUserDetails(UserInfo userInfo , Set<? extends GrantedAuthority> authorities) { LocalDateTime now = LocalDateTime.now(); this.userInfo = userInfo; this.authorities = authorities; this.accountNonExpired = !userInfo.getExpiredAccount().isBefore(now); this.accountNonLocked = (userInfo.getCntBadcredentials() < 5); this.credentialsNonExpired = !userInfo.getExpiredPassword().isBefore(now); this.enabled = (userInfo.getEnabled() == 1); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { return userInfo.getPassword(); } @Override public String getUsername() { return userInfo.getMailAddress(); } public String getName() { return userInfo.getUsername(); } @Override public boolean isAccountNonExpired() { return accountNonExpired; } @Override public boolean isAccountNonLocked() { return accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return credentialsNonExpired; } @Override public boolean isEnabled() { return enabled; } }
- getUsername メソッドで userInfo.getMailAddress() を返すように変更します。
- user_info.getUsername() の値を返すための getName メソッドを追加します。
- userInfo.getMailAddress() を返すために用意していた getMailAddress メソッドは削除します。
loginsuccess.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.w3.org/1999/xhtml"> <head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title>テスト用ログイン成功ページ</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/> <!-- Bootstrap 3.3.4 --> <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <!-- Font Awesome Icons --> <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css"/> <!-- Ionicons --> <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css"/> <!-- Theme style --> <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css"/> <!-- AdminLTE Skins. Choose a skin from the css/skins folder instead of downloading all of them to reduce the load. --> <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <style> <!-- .content-wrapper { background-color: #fffafa; padding-top: 50px; } .form-group { margin-bottom: 5px; } --> </style> </head> <!-- ADD THE CLASS layout-top-nav TO REMOVE THE SIDEBAR. --> <body class="skin-blue layout-top-nav"> <div class="wrapper"> <!-- Full Width Column --> <div class="content-wrapper"> <div class="container"> <!-- Main content --> <section class="content"> ログイン成功!<br/> ユーザ名: <span sec:authentication="principal.name">Bob</span><br/> メールアドレス: <span sec:authentication="name">test@sample.com</span><br/> Roles: <span sec:authentication="principal.authorities">[ROLE_USER, ROLE_ADMIN]</span><br/> <div sec:authorize="hasRole('ROLE_ADMIN')">ROLE_ADMIN が付与されています</div> <div sec:authorize="hasRole('ROLE_APPROVER')">ROLE_APPROVER が付与されています</div> <div sec:authorize="hasRole('ROLE_USER')">ROLE_USER が付与されています</div> </section> <!-- /.content --> </div> <!-- /.container --> </div> </div> <!-- ./wrapper --> <!-- jQuery 2.1.4 --> <script src="/js/jQuery-2.1.4.min.js" type="text/javascript"></script> <!-- Bootstrap 3.3.2 JS --> <script src="/js/bootstrap.min.js" type="text/javascript"></script> <!-- AdminLTE App --> <script src="/js/app.min.js" type="text/javascript"></script> <script type="text/javascript"> <!-- --> </script> </body> </html>
ユーザ名: <span sec:authentication="name">Bob</span><br/>
→ユーザ名: <span sec:authentication="principal.name">Bob</span><br/>
に変更します。メールアドレス: <span sec:authentication="principal.mailAddress">test@sample.com</span><br/>
→メールアドレス: <span sec:authentication="name">test@sample.com</span><br/>
に変更します。
UserInfoDao.java
package ksbysample.webapp.lending.dao; import ksbysample.webapp.lending.entity.UserInfo; import ksbysample.webapp.lending.util.doma.ComponentAndAutowiredDomaConfig; import org.seasar.doma.Dao; import org.seasar.doma.Delete; import org.seasar.doma.Insert; import org.seasar.doma.Select; import org.seasar.doma.Update; /** */ @Dao @ComponentAndAutowiredDomaConfig public interface UserInfoDao { /** * @param userId * @return the UserInfo entity */ @Select UserInfo selectById(Long userId); @Select UserInfo selectByMailAddress(String mailAddress); /** * @param entity * @return affected rows */ @Insert int insert(UserInfo entity); /** * @param entity * @return affected rows */ @Update int update(UserInfo entity); @Update(sqlFile = true) int incCntBadcredentialsByMailAddress(String mailAddress); @Update(sqlFile = true) int initCntBadcredentialsByMailAddress(String mailAddress); /** * @param entity * @return affected rows */ @Delete int delete(UserInfo entity); }
int initCntBadcredentialsByUsername(String username);
→int initCntBadcredentialsByMailAddress(String mailAddress);
に変更します。
initCntBadcredentialsByMailAddress.sql
update user_info set cnt_badcredentials = 0 where mail_address = /* mailAddress */'tanaka.taro@sample.com'
username = /* username */'tanaka taro'
→mail_address = /* mailAddress */'tanaka.taro@sample.com'
へ変更します。
UserInfoService.java
package ksbysample.webapp.lending.service; import ksbysample.webapp.lending.dao.UserInfoDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserInfoService { @Autowired private UserInfoDao userInfoDao; public void incCntBadcredentials(String mailAddress) { userInfoDao.incCntBadcredentialsByMailAddress(mailAddress); } public void initCntBadcredentials(String mailAddress) { userInfoDao.initCntBadcredentialsByMailAddress(mailAddress); } }
- initCntBadcredentials メソッド内で呼び出すメソッドを
userInfoDao.initCntBadcredentialsByUsername
→userInfoDao.initCntBadcredentialsByMailAddress
へ変更します。
WebSecurityConfig.java
package ksbysample.webapp.lending.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @SuppressWarnings("SpringJavaAutowiringInspection") @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 認証の対象外にしたいURLがある場合には、以下のような記述を追加します // 複数URLがある場合はantMatchersメソッドにカンマ区切りで対象URLを複数列挙します // .antMatchers("/country/**").permitAll() .antMatchers("/fonts/**").permitAll() .antMatchers("/html/**").permitAll() .antMatchers("/encode").permitAll() .anyRequest().authenticated(); http.formLogin() .loginPage("/") .loginProcessingUrl("/login") .defaultSuccessUrl("/loginsuccess") .usernameParameter("id") .failureUrl("/") .passwordParameter("password") .permitAll() .and() .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutSuccessUrl("/") .deleteCookies("JSESSIONID") .deleteCookies("remember-me") .invalidateHttpSession(true) .permitAll() .and() .rememberMe() .key("ksbysample-webapp-lending") .tokenValiditySeconds(60 * 60 * 24 * 30); } @Bean public AuthenticationProvider daoAuhthenticationProvider() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailsService); daoAuthenticationProvider.setHideUserNotFoundExceptions(false); daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder()); return daoAuthenticationProvider; } @Autowired public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(daoAuhthenticationProvider()) .userDetailsService(userDetailsService); } }
- configure メソッド内の処理の以下の点を変更します。
- logout 時の処理に
.deleteCookies("remember-me")
を追加します。 .and().rememberMe() ... .tokenValiditySeconds(60 * 60 * 24 * 30);
を追加します。Remember-me 機能が生成する Token の有効期限を 30日にします。
- logout 時の処理に
- configAuthentication メソッド内の処理に
.userDetailsService(userDetailsService)
を追加します。これは Remember-me 機能が UserDetailsService 必須のため、AuthenticationProvider Bean で設定したのとは別に設定する必要があるためです。
Application.java
package ksbysample.webapp.lending; import org.apache.commons.lang3.StringUtils; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ImportResource; import java.text.MessageFormat; @ImportResource("classpath:applicationContext-${spring.profiles.active}.xml") @SpringBootApplication public class Application { public static void main(String[] args) { String springProfilesActive = System.getProperty("spring.profiles.active"); if (!StringUtils.equals(springProfilesActive, "product") && !StringUtils.equals(springProfilesActive, "develop") && !StringUtils.equals(springProfilesActive, "unittest")) { throw new UnsupportedOperationException(MessageFormat.format("JVMの起動時引数 -Dspring.profiles.active で develop か unittest か product を指定して下さい ( -Dspring.profiles.active={0} )。", springProfilesActive)); } SpringApplication.run(Application.class, args); } }
- @ImportResource アノテーションの中で指定する xml ファイル名を
applicationContext.xml
→applicationContext-${spring.profiles.active}.xml
へ変更します。
applicationContext-develop.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*" rollback-for="Exception"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="pointcutService" expression="execution(* ksbysample.webapp.lending..*Service.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcutService"/> </aop:config> </beans>
<tx:method name="*" rollback-for="Exception" timeout="3"/>
→<tx:method name="*" rollback-for="Exception"/>
へ変更します。timeout の設定を取り除きます。
履歴
2015/07/22
初版発行。