使用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:当会话超时时该怎么办?

为什么业务系统要重视会话超时?虽然可以添加,但实际实施复杂。

在超时情况下,有两种模式。

    1. 在超时后尝试进行页面跳转时:

 

    1. 如果发生了会话超时,会跳转到登录页面,并显示消息:“会话已超时”。

 

    1. 但是,登录页面和登出页面不受会话超时的影响。

 

    1. 在超时后尝试进行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也会显示权限错误。

2015y10m18d_160505348.jpg

代码横跨多个类,但只需要抽取部分内容。

 /**
 * 認証処理を実施するクラス
 */
@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项目,这似乎是唯一的选择…

广告
将在 10 秒后关闭
bannerAds