write.jsp,嵌入 <img src="/vote/servlet/VoteImage"> 顯示即時圓餅圖。count.txt,重導向回 index.jsp。count.txt,計算百分比,繪製圓餅圖輸出 JPEG。<%@ page contentType="text/html;charset=gb2312" %>
<%@ page import="java.io.*" %>
<%
String realPath = request.getRealPath("question.txt");
String[] questions = readQuestions(realPath); // 自訂方法讀取 4 行題目
%>
<html><body>
<form method="post" action="write.jsp">
<% for (int i=0; i<4; i++) { %>
<input type="radio" name="choice" value="<%=i%>"> <%= (char)('A'+i) %>、<%= questions[i] %><br>
<% } %>
<input type="submit" value="我 投 一 票">
<br><img src="servlet/VoteImage" width="150" height="100"> <!-- 動態圖片 -->
</form>
</body></html>
<%@ page import="java.io.*" %>
<%
String choiceStr = request.getParameter("choice");
if (choiceStr != null) {
int choice = Integer.parseInt(choiceStr);
String countFile = request.getRealPath("count.txt");
int[] counts = readCounts(countFile); // 讀取 4 行整數
counts[choice]++; // 對應選項 +1
writeCounts(countFile, counts); // 寫回檔案
}
response.sendRedirect("index.jsp");
%>
public class VoteImage extends HttpServlet {
private final Color[] colors = {Color.RED, Color.BLACK, Color.ORANGE, Color.GREEN};
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String countFile = req.getRealPath("count.txt");
float[][] data = readData(countFile); // [[count, percent], ...]
// 計算角度
int[] angles = new int[4];
for (int i=0; i<3; i++) angles[i] = (int)(data[i][1] * 360);
angles[3] = 360 - angles[0] - angles[1] - angles[2]; // 修正誤差
resp.setContentType("image/jpeg");
resp.setHeader("Cache-Control", "no-store");
resp.setDateHeader("Expires", 0);
BufferedImage img = new BufferedImage(150, 100, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = img.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, 150, 100);
// 繪製圓餅圖 (含 3D 傾斜效果)
AffineTransform at = AffineTransform.getRotateInstance(-20 * Math.PI / 180, 45, 37);
g2d.setTransform(at);
int startAngle = 30;
for (int i=0; i<4; i++) {
Arc2D arc = new Arc2D.Double(10, 24, 80, 50, startAngle, angles[i], Arc2D.PIE);
g2d.setColor(colors[i]);
g2d.fill(arc);
startAngle += angles[i];
if (startAngle >= 360) startAngle -= 360;
}
// 恢復變換,繪製圖例
g2d.setTransform(new AffineTransform());
for (int i=0; i<4; i++) {
g2d.setColor(colors[i]);
g2d.fillRect(100, 15*i+20, 10, 10);
g2d.drawString("" + (char)('A'+i), 120, 15*i+20+8);
}
ImageIO.write(img, "JPEG", resp.getOutputStream());
g2d.dispose();
}
// 讀取 count.txt 並計算百分比
private synchronized float[][] readData(String file) {
float[][] result = new float[4][2];
int total = 0;
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
for (int i=0; i<4; i++) {
String line = raf.readLine();
int val = (line != null) ? Integer.parseInt(line.trim()) : 0;
result[i][0] = val;
total += val;
}
}
if (total == 0) total = 1; // 避免除零
for (int i=0; i<4; i++) result[i][1] = result[i][0] / total;
return result;
}
}
response.setContentType("image/jpeg") + ImageIO.write(..., resp.getOutputStream())。AffineTransform 實現 3D 傾斜視覺效果,Arc2D.PIE 繪製扇形,抗鋸齒渲染提示。synchronized 方法配合 RandomAccessFile 簡易並發控制 (生產環境請改用資料庫交易)。no-store + Expires=0 確保每次請求獲取最新圖表。webapp/
├── index.jsp
├── write.jsp
├── question.txt (4 行題目)
├── count.txt (4 行初始計數)
└── WEB-INF/
├── classes/
│ └── VoteImage.class (Servlet 編譯後)
└── web.xml (需配置 <servlet> <servlet-mapping> VoteImage /servlet/VoteImage)
AtomicInteger 或資料庫 UPDATE ... SET count=count+1 替代檔案鎖。此範例展示了早期 JSP/Servlet 時代「動態圖片 + 簡易持久化」的經典實現模式。雖然檔案儲存與手工繪圖在現代專案已罕見,但 Servlet 輸出二進位流、Graphics2D 向量繪圖、AffineTransform 幾何變換、ImageIO 編碼、快取控制 等核心技術點,至今仍是 Java 圖像處理、驗證碼生成、報表導出的基礎功力。