[Из песочницы] Как написать простой калькулятор клиент-сервер (JavaFX+EJB+WildFly)
Итак, допустим, мы хотим реализовать клиент-серверное приложение, где на стороне клиента будут формироваться нужные данные, а на стороне сервера будет производится расчет и возвращаться клиенту в виде результата. Если брать в расчет простой калькулятор (давайте сделаем его еще проще, 4 оператора, операнды без дробей и работа по схеме [операнд1] [оператор] [операнд2] [результат]) и, допустим, реализовать его на каком-нибудь ЯП (язык программирования), например, Java, с использование сервера приложения (допустим WildFly/JBoss)+клиента (можно взять на вооружение JavaFX), то можно это сделать следующим способом:
Этот же вариант можно решить с использованием RMI (Remote Method Invocation) без сервера, клиента GUI и EJB, в консоли, но этот вариант рассматривать не будем, а приступим к более интересной реализации.
1. Нам понадобятся следующие ингредиенты:
1.1. JDK,
1.2. IDE (с поддержкой Java EE),
1.3. WildFly (или другой сервер приложений для Java),
1.4. SceneBuilder (для удобства и быстрого создания GUI).
Для связи клиента с сервером будем использовать JNDI (служба именования и каталогов), используя EJB (фреймворк для построения бизнес-логики).
2. Создаем реализацию серверной части:
2.1. Удаленный интерфейс, посредством которого будет происходить связь между клиентов и сервером (используем аннотацию Remote — компонент EJB будет использовать RMI).
package com.calc.server;
import javax.ejb.Remote;
@Remote
public interface CalcRemote {
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b) throws MyException;
}
2.2. Класс, реализующий этот интерфейс (используем аннотацию @Stateless — сеансовый компонент без сохранения состояния).
package com.calc.server;
import javax.ejb.Stateless;
@Stateless(name = "CalcSessionEJB")
public class CalcSessionBean implements CalcRemote {
public CalcSessionBean() {
}
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int sub(int a, int b) {
return a - b;
}
@Override
public int mul(int a, int b) {
return a * b;
}
@Override
public int div(int a, int b) throws MyException {
try {
return a / b;
} catch (ArithmeticException ex) {
throw new MyException("Divide by Zero!!!");
}
}
}
2.3. Добавим наш класс исключения, который будет сигнализировать деление на ноль или неверный формат.
package com.calc.server;
public class MyException extends Exception {
public MyException(String message) {
super(message);
}
}
2.4. С помощью IDE создаем ear-файл (Enterprise Archive), запускаем сервер (можно со стандартным портом), деплоим на него и если ошибок не было замечено, то с серверной частью закончено.
Лог сервера может быть такой:
java:global/Calc_ear_exploded/ejb/CalcSessionEJB!com.calc.server.CalcRemote
java:app/ejb/CalcSessionEJB!com.calc.server.CalcRemote
java:module/CalcSessionEJB!com.calc.server.CalcRemote
java:jboss/exported/Calc_ear_exploded/ejb/CalcSessionEJB!com.calc.server.CalcRemote
ejb:Calc_ear_exploded/ejb/CalcSessionEJB!com.calc.server.CalcRemote
java:global/Calc_ear_exploded/ejb/CalcSessionEJB
java:app/ejb/CalcSessionEJB
java:module/CalcSessionEJB
3. Создаем реализацию клиентской части:
3.1. В SceneBuilder набрасываем следующий макет калькулятора (main.fxml), css пока накручивать не будем:
3.2. Подключаем контроллер класс к fxml-форме, для антиблокировки GUI (залипания формы) при ожидании данных с сервера на кнопку »=» добавим новую нитку:
package com.calc.client.impl2.controller;
import com.calc.server.CalcRemote;
import com.calc.server.MyException;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Properties;
public class Controller implements Runnable {
private Calculator calculator;
@FXML
private TextField displayTextField;
@FXML
private Button num1Button;
@FXML
private Button num2Button;
@FXML
private Button num3Button;
@FXML
private Button num4Button;
@FXML
private Button num5Button;
@FXML
private Button num6Button;
@FXML
private Button num7Button;
@FXML
private Button num8Button;
@FXML
private Button num9Button;
@FXML
private Button num0Button;
@FXML
private Button addButton;
@FXML
private Button subButton;
@FXML
private Button mulButton;
@FXML
private Button divButton;
@FXML
private Button clrButton;
@FXML
private Button resButton;
@FXML
private void initialize() {
System.out.println("initialize()");
calculator = new Calculator();
displayTextField.setText("0");
num1Button.setOnAction(event -> displayTextField.setText(calculator.addNumber("1")));
num2Button.setOnAction(event -> displayTextField.setText(calculator.addNumber("2")));
num3Button.setOnAction(event -> displayTextField.setText(calculator.addNumber("3")));
num4Button.setOnAction(event -> displayTextField.setText(calculator.addNumber("4")));
num5Button.setOnAction(event -> displayTextField.setText(calculator.addNumber("5")));
num6Button.setOnAction(event -> displayTextField.setText(calculator.addNumber("6")));
num7Button.setOnAction(event -> displayTextField.setText(calculator.addNumber("7")));
num8Button.setOnAction(event -> displayTextField.setText(calculator.addNumber("8")));
num9Button.setOnAction(event -> displayTextField.setText(calculator.addNumber("9")));
num0Button.setOnAction(event -> displayTextField.setText(calculator.addNumber("0")));
addButton.setOnAction(event -> {
calculator.addOperator("+");
displayTextField.setText("");
});
subButton.setOnAction(event -> {
calculator.addOperator("-");
displayTextField.setText("");
});
mulButton.setOnAction(event -> {
calculator.addOperator("*");
displayTextField.setText("");
});
divButton.setOnAction(event -> {
calculator.addOperator("/");
displayTextField.setText("");
});
resButton.setOnAction(event -> new Thread(this).start());
clrButton.setOnAction(event -> displayTextField.setText(""));
}
private void doRequest(String[] data) throws NamingException, MyException {
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, "org.wildfly.naming.client.WildFlyInitialContextFactory");
// props.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
// props.put(Context.PROVIDER_URL, "remote+http://localhost:8080");
props.put(Context.PROVIDER_URL, "http-remoting://localhost:8080");
// props.put("jboss.naming.client.ejb.context", "true");
// props.put(Context.SECURITY_PRINCIPAL, "admin");
// props.put(Context.SECURITY_CREDENTIALS, "123");
Context ctx = new InitialContext(props);
//System.out.println(ctx.getEnvironment());
CalcRemote calcRemote = (CalcRemote) ctx.lookup("Calc_ear_exploded/ejb/CalcSessionEJB!com.calc.server.CalcRemote"); //java:jboss/exported/Calc_ear_exploded/ejb/CalcSessionEJB!com.calc.server.CalcRemote
String res = Integer.toString(process(calcRemote, data));
displayTextField.setText(res);
System.out.println(res);
}
private int process(CalcRemote calcRemote, String[] resData) throws MyException {
int operand1 = Integer.parseInt(resData[0]);
int operand2 = Integer.parseInt(resData[1]);
String operator = resData[2];
switch (operator) {
case "+":
return calcRemote.add(operand1, operand2);
case "-":
return calcRemote.sub(operand1, operand2);
case "*":
return calcRemote.mul(operand1, operand2);
case "/":
return calcRemote.div(operand1, operand2);
}
return 0;
}
@Override
public void run() {
displayTextField.setText("WAITING...");
try {
doRequest(calculator.getResult());
} catch (NamingException ex) {
ex.printStackTrace();
} catch (MyException ex) {
displayTextField.setText(ex.getMessage());
}
}
}
3.3. Добавим логику работы простейшего калькулятора:
package com.calc.client.impl2.controller;
import com.calc.server.MyException;
public class Calculator {
private String buffer = "", operator, operand1, operand2;
private boolean isOperator = false;
public String addNumber(String value) {
buffer += value;
if (!isOperator) {
operand1 = buffer;
} else {
operand2 = buffer;
}
return buffer;
}
public void addOperator(String value) {
operator = value;
buffer = "";
isOperator = true;
}
public String[] getResult() throws MyException {
isOperator = false;
buffer = "";
int check;
try {
check = Integer.parseInt(operand1);
check = Integer.parseInt(operand2);
} catch (NumberFormatException ex) {
throw new MyException("Wrong format!!!");
}
return new String[]{operand1, operand2, operator};
}
}
3.5. Добавляем клиентскую библиотеку (в случае с WildFly это jboss-client.jar), запускаем GUI.
Как видно из приложенных кодов, пользователь набирает в буфер для операндов цифры, выбирает оператора, при нажатии кнопки »=» происходит соединение с сервером (через службу JNDI), надпись на дисплее «WAITING…» будет свидетельствовать ожидание ответа с сервера.
Также отметим пару исключений: NumberFormatException (отлавливается при вводе данных пользователем на клиенте) и ArithmeticException (отлавливается при делении на ноль на сервере).