AWSのAmazonLinux上にiPhoneの電卓のようなアプリを作ってみました。
(※内容はChatGPTに質問して返されたものです。)
全体構成
・OS:Amazon Linux 2023
・Webサーバ:Apache HTTP Server(mod_proxy_ajpでTomcatへ中継)
・アプリサーバ:Apache Tomcat 10
・Java:OpenJDK 17
・アプリ:電卓(HTML+Servlet)
※2025年11月現在の前提です。EC2の作成手順は割愛します。
1. 環境構築手順
(1) 依存パッケージとJavaの導入
sudo dnf update -y
sudo dnf install -y java-17-amazon-corretto-devel
(2) Apache HTTP Server のインストール
sudo dnf install -y httpd
sudo systemctl enable httpd
sudo systemctl start httpd
ブラウザで http://EC2のパブリックIP/ にアクセスし、「It works!」と出ればOK。
(3) Tomcat のインストール
cd /opt
sudo wget https://archive.apache.org/dist/tomcat/tomcat-10/v10.1.30/bin/apache-tomcat-10.1.30.tar.gz
sudo tar xzf apache-tomcat-10.1.30.tar.gz
sudo mv apache-tomcat-10.1.30 tomcat
sudo chown -R ec2-user:ec2-user /opt/tomcat
(4) Tomcat の起動
/opt/tomcat/bin/startup.sh
ブラウザで
http://EC2のパブリックIP:8080 にアクセスし、Tomcatのトップ画面が表示されればOK。
(5) Apache と Tomcat の連携(mod_proxy_ajp設定)
/etc/httpd/conf.d/tomcat_proxy.confを新規で作成して下記を記述。
<VirtualHost *:80>
ServerName localhost
ProxyRequests Off
# AJP経由でTomcatへ転送
ProxyPass / ajp://localhost:8009/
ProxyPassReverse / ajp://localhost:8009/
# ログ出力
ErrorLog /var/log/httpd/tomcat_proxy_error.log
CustomLog /var/log/httpd/tomcat_proxy_access.log combined
</VirtualHost>
AJPを使ってApache(httpd)とTomcatを連携する。
/opt/tomcat/conf/server.xmlを下記のように修正。
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector protocol="AJP/1.3"
address="127.0.0.1"
port="8009"
redirectPort="8443"
maxParameterCount="1000"
secretRequired="false" />
<!-- Define an AJP 1.3 Connector on port 8009 --> の、1行下の
<!--
と、 secretRequired="false" /> の、1行下の
-->
は、削除してください。
Apache(httpd)サービスの再起動を実施。
sudo systemctl restart httpd
Tomcatサービスの再起動を実施。
/opt/tomcat/bin/shutdown.sh
/opt/tomcat/bin/startup.sh
→ これで http://EC2のパブリックIP/ にアクセスすると Apache経由でTomcatのアプリに到達します。
2. 電卓環境構築
(1)ディレクトリ構成を作る
Tomcat の標準アプリ配置先 /opt/tomcat/webapps/ にアプリフォルダを作ります。
cd /opt/tomcat/webapps/
mkdir -p calc/WEB-INF/classes
構成は下記のようになります。
/opt/tomcat/webapps/calc/
└ index.html(あとで作成)
└ WEB-INF/
└ web.xml(あとで作成)
└ classes/
└ CalcServlet.java(あとで作成)
(2)画面の作成
/opt/tomcat/webapps/calc/index.htmlを新規作成して下記を記載します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" />
<title>電卓</title>
<style>
:root { --bg:#111; --fg:#fff; --sub:#9aa0a6; --key:#2a2a2a; --op:#ff9500; --acc:#4a4a4a; }
* { box-sizing:border-box; -webkit-tap-highlight-color: transparent; }
body {
margin:0; background:var(--bg); color:var(--fg);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans JP", sans-serif;
display:grid; place-items:center; height:100dvh;
}
.calc {
width:min(420px, 100vw);
padding:16px 16px 28px;
display:grid; gap:12px;
}
.display {
width:100%; min-height:96px; background:#000; border-radius:18px; padding:18px;
display:flex; align-items:flex-end; justify-content:flex-end; overflow:hidden;
box-shadow: inset 0 0 0 1px #222;
}
.expr { font-size:16px; color:var(--sub); margin-right:8px; user-select:none; }
.value { font-size:42px; font-weight:600; word-break:break-all; }
.keys {
display:grid; gap:12px;
grid-template-columns: repeat(4, 1fr);
}
button {
height:64px; border:none; border-radius:18px; font-size:22px; font-weight:600;
background:var(--key); color:var(--fg); cursor:pointer;
box-shadow: 0 2px 0 #0007;
transition: transform .03s ease-out, filter .1s;
}
button:active { transform: translateY(1px); filter:brightness(1.1); }
.op { background:var(--op); color:#fff; }
.acc { background:var(--acc); color:#fff; }
.wide { grid-column: span 2; }
.zero { grid-column: span 2; text-align:left; padding-left:22px; }
</style>
</head>
<body>
<div class="calc">
<div class="display">
<div class="expr" id="expr"></div>
<div class="value" id="value">0</div>
</div>
<div class="keys">
<button class="acc" data-act="clear">AC</button>
<button class="acc" data-act="sign">±</button>
<button class="acc" data-act="percent">%</button>
<button class="op" data-op="÷">÷</button>
<button data-num="7">7</button>
<button data-num="8">8</button>
<button data-num="9">9</button>
<button class="op" data-op="×">×</button>
<button data-num="4">4</button>
<button data-num="5">5</button>
<button data-num="6">6</button>
<button class="op" data-op="-">-</button>
<button data-num="1">1</button>
<button data-num="2">2</button>
<button data-num="3">3</button>
<button class="op" data-op="+">+</button>
<button class="zero" data-num="0">0</button>
<button data-dot=".">.</button>
<button class="op wide" data-act="equals">=</button>
</div>
</div>
<script>
const v = document.getElementById('value');
const e = document.getElementById('expr');
let num1 = null, op = null, typing = false;
const mapToServer = (symbol) => ({'+':'+','-':'-','×':'*','÷':'/'}[symbol] || symbol);
const setValue = (txt) => v.textContent = txt;
const setExpr = (txt) => e.textContent = txt;
const appendDigit = (d) => {
if (!typing || v.textContent === '0') { setValue(d); typing = true; }
else { setValue(v.textContent + d); }
};
const appendDot = () => {
if (!typing) { setValue('0.'); typing = true; return; }
if (!v.textContent.includes('.')) setValue(v.textContent + '.');
};
const chooseOp = (symbol) => {
if (op && typing) {
// 直前の計算を即時確定(連続演算対応)
doEquals().then(() => {
num1 = parseFloat(v.textContent);
op = symbol;
setExpr(num1 + ' ' + symbol);
typing = false;
});
return;
}
num1 = parseFloat(v.textContent);
op = symbol;
setExpr(num1 + ' ' + symbol);
typing = false;
};
const clearAll = () => {
num1 = null; op = null; typing = false;
setExpr(''); setValue('0');
};
const toggleSign = () => {
if (v.textContent === '0') return;
if (v.textContent.startsWith('-')) setValue(v.textContent.slice(1));
else setValue('-' + v.textContent);
};
const percent = () => {
const n = parseFloat(v.textContent || '0') / 100;
setValue(String(n));
};
async function doEquals() {
if (op == null || num1 == null) return;
const num2 = parseFloat(v.textContent || '0');
setExpr(num1 + ' ' + op + ' ' + num2 + ' =');
// AJAXでサーブレットへ(JSONを優先して受け取る)
const form = new URLSearchParams();
form.set('num1', String(num1));
form.set('num2', String(num2));
form.set('op', mapToServer(op));
try {
const res = await fetch('calc', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },
body: form.toString()
});
let resultText;
const ct = res.headers.get('content-type') || '';
if (ct.includes('application/json')) {
const json = await res.json();
resultText = String(json.result);
} else {
// 旧HTML応答の場合は簡易パース(フォールバック)
const html = await res.text();
const m = html.match(/<b>(.*?)<\/b>/i);
resultText = m ? m[1] : 'NaN';
}
setValue(resultText);
num1 = null; op = null; typing = false;
} catch (err) {
setValue('Error');
console.error(err);
}
}
// ボタンイベント
document.addEventListener('click', (ev) => {
const b = ev.target.closest('button'); if (!b) return;
if (b.dataset.num) return appendDigit(b.dataset.num);
if (b.dataset.dot) return appendDot();
if (b.dataset.op) return chooseOp(b.dataset.op);
if (b.dataset.act === 'clear') return clearAll();
if (b.dataset.act === 'sign') return toggleSign();
if (b.dataset.act === 'percent') return percent();
if (b.dataset.act === 'equals') return doEquals();
});
// キー入力(PC操作の補助)
window.addEventListener('keydown', (ev) => {
const k = ev.key;
if (/\d/.test(k)) return appendDigit(k);
if (k === '.') return appendDot();
if (k === '+' || k === '-' || k === '*' || k === '/') {
const inv = { '+':'+','-':'-','*':'×','/':'÷' }; return chooseOp(inv[k]);
}
if (k === 'Enter' || k === '=') return doEquals();
if (k.toLowerCase() === 'c' || k === 'Escape') return clearAll();
});
</script>
</body>
</html>
(3)サーブレット (CalcServlet.java)の作成・コンパイル
/opt/tomcat/webapps/calc/WEB-INF/web.xmlを新規作成して下記を記載します。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
<servlet>
<servlet-name>CalcServlet</servlet-name>
<servlet-class>CalcServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>CalcServlet</servlet-name>
<url-pattern>/calc</url-pattern>
</servlet-mapping>
</web-app>
/opt/tomcat/webapps/calc/WEB-INF/classes/CalcServlet.javaを新規作成して下記を記載します。
import java.io.*;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
public class CalcServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
double num1 = parse(request.getParameter("num1"));
double num2 = parse(request.getParameter("num2"));
String op = request.getParameter("op");
double result = calc(num1, num2, op);
String accept = request.getHeader("Accept");
if (accept != null && accept.contains("application/json")) {
response.setContentType("application/json; charset=UTF-8");
try (PrintWriter out = response.getWriter()) {
// 非常にシンプルなJSON生成(ライブラリなし)
out.printf("{\"num1\":%s,\"num2\":%s,\"op\":\"%s\",\"result\":%s}",
stripNaN(num1), stripNaN(num2), escape(op), stripNaN(result));
}
} else {
response.setContentType("text/html; charset=UTF-8");
try (PrintWriter out = response.getWriter()) {
out.println("<html><body>");
out.println("<h2>計算結果</h2>");
out.println("<p>" + num1 + " " + op + " " + num2 + " = <b>" + result + "</b></p>");
out.println("<a href=\"index.html\">戻る</a>");
out.println("</body></html>");
}
}
}
private static double parse(String s) {
try { return Double.parseDouble(s); } catch (Exception e) { return Double.NaN; }
}
private static double calc(double a, double b, String op) {
if (op == null) return Double.NaN;
switch (op) {
case "+": return a + b;
case "-": return a - b;
case "*": return a * b;
case "/": return b != 0 ? a / b : Double.NaN;
default: return Double.NaN;
}
}
private static String escape(String s) {
if (s == null) return "";
return s.replace("\\","\\\\").replace("\"","\\\"");
}
private static String stripNaN(double d) {
if (Double.isNaN(d) || Double.isInfinite(d)) return "\"NaN\"";
return Double.toString(d);
}
}
下記コマンドでコンパイルを実施します。
cd /opt/tomcat/webapps/calc/WEB-INF/classes
javac -classpath /opt/tomcat/lib/servlet-api.jar CalcServlet.java
Tomcatを再起動します。
/opt/tomcat/bin/shutdown.sh
/opt/tomcat/bin/startup.sh
http://EC2のパブリックIP/calc/ を開くと画面が表示されます。