Java-апплеты: интерактивный веб в Бишкеке 1999 года
1999 год. Бишкек. Интернет через провайдера ElCat или AsiaInfo - dial-up, 28-33 кбит/с. Задержка до ближайшего крупного сервера: 150-250 мс до Москвы, 250-400 мс до Европы. Загрузить обычную веб-страницу с парой картинок - 20-30 секунд. Загрузить Java-апплет - ещё минута сверху.
И всё равно мы их писали.
Потому что в 1999 году никакой другой технологии, позволявшей создать интерактивную графику прямо в браузере, просто не существовало. JavaScript умел только менять тексты и переключать картинки. Flash требовал отдельного платного инструментария. Java был открытым, бесплатным, и - теоретически - «работал везде».
Архитектура: JDK 1.1, AWT и двойная буферизация
// VisitorCounterApplet.java - счётчик посещений с анимацией
// JDK 1.1, AWT, Бишкек 1999 год
// Применение: "живой" счётчик на главной странице компании
import java.applet.Applet;
import java.awt.*;
import java.net.*;
import java.io.*;
public class VisitorCounterApplet extends Applet implements Runnable {
private int count = 0;
private String label = "Посетителей сегодня:";
private Font bigFont;
private Font smallFont;
private Thread loadThread;
private boolean loading = true;
@Override
public void init() {
String paramLabel = getParameter("label");
if (paramLabel != null) label = paramLabel;
bigFont = new Font("SansSerif", Font.BOLD, 28);
smallFont = new Font("SansSerif", Font.PLAIN, 11);
setBackground(new Color(0, 33, 99));
}
@Override
public void start() {
// Загрузить счётчик с сервера в отдельном потоке
// (прямой HTTP-запрос из апплета к тому же хосту)
loadThread = new Thread(this);
loadThread.start();
}
@Override
public void run() {
try {
// Апплет мог обращаться только к своему хосту - sandbox ограничение
URL url = new URL(getCodeBase(), "/cgi-bin/counter.pl?output=plain");
URLConnection conn = url.openConnection();
conn.setConnectTimeout(5000);
BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream())
);
String line = reader.readLine();
reader.close();
if (line != null && line.matches("\\d+")) {
count = Integer.parseInt(line.trim());
}
} catch (Exception e) {
count = -1; // ошибка загрузки
} finally {
loading = false;
repaint();
}
}
@Override
public void paint(Graphics g) {
Dimension d = getSize();
g.setColor(new Color(0, 33, 99));
g.fillRect(0, 0, d.width, d.height);
if (loading) {
g.setColor(Color.gray);
g.setFont(smallFont);
g.drawString("Загрузка...", 10, d.height / 2);
return;
}
g.setColor(new Color(255, 215, 0)); // золотой
g.setFont(smallFont);
g.drawString(label, 8, 16);
if (count >= 0) {
g.setFont(bigFont);
g.setColor(Color.white);
String countStr = formatNumber(count);
FontMetrics fm = g.getFontMetrics();
int x = (d.width - fm.stringWidth(countStr)) / 2;
g.drawString(countStr, x, d.height - 8);
} else {
g.setColor(Color.red);
g.setFont(smallFont);
g.drawString("Ошибка загрузки", 8, d.height - 8);
}
}
private String formatNumber(int n) {
// Форматирование числа с пробелами (1 234 567)
String s = String.valueOf(n);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if (i > 0 && (s.length() - i) % 3 == 0) sb.append(' ');
sb.append(s.charAt(i));
}
return sb.toString();
}
// Двойная буферизация - без этого в AWT мерцало
private Image buf; private Graphics bufG;
@Override
public void update(Graphics g) {
Dimension d = getSize();
if (buf == null) { buf = createImage(d.width, d.height); bufG = buf.getGraphics(); }
paint(bufG);
g.drawImage(buf, 0, 0, this);
}
}
<!-- Вставка на главную страницу компании в Бишкеке, 1999 год -->
<applet code="VisitorCounterApplet.class"
archive="counter.jar"
width="200"
height="60"
alt="Счётчик посещений">
<param name="label" value="Посетителей сегодня:">
<!-- Фолбэк: старый добрый GIF-счётчик -->
<img src="/cgi-bin/counter.pl?output=gif" alt="Счётчик">
</applet>
Более серьёзный апплет: интерактивный прайс-лист
// PriceListApplet.java - кликабельный прайс с сортировкой
// Актуально для торговых компаний Бишкека 1999 года
import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class PriceListApplet extends Applet implements MouseListener {
// Данные: название товара, цена в сомах
private String[][] items = {
{"Pentium II 350 МГц", "18500"},
{"Pentium II 450 МГц", "24000"},
{"HDD 4.3 ГБ Seagate", "5800"},
{"HDD 8.4 ГБ WD", "8200"},
{"RAM 64 МБ SDRAM", "3400"},
{"Монитор 15\" Samsung", "14500"},
{"Модем 56k US Robotics", "6800"},
};
private int selectedRow = -1;
private int sortCol = 0;
private boolean sortAsc = true;
private static final int ROW_H = 22;
private static final int COL1_W = 220;
private static final int COL2_W = 100;
private static final int HDR_H = 24;
@Override
public void init() {
addMouseListener(this);
setBackground(Color.white);
}
@Override
public void paint(Graphics g) {
// Заголовок таблицы
g.setColor(new Color(0, 51, 153));
g.fillRect(0, 0, COL1_W + COL2_W, HDR_H);
g.setColor(Color.white);
g.setFont(new Font("SansSerif", Font.BOLD, 11));
g.drawString("Товар" + (sortCol == 0 ? (sortAsc ? " ▲" : " ▼") : ""), 8, HDR_H - 6);
g.drawString("Цена (сом)" + (sortCol == 1 ? (sortAsc ? " ▲" : " ▼") : ""),
COL1_W + 4, HDR_H - 6);
// Строки
g.setFont(new Font("SansSerif", Font.PLAIN, 11));
for (int i = 0; i < items.length; i++) {
int y = HDR_H + i * ROW_H;
if (i == selectedRow) {
g.setColor(new Color(204, 221, 255));
} else {
g.setColor(i % 2 == 0 ? Color.white : new Color(245, 245, 245));
}
g.fillRect(0, y, COL1_W + COL2_W, ROW_H);
g.setColor(Color.black);
g.drawString(items[i][0], 8, y + ROW_H - 6);
g.drawString(items[i][1], COL1_W + 4, y + ROW_H - 6);
// Горизонтальная линия
g.setColor(new Color(220, 220, 220));
g.drawLine(0, y + ROW_H, COL1_W + COL2_W, y + ROW_H);
}
// Вертикальный разделитель
g.setColor(new Color(200, 200, 200));
g.drawLine(COL1_W, 0, COL1_W, HDR_H + items.length * ROW_H);
}
@Override
public void mouseClicked(MouseEvent e) {
int y = e.getY();
if (y < HDR_H) {
// Клик по заголовку - сортировка
int newSortCol = e.getX() < COL1_W ? 0 : 1;
if (newSortCol == sortCol) {
sortAsc = !sortAsc;
} else {
sortCol = newSortCol;
sortAsc = true;
}
sortItems();
repaint();
return;
}
int row = (y - HDR_H) / ROW_H;
if (row >= 0 && row < items.length) {
selectedRow = row;
showStatus("Выбрано: " + items[row][0] + " - " + items[row][1] + " сом");
repaint();
}
}
private void sortItems() {
final int col = sortCol;
final boolean asc = sortAsc;
Arrays.sort(items, new Comparator<String[]>() {
public int compare(String[] a, String[] b) {
int result;
if (col == 1) {
result = Integer.compare(
Integer.parseInt(a[1]),
Integer.parseInt(b[1])
);
} else {
result = a[0].compareToIgnoreCase(b[0]);
}
return asc ? result : -result;
}
});
}
@Override public void mousePressed(MouseEvent e) {}
@Override public void mouseReleased(MouseEvent e) {}
@Override public void mouseEntered(MouseEvent e) {}
@Override public void mouseExited(MouseEvent e) {}
}
Проблемы с JVM в Кыргызстане 1999 года
Бишкекские разработчики столкнулись с теми же проблемами JVM, что и весь мир, но с местной спецификой:
Установка JVM у пользователей. Большинство пользователей в Бишкеке работали с IE4/IE5, у которых был встроенный Microsoft JVM. Но если пользователь скачал Netscape - нужно было отдельно устанавливать плагин Sun JVM (~10 МБ). По dial-up это было 30-40 минут загрузки. Многие просто не делали этого.
// Проверить версию JVM и показать сообщение если старая:
String version = System.getProperty("java.version");
// java.version = "1.1.8" или "1.2.2" и т.д.
if (version.startsWith("1.0") || version.startsWith("1.1")) {
// Старая JVM - некоторые API могут отсутствовать
// В Кыргызстане в 1999 встречалась JVM 1.0 на старых машинах
showStatus("Рекомендуется обновить Java до версии 1.2 или выше");
}
Кириллица в апплетах. AWT-шрифты в JVM не всегда корректно отображали кириллицу:
// Шрифт "Dialog" в JDK 1.1 на русском Windows 98 - кириллица работала
// На немецком сервере с Sun JVM - кириллица могла не отображаться
// Решение: использовать только системные шрифты без явного задания
// Проблемный вариант:
g.setFont(new Font("Arial", Font.PLAIN, 12)); // Arial мог не иметь кириллицы
// Надёжный вариант для 1999 года:
g.setFont(new Font("Dialog", Font.PLAIN, 12)); // Dialog = системный шрифт Java
2001: Flash победил, Java ушла в бэкенд
К 2001 году в Бишкеке, как и везде, стало ясно: Flash выигрывает нишу интерактивного веба. Причины для Кыргызстана были особенно весомыми: Flash-плагин уже был в комплекте с Netscape и IE, Flash-ролик весил в 5-10 раз меньше JAR-файла, а инструментарий Macromedia не требовал знания Java.
Но опыт написания апплетов в 1999-2000 годах не прошёл бесследно. Разработчики, освоившие многопоточность, событийную модель и рисование в AWT, в 2008 году легко перешли на Android-разработку. Жизненный цикл Activity (onCreate/onStart/onStop/onDestroy) - прямой потомок жизненного цикла апплета (init/start/stop/destroy). Архитектура не устарела - сменилась платформа.