レガシーなWebアプリケーションの開発と比較して、Spring Bootベースのアプリケーションの開発生産性はどの程度の違うのでしょうか?
少し古めの技術がベースのWebアプリケーション「EasyBuggy」と、そのクローン(全く同じ機能)であるSpring Bootベースの「EasyBuggy Boot」を、開発生産性に影響する次の数値で比較してみました。
- ビルドにかかる時間
- 起動時間
- ソースコードの修正から動作確認までの時間
- ソースコードの行数
構成の相違点
ちなみに、主な構成の相違点は以下の通りです。
相違点 | EasyBuggy | EasyBuggy Boot |
---|---|---|
ベースとなる技術 | Servlet 3.0.1 | Spring Boot 1.5.6 (Servlet 3.0.1) |
プレゼンテーション層 | 未使用 (一部 JSP 2.2 + JSTL 1.2) | Thymeleaf 2.1.5 (一部 JSP 2.3 + JSTL 1.2) |
Javaコンテナ | Apache Tomcat/7.0.37 | Apache Tomcat/8.5.16 |
DBクライアント/サーバー | JDBC / Derby 10.8.3.0 | Spring JDBC 4.3.9 / Derby 10.12.1.1 (Java 7の場合)、または10.13.1.1 (Java 8の場合) |
LDAPクライアント/サーバー | Apache DS Client API 1.0.0 / Server 1.5.5 | Spring LDAP 2.3.1 / unboundid-ldapsdk 3.2.1 |
メール | JavaMail 1.5.1 | JavaMail 1.5.1 (Spring Boot Mailで導入されるJavaMail 1.5.6をオーバーライド) |
開発ツール | 無し | Spring Boot Developer Tools 1.5.6 |
Java | Java 6以上をサポート | Java 7以上をサポート |
ビルドにかかる時間
まずは、ビルドにかかる時間の平均です。結果は以下の通りになりました。
EasyBuggy
約6秒
$ mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building easybuggy 1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
・・・(長いので省略)・・・
[INFO] --- tomcat7-maven-plugin:2.1:exec-war-only (tomcat-run) @ easybuggy ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.847s
[INFO] Finished at: Thu Aug 31 23:28:55 JST 2017
[INFO] Final Memory: 36M/220M
[INFO] ------------------------------------------------------------------------
EasyBuggy Boot
約12秒
$ mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building easybuggy4sb 1.0.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
・・・(長いので省略)・・・
[INFO] --- spring-boot-maven-plugin:1.5.6.RELEASE:repackage (default) @ easybuggy4sb ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 12.448s
[INFO] Finished at: Thu Aug 31 23:26:55 JST 2017
[INFO] Final Memory: 26M/242M
[INFO] ------------------------------------------------------------------------
倍以上の差で前者の方が早いです。
ちなみに検証に使った環境はVMWare上のCentOS 6.9、CPU 4コア(Intel® Xeon® X5680 @3.33GHz)、メモリ4GBです。依存ライブラリは全てローカルにダウンロード済みです。
起動時間
起動にかかる時間の平均は以下の通りです。
EasyBuggy
約3秒
$ java -jar easybuggy.jar
8 31, 2017 2:44:56 午後 org.apache.coyote.AbstractProtocol init
情報: Initializing ProtocolHandler ["http-bio-8080"]
8 31, 2017 2:44:56 午後 org.apache.catalina.core.StandardService startInternal
情報: Starting service Tomcat
8 31, 2017 2:44:56 午後 org.apache.catalina.core.StandardEngine startInternal
情報: Starting Servlet Engine: Apache Tomcat/7.0.37
8 31, 2017 2:44:59 午後 org.apache.coyote.AbstractProtocol start
情報: Starting ProtocolHandler ["http-bio-8080"]
EasyBuggy Boot
約15秒(mvn spring-boot:run
の場合は約9秒)
$ java -jar ROOT.war
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/Users/ktamura/git/easybuggy4sb/target/ROOT.war!/WEB-INF/lib/logback-classic-1.1.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/ktamura/git/easybuggy4sb/target/ROOT.war!/WEB-INF/lib/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.5.6.RELEASE)
2017-08-31 15:08:46.454 INFO 5460 --- [ main] o.t.e.Easybuggy4sbApplication : Starting Easybuggy4sbApplication v1.0.0-SNAPSHOT on ktamura-PC with PID 5460 (C:\Users\ktamura\git\easybuggy4sb\target\ROOT.war started by k
tamura in C:\Users\ktamura\git\easybuggy4sb)
・・・(長いので省略)・・・
2017-08-31 15:09:01.198 INFO 5460 --- [ main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0
2017-08-31 15:09:01.273 INFO 5460 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-08-31 15:09:01.276 INFO 5460 --- [ main] o.t.e.Easybuggy4sbApplication : Started Easybuggy4sbApplication in 15.253 seconds (JVM running for 15.81)
EasyBuggyの起動が非常に早いので、15秒でも遅く感じます。EasyBuggy Bootのログを見たところ、極端に時間を食いつぶしてる処理があるわけではないので、改善の余地はあまり無いかもしれません。Spring Bootの起動時間は最低でもそのくらいかかるということだと思います。
なお、この結果は組み込みのTomcatでの結果ですが、組み込みではないTomcatにwarファイルをデプロイした場合でもほぼ同じ結果となりました。
ただし、EasyBuggy Bootはmvn spring-boot:run
コマンドで起動すると9秒程度になります。なぜこの差が出るのか、両者のログを比較してみたのですが、実行している処理に大きな差は無さそうで、明確な理由は分かりませんでした。
ソースコードの修正から動作確認までの時間
ソースコードを修正してから動作確認ができる状態になるまで時間の平均は以下の通りです。
EasyBuggy
約15秒
EasyBuggyはmvn install
コマンドでjarファイルの作成から起動まで一気に行います。これにかかる時間と停止を合わせて、再起動には15秒程度の時間がかかります。もちろんJVMのホットスワップで、デバッグしながら修正を反映させるので、実際の開発ではそれほど時間はかかりませんが...(さらにJRebelを導入すればもっと早くなります)。
EasyBuggy Boot
約3秒
2017-09-01 12:04:55.414 INFO 2800 --- [ Thread-104] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@27231264: startup date [Fri Sep 01 12:04:21 JST 2017]; root of context hierarchy
2017-09-01 12:04:55.417 INFO 2800 --- [ Thread-104] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4274ec33: startup date [Fri Sep 01 12:04:23 JST 2017]; parent: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@27231264
2017-09-01 12:04:55.532 INFO 2800 --- [ Thread-104] o.s.c.support.DefaultLifecycleProcessor : Stopping beans in phase 0
2017-09-01 12:04:55.537 WARN 2800 --- [ Thread-104] o.s.b.f.support.DisposableBeanAdapter : Invocation of destroy method failed on bean with name 'inMemoryDatabaseShutdownExecutor': java.sql.SQLSyntaxErrorException: Syntax error: Encountered "SHUTDOWN" at line 1, column 1.
2017-09-01 12:04:55.538 INFO 2800 --- [ Thread-104] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.5.6.RELEASE)
・・・(長いので省略)・・・
2017-09-01 12:04:57.895 INFO 2800 --- [ restartedMain] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8081 (http)
2017-09-01 12:04:57.897 INFO 2800 --- [ restartedMain] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0
2017-09-01 12:04:57.923 INFO 2800 --- [ restartedMain] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-09-01 12:04:57.925 INFO 2800 --- [ restartedMain] o.t.e.Easybuggy4sbApplication : Started Easybuggy4sbApplication in 2.125 seconds (JVM running for 828.681)
EasyBuggy BootはSpring Boot Developer Tools 1.5.6を導入しているので、修正が即時反映されて、ほとんど待つことなく開発ができます。このスピード感はとても快適です。
ソースコードの行数
ソースコードの行数を比較してみます。
EasyBuggy
種類 | 空行 | コメント行 | コード | ファイル数 |
Java | 838 | 359 | 4881 | 98 |
JSP | 11 | 10 | 810 | 6 |
XML | 5 | 0 | 47 | 2 |
HTML | 2 | 0 | 35 | 7 |
properties | 71 | 325 | 799 | 6 |
合計 | 927 | 694 | 6572 | 119 |
EasyBuggy Boot
種類 | 空行 | コメント行 | コード | ファイル数 |
Java | 663 | 131 | 3500 | 92 |
HTML | 8 | 39 | 1034 | 46 |
JSP | 0 | 4 | 149 | 2 |
XML | 0 | 1 | 32 | 4 |
SQL | 0 | 0 | 18 | 3 |
JSON | 0 | 0 | 17 | 1 |
DTD | 0 | 0 | 3 | 1 |
LDIF | 0 | 5 | 46 | 1 |
properties | 86 | 321 | 830 | 4 |
合計 | 757 | 501 | 5629 | 154 |
EasyBuggyのソースコードの行数が6,572なのに対して、EasyBuggy Bootは5,629なので、Spring Bootを使うことで15%程度の削減ができています。ファイル数にはおよそ1.3倍の差がありますが、EasyBuggyが画面とロジックを別ファイル(JSPなど)に分離しなかったためだと思います(※1つのバグを1ファイルで説明したかったため、そういうつくりにしました)。
[おまけ] ソースコードの可読性
ソースコードの可読性はどう変わったでしょうか?次のような機能に対して、
それぞれのソースコードは以下のようになっています。
EasyBuggy
package org.t246osslab.easybuggy.vulnerabilities;
import java.io.IOException;
import java.util.Locale;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.t246osslab.easybuggy.core.utils.HTTPResponseCreator;
import org.t246osslab.easybuggy.core.utils.MessageUtils;
@SuppressWarnings("serial")
@WebServlet(urlPatterns = { "/xss" })
public class XSSServlet extends HttpServlet {
private static final Logger log = LoggerFactory.getLogger(XSSServlet.class);
@Override
protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
String string = req.getParameter("string");
Locale locale = req.getLocale();
StringBuilder bodyHtml = new StringBuilder();
bodyHtml.append("<form action=\"xss\" method=\"post\">");
bodyHtml.append(MessageUtils.getMsg("description.reverse.string", locale));
bodyHtml.append("<br><br>");
bodyHtml.append(MessageUtils.getMsg("label.string", locale) + ": ");
bodyHtml.append("<input type=\"text\" name=\"string\" size=\"100\" maxlength=\"100\">");
bodyHtml.append("<br><br>");
bodyHtml.append("<input type=\"submit\" value=\"" + MessageUtils.getMsg("label.submit", locale) + "\">");
bodyHtml.append("<br><br>");
if (!StringUtils.isBlank(string)) {
// Reverse the given string
String reversedName = StringUtils.reverse(string);
bodyHtml.append(MessageUtils.getMsg("label.reversed.string", locale) + " : "
+ reversedName);
} else {
bodyHtml.append(MessageUtils.getMsg("msg.enter.string", locale));
}
bodyHtml.append("<br><br>");
bodyHtml.append(MessageUtils.getInfoMsg("msg.note.xss", locale));
bodyHtml.append("</form>");
HTTPResponseCreator.createSimpleResponse(req, res, MessageUtils.getMsg("title.xss.page", locale),
bodyHtml.toString());
}
}
EasyBuggy Boot
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" th:with="lang=${#locale.language}" th:lang="${lang}">
<div th:replace="head"></div>
<body style="margin-left: 20px; margin-right: 20px;">
<div th:replace="header"></div>
<form action="xss" method="post">
<p th:text="#{description.reverse.string}" /><br />
<label th:text="#{label.string}"></label><span>: </span>
<input type="text" name="string" size="100" maxlength="100" /><br /><br />
<input type="submit" /><br /><br />
<p th:utext="${msg}" /><br />
<div class="alert alert-info" role="alert">
<span class="glyphicon glyphicon-info-sign" th:utext="#{msg.note.xss}"></span>
</div>
</form>
</body>
</html>
package org.t246osslab.easybuggy4sb.vulnerabilities;
import java.util.Locale;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class XSSController {
@Autowired
MessageSource msg;
@RequestMapping(value = "/xss")
public ModelAndView process(@RequestParam(value = "string", required = false) String string, ModelAndView mav,
Locale locale) {
mav.setViewName("xss");
mav.addObject("title", msg.getMessage("title.xss.page", null, locale));
if (!StringUtils.isBlank(string)) {
// Reverse the given string
String reversedName = StringUtils.reverse(string);
mav.addObject("msg", msg.getMessage("label.reversed.string", null, locale) + " : " + reversedName);
} else {
mav.addObject("msg", msg.getMessage("msg.enter.string", null, locale));
}
return mav;
}
}
機能自体が単純なので、読みやすさには大きな差は無いかもしれませんが、後者の方がプログラマーとデザイナーの作業分担がしやすいといえます。もちろん前者もJSPを使えば、それに近いものになります。