使用Spring Boot + Thymeleaf + Tomcat + Gradle开发业务应用时的6个问题
有许多框架可以满足想要用Java制作WEB应用程序的需求,但其中Spring Boot在其中具有重要的存在感。
由于收到了希望能快速建立业务应用程序的需求,所以我选择使用了Spring Boot进行开发。
由于遇到了一些问题,我做了一些笔记,现在与大家分享一下。
※请注意:以下内容是为了分享使用过程中遇到的困难而写下的。
※如果您没有使用过Spring Boot,我认为这篇文章可能会对您有所帮助:http://qiita.com/opengl-8080/items/05d9490d6f0544e2351a
整个系统的结构
・框架:Spring Boot
・运行环境:开发中使用内嵌Tomcat。生产环境使用Tomcat7。
・登录认证:Spring Security
・表示层:Thymeleaf。使用Bootstrap + Flat UI来美化界面。
・语言:Java SE 8
・运行时假设使用HTTPS通信
・前端使用Apache2.2
由于开发团队中使用Eclipse的用户较多,所以选择了Eclipse4.4版本。并定义了Checkstyle与CodeFormatter,供所有人共享使用。
1:定义开发环境挺麻烦的呢…
使用Spring Boot可以使用gradle + application.properties几乎完成环境配置。然而,从零开始构建需要花费时间,因此最好尽量从Spring Boot官方网页中获取。然而,每一行都有意义,所以不能随意翻译。
apply plugin: "java"
apply plugin: "spring-boot"
apply plugin: "war"
apply plugin: "eclipse"
description = "XXXXXXX"
war {
baseName = "XXXXXXX"
}
configurations.all {
resolutionStrategy {
eachDependency {
if (it.requested.group == "org.apache.tomcat.embed") {
it.useVersion "7.0.59"
}
}
}
}
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE"
classpath "org.springframework:springloaded:1.2.3.RELEASE"
}
}
configurations {
providedRuntime
}
dependencies {
def springBootVersion="1.2.3.RELEASE"
compile "net.sf.dozer:dozer:5.5.1"
compile "org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}"
compile "org.springframework.boot:spring-boot-starter-thymeleaf:${springBootVersion}"
compile "org.springframework.boot:spring-boot-starter-security:${springBootVersion}"
compile "org.thymeleaf.extras:thymeleaf-extras-springsecurity3:2.1.2.RELEASE"
compile "org.springframework.boot:spring-boot-starter-web:${springBootVersion}"
providedRuntime "org.springframework.boot:spring-boot-starter-tomcat:${springBootVersion}"
testCompile "org.springframework:spring-test"
testCompile "org.springframework.boot:spring-boot-starter-test:${springBootVersion}"
//ここにjdbcを入れてね
}
如果开启热部署功能,即使在运行时也可以进行重新加载。这是一个必备功能。
org.springframework:springloaded:1.2.3.RELEASE 可以改写为
org.springframework:springloaded:1.2.3.RELEASE
・spring-boot-starter-tomcat 是内置的 Tomcat。开发者可以通过 Eclipse 的运行按钮启动应用程序,而无需安装 Tomcat。然而,在使用 Tomcat 运行环境时,内置的 Tomcat 可能会造成干扰,因此需要进行以下操作。
提供运行时环境 “org.springframework.boot:spring-boot-starter-tomcat:${springBootVersion}”
将ConnectionPool的管理交给Tomcat处理。
接下来是/src/main/resources/application.properties。
请继续/src/main/resources/application.properties。
spring.datasource.url=jdbc:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
spring.datasource.username=XXXX
spring.datasource.password=XXXX
spring.datasource.driverClassName=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
#コネクションプール設定(default)
spring.datasource.max-active=100
spring.datasource.max-idle=8
spring.datasource.min-idle=8
spring.datasource.initial-size=10
#セッションタイムアウトを1800秒とする。
spring.datasource.session-timeout=1800
#コネクションを利用する際に検証を行う。DBが再起動していてもこの処理を挟むことでtomcatを再起動しなくても済む
spring.datasource.test-on-borrow=true
spring.datasource.validation-query=SELECT 1
#コミットされずに残ったコネクションは60秒後に破棄される。
spring.datasource.remove-abandoned=true
spring.datasource.remove-abandoned-timeout=60
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html
spring.thymeleaf.cache=false
server.session-timeout=1800
server.context-path=/XXXXXXXX
spring.messages.cache-seconds=-1
error.whitelabel.enabled=false
security.require_ssl=true
最好是提前定义 server.context,这样会更轻松。我在数据库方面遇到了一些困惑。我进行了多次测试,关闭和重新启动数据库,最终得到了这个结论。
2:当会话超时时该怎么办?
为什么业务系统要重视会话超时?虽然可以添加,但实际实施复杂。
在超时情况下,有两种模式。
-
- 在超时后尝试进行页面跳转时:
-
- 如果发生了会话超时,会跳转到登录页面,并显示消息:“会话已超时”。
-
- 但是,登录页面和登出页面不受会话超时的影响。
-
- 在超时后尝试进行Ajax通信时:
- 我们创建了一个专门用于Ajax的共享响应模型,在任何页面中都返回此响应模型。
private boolean success = false;
private List<String> messages = new ArrayList<>();
private Object data;
+ getter / setter
只要在所有API中都返回这个模型,就可以返回超时消息(并且可能会在模态框中显示)。
好的,让我们开始实现。
在一个拦截器中写下以下代码。代码示例如下:
/**
* セッションタイムアウト時の挙動を定義する。
*/
public class SessionExpireInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
if (isTarget(request)) {
// セッションタイムアウトのチェック
if (isSessionTimeout(request)) { // セッションタイムアウトの場合
// Ajax処理の場合は「セッションタイムアウトが発生しました。ログインしなおしてください。」というメッセージを返却する。
if (isAjaxRequest(request)) {
String message =
"{\"success\":false,\"messages\":[\"セッションタイムアウトが発生しました。ログインしなおしてください。\"]}";
response.setContentType("text/html;charset=UTF-8");
// ステータスは200となるが、success = falseとする。
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write(message);
return false;
} else {
String requestUri = request.getRequestURI();
// ログイン・ログアウト・タイムアウトページのタイムアウトは行わない。
if (!((request.getContextPath() + AppConst.URL_LOGIN_PAGE + "/timeout")
.equals(requestUri) //
|| (request.getContextPath() + AppConst.URL_LOGIN_PAGE)
.equals(requestUri) //
|| (request.getContextPath() + AppConst.URL_LOGOUT_COMPLETED_PAGE)
.equals(requestUri))) {
// 通常画面の場合は、ログイン画面を表示しセッションタイムアウトメッセージを伝える。
response.sendRedirect(request.getContextPath() + AppConst.URL_LOGIN_PAGE
+ "/timeout");
}
}
}
}
return true;
}
/**
* 対象のリクエストか判定
* css, js, favicon等のリソースアクセスについては出力対象外
*
* @param request リクエスト
* @return true : ロギング対象 / false : ロギング対象ではない
*/
private boolean isTarget(ServletRequest request) {
String uri = ((HttpServletRequest) request).getRequestURI();
return (uri.indexOf("/static") < 0 && uri.indexOf("/favicon.ico") < 0);
}
/**
* Ajaxリクエスト判定
*
* @param request リクエスト
* @return true : Ajaxリクエスト / false : そうではない
*/
private boolean isAjaxRequest(ServletRequest request) {
return StringUtils.equals("XMLHttpRequest",
((HttpServletRequest) request).getHeader("X-Requested-With"));
}
/**
* セッションタイムアウト判定
*
* @param request
* @return
*/
private boolean isSessionTimeout(HttpServletRequest request) {
HttpSession falsecurrentSession = request.getSession(false);
if (falsecurrentSession == null) {
return true;
}
String requestSession = request.getRequestedSessionId();
boolean isValid = request.isRequestedSessionIdValid();
return falsecurrentSession == null || requestSession == null || !isValid
|| !requestSession.equals(falsecurrentSession.getId());
}
@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 {}
}
使用Spring Security为一个用户分配多个权限的方法
引入了能够处理登录验证的Spring Security。本次规则如下。
· 每个用户只能拥有一个角色。
· 每个角色可以拥有多个权限。
· 角色示例: “管理员”, “操作员”, “普通用户”。
· 权限示例: 功能A_只读权限/功能A_更新权限/功能B_只读权限/功能B_更新权限。
· 用户登录后,如果没有权限,将不会显示打开该页面的按钮。直接输入URL也会显示权限错误。
代码横跨多个类,但只需要抽取部分内容。
/**
* 認証処理を実施するクラス
*/
@Service
public class UserDetailLogic implements UserDetailsService {
@Autowired
UserLogic userLogic;
private Logger logger = LoggerFactory.getLogger(UserDetailLogic.class);
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// ユーザ情報取得
MUser mUser = userLogic.findUserByUserId(username);
if (mUser == null) {
String errorMessage = **********************************************
logger.info(errorMessage);
throw new UsernameNotFoundException(errorMessage);
}
// 権限をセットする。
List<SimpleGrantedAuthority> authorityList = new ArrayList<SimpleGrantedAuthority>();
// 権限取得
List<MRRoleAuthority> auths = userLogic.getAuthorities(mUser.getRoleId());
for (MRRoleAuthority auth : auths) {
authorityList.add(new SimpleGrantedAuthority(auth.getId().getAuthorityName()));
}
return new UserDetail(mUser, authorityList);
}
}
如果控制器上写上以下内容,那么当没有权限的用户到来时,可以将其转至403错误页面。
@PreAuthorize("hasRole('SELECT_ZYX')") // ZYXの参照権が必要
@RequestMapping(value = "/")
public String index(Model model) {
....
}
如果在页面上按照以下方式编写,只有在具备权限时才会创建可见的按钮。
<li sec:authorize="hasRole('SELECT_ZYX')"><a th:href="@{/sites/}">ZYX機能</a></li>
4:你是怎么进行验证的?
客户端验证已进行自定义实现。通过调用Validator.checkRule(“域规则名称”, 值),可以返回相应的消息实现。
将消息存储在messages中,并在有消息时显示错误模态框。
messages.push({
message: Validator.checkRule("NAME_255", $("#zyxName").val()),
arg: "名称"
});
messages.push({
message: Validator.checkRule("ID_8", $("#zyxId").val()),
arg: "ID"
});
messages.push({
message: Validator.checkRule("URL", $("#zyxUrl").val()),
arg: "URL"
});
服务器端验证采用了@BindingResult。只需将@Length(max = 10)或@Pattern(regexp=正则表达式)等验证规则作为参数传递给模型对象的字段,可以轻松地编写验证规则。
@Pattern(regexp = ValidationConst.PATTERN_URL_POLICY,
message = ValidationConst.ERROR_MESSAGE_MATCH)
@Length(max = ValidationConst.VALIDATION_RULE_URL,
message = ValidationConst.ERROR_MESSAGE_MAXSIZE)
private String url = "";
用接收器将消息转换并返回到屏幕。
@PreAuthorize("hasRole('UPDATE_ZYX')")
@RequestMapping(value = "/entry", method = RequestMethod.POST)
@ResponseBody
public CommonResultModel executeEntry(@RequestBody @Valid ZyxModel zxyInfoForm,
BindingResult bindingResult) {
CommonResultModel result = new CommonResultModel();
// parameter check result has error -> return error messages
if (bindingResult.hasErrors()) {
for (FieldError fe : bindingResult.getFieldErrors()) {
result.addMessage(fe.getDefaultMessage(), fe.getField(), fe.getRejectedValue());
}
logger.warn(result.getMessages().toString()); // server side validation -> warn
return result;
}
让我们使用共同的日志记录。
有些项目要求每次在应用程序中记录类似于“已开始○○。用户名:hoge”和“已结束××。参数:bar=foo”这样的日志。由于清楚地显示了哪个用户进行了什么操作,所以我很喜欢。这次我使用了过滤器来记录“哪个用户进行了什么操作?”的日志输出,并用过滤器进行了描述。
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// HTTPリクエストの場合、処理開始ログを出力
if (request instanceof HttpServletRequest) {
String uri = ((HttpServletRequest) request).getRequestURI();
String method = ((HttpServletRequest) request).getMethod();
// 開始ロギング対象のアクセスの場合はログ出力
if (isStartLoggingTarget(request)) {
String messageCode = getProcessStartMessageCode(request);
String userId = getUserIdFromSession(request);
logger.info(MessageUtil.make(messageCode, userId, method, uri));
}
}
// 実処理実施
chain.doFilter(request, response);
// HTTPリクエストの場合、処理終了ログを出力
if (request instanceof HttpServletRequest) {
String uri = ((HttpServletRequest) request).getRequestURI();
String method = ((HttpServletRequest) request).getMethod();
// 終了ロギング対象のアクセスの場合はログ出力
if (isEndLoggingTarget(request)) {
String messageCode = MessageUtil.MC_COMMON_API_END;
String userId = getUserIdFromSession(request);
logger.info(MessageUtil.make(messageCode, userId, method, uri));
}
}
}
提前确定日志输出策略。必须进行应用程序开发和管理(监控)人员以及日志文件的轮转的协商。
使用SpringSecurity创建登录页面。在HTTP通信中没有问题,但在使用HTTPS通信时,登录后的URL会出现异常。
具体来说,情况是这样的。
https://example.com:80/app1/logined
虽然使用了https,但却试图连接到80端口。
如果将其交给SpringSecurity处理,登录后的URL将被重定向。
在Tomcat的server.xml中,将proxyPort定义为443。
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" proxyPort="443" scheme="https" secure="true"/>
外传
减少工作量!减少工作量!在一瞬之间分神时,不知怎地就变成了不制造403/404/500的方针。但最终还是不得不制造。要好好安排工作量。很简单。
/**
* ServletContainerCustomizer
*
* @return
*/
@Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
return new ServletContainerCustomizer();
}
private static class ServletContainerCustomizer implements EmbeddedServletContainerCustomizer {
@Override
public void customize(ConfigurableEmbeddedServletContainer factory) {
factory.addErrorPages(new ErrorPage(HttpStatus.FORBIDDEN, "/403"));
factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/404"));
factory.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500"));
}
}
/**
* 403エラー画面表示
*
* @return
*/
@RequestMapping("/403")
public String forbiddenError() {
return "error/403";
}
/**
* 404エラー画面表示
*
* @return
*/
@RequestMapping("/404")
public String notFoundError() {
return "error/404";
}
/**
* 500エラー画面表示
*
* @return
*/
@RequestMapping("/500")
public String internalServerError() {
return "error/500";
}
如果准备好{项目根目录}/src/main/resources/template/error/500.html等,它会在出错时显示出来。
尝试使用SpringBoot…
这个地方很好:即使不安装Tomcat也可以运行,非常方便。环境配置几乎可以通过gradle + application.properties完成。
不足之处:文档量少。绊倒的地方很多。没有共享绊倒的地方。测试代码启动缓慢。需要花费时间适应测试。基本上不考虑介绍Spring 3之前的页面。
介绍了使用XML编写的方法,但不知道如何用Java重新编写。
Spring相关功能过于丰富,不知道从哪里开始做起,很困惑。
春天本身变得多功能且复杂化,因此出现了可以组合SpringBoot组件的功能。但与Django、Rails等全栈框架相比,它更加臃肿且难以处理。但是,如果是Java项目,这似乎是唯一的选择…