2020.07.22 update

1 overview

1.1 introduction

A simple small salary management system, front-end JavaFX+ back-end Spring Boot, not much function, the main energy on the UI and some front-end logic above, the back-end is actually very simple.

Main functions:

  • User registration/login
  • Verification code retrieve password
  • Users modify information and profile pictures
  • Salaries are shown in bar chart form
  • The administrator manages users and records wages

1.2 Response Process

1.3 presentation

Login interface:

User Interface:

Administrator interface:

2 the environment

2.1 Local Development Environment

  • Manjaro 20.0.3
  • The IDEA of 2020.1.1
  • Its 11.0.7. U10-1
  • OepnJFX 11.0.3. U1-1
  • Spring Boot 2.3.0
  • MySQL 8.0.20

2.2 Server Environment

  • CentOS 8.1.1911
  • OpenJDK 11
  • Tomcat 9.0.33
  • MySQL 8.0.17

3 Front-end code

3.1 Front-end Overview

The front end is mainly divided into five parts: controller module, view module, network module, animation module and tool module.

  • Controller module: responsible for interactive events
  • View module: Responsible for updating the UI
  • Network module: sends the data request to the background
  • Animation module: shift, zoom, fade in/out, rotate animation
  • Tool module: encryption, check network connectivity, center interface, etc

3.2 an overview

3.2.1. Code directory tree

  • constantPackage: String constants and enumerated constants required by the project
  • controllerPackage: Controller class, responsible for UI and user interaction
  • entityPackage: Entity class
  • logPackage: logging class
  • networkPacket: Responsible for network requests, including request generation and request delivery
  • transitionPackage: handles animations
  • utilsPackage: utility classes
  • viewPackage: responsible for UI initialization and expected updates

3.2.2 Resource Directory Tree

  • css: The style used by the interface
  • fxml: a special XML file that defines the interface and functions in the binding Controller, that is, binding events
  • image: Static image
  • key: certificate file for HTTPS connections in OkHttp
  • properties: Some constant properties in the project

3.2.3 Project dependency

The main dependence is as follows:

  • Gson: For converting between entity classes and maps and JSON strings
  • Log4j2: log
  • Lombok: Artifact not explained, but some voices say don’t use it, refer to here or here, depending on the individual
  • OkHttp3: Network request
  • Apache Commons: utility class
  • OpenJFX11: OpenJFX core

3.3 Constant Module

Contains strings and enumeration constants required by the program:

  • CSSPath: CSS path used to style the Scene, as inscene.getStylesheets.add(path)
  • FXMLPath: FXML path used forFXMLLoaderloadingFXMLFiles, such asFXMLLoader.load(getClass.getResource(path).openStream())
  • AllURL: URL to send the request to the back end
  • BuilderKeys: the OkHttpFormBody.BuilderThe constant key name used in
  • PaneName: Pane The name of the Pane used to switch between different Panes on the same Scene
  • ReturnCode: Indicates the return code of the backend, which must be negotiated with the backend
  • ViewSize: Interface size

To focus on the path problem, the author’s CSS and FXML files are under resources:

Where the FXML path is used in the project as follows:

URL url = getClass().getResource(FXMLPath.xxxx);
FXMLLoader loader = new FXMLLoader();
loader.setLocation(url);
loader.load(url.openStream());
Copy the code

The fetch path is taken from the root path, such as messagebox.fxml in the figure above:

private static final String FXML_PREFIX = "/fxml/";
private static final String FXML_SUFFIX = ".fxml";	
public static final String MESSAGE_BOX = FXML_PREFIX + "MessageBox" + FXML_SUFFIX;
Copy the code

If the FXML file is placed directly under the resources root directory, you can use:

getClass().getResource("/xxx.fxml");
Copy the code

Direct access.

The CSS in the same way:

private static final String CSS_PREFIX = "/css/";
private static final String CSS_SUFFIX = ".css";
public static final String MESSAGE_BOX = CSS_PREFIX + "MessageBox" + CSS_SUFFIX;
Copy the code

The URL of the network request suggests writing the path to the configuration file, such as reading from the configuration file here:

Properties properties = Utils.getProperties();
if(properties ! =null)
{
    String baseUrl = properties.getProperty("baseurl") + properties.getProperty("port") + "/" + properties.getProperty("projectName");
    SIGN_IN_UP_URL = baseUrl + "signInUp";
    / /...
}
Copy the code

3.4 Controller Module

The controller module is used to process user interaction events, which can be divided into three categories:

  • Login registration interface Controller (Start package)
  • User Interface Controller (Worker package)
  • Admin Interface Controller (Admin package)

3.4.1 Login registration Page

This is the interface the program enters at the beginning, where it will bind some basic close, minimize, title bar drag events:

public void onMousePressed(MouseEvent e)
{
    stageX = stage.getX();
    stageY = stage.getY();
    screexX = e.getScreenX();
    screenY = e.getScreenY();
}
public void onMouseDragged(MouseEvent e)
{
    stage.setX(e.getScreenX() - screexX + stageX);
    stage.setY(e.getScreenY() - screenY + stageY);
}
public void close(a)
{
    GUI.close();
}
public void minimize(a)
{
    GUI.minimize();
}
Copy the code

Login interface controller is also very simple, a login/registration function plus a jump to retrieve the password interface, the code is not posted.

As for retrieving password interface, need to do more, you will first need to determine whether the user to enter the phone in the back-end database exists, also, check whether the two input password is consistent, but also to determine whether a text message sent successfully, and check the user input verification code with the back-end returned authentication code are consistent (message authentication code part actually don’t need the back-end processing, It was originally put in the front end, but was put in the back end because it might leak some important information.

3.4.2 User Interface

Next comes the interface the user enters after logging in, with fade and move animations:

public void userEnter(a)
{
    new Transition()
    .add(new Move(userImage).x(-70))
    .add(new Fade(userLabel).fromTo(0.1)).add(new Move(userLabel).x(95))
    .add(new Scale(userPolygon).ratio(1.8)).add(new Move(userPolygon).x(180))
    .add(new Scale(queryPolygon).ratio(1.8)).add(new Move(queryPolygon).x(180))
    .play();
}

public void userExited(a)
{
    new Transition()
    .add(new Move(userImage).x(0))
    .add(new Fade(userLabel).fromTo(1.0)).add(new Move(userLabel).x(0))
    .add(new Scale(userPolygon).ratio(1)).add(new Move(userPolygon).x(0))
    .add(new Scale(queryPolygon).ratio(1)).add(new Move(queryPolygon).x(0))
    .play();
}
Copy the code

The effect is as follows:

The actual processing is to put and

.add(new Move(userImage).x(-70))
Copy the code

X is the lateral displacement.

This is followed by fade and displacement text:

.add(new Fade(userLabel).fromTo(0.1)).add(new Move(userLabel).x(95))    
Copy the code

FromTo represents the change in transparency, from 0 to 1, which is equivalent to fading in.

Finally, zoom in 1.8x and move the polygon right:

.add(new Scale(userPolygon).ratio(1.8)).add(new Move(userPolygon).x(180))
Copy the code

Ratio means magnification, in this case 1.8 times.

Similarly, the upper right also needs to be enlarged and moved:

.add(new Scale(queryPolygon).ratio(1.8)).add(new Move(queryPolygon).x(180))
Copy the code

The Transition, Scale and Fade used are customized animation processing classes. For details, see “3.8 Animation Module “.

3.5 Entity class module

A simple Worker:

@Getter
@Setter
@NoArgsConstructor
public class Worker {
    private String cellphone;
    private String password;
    private String name = "No name";
    private String department = "No department";
    private String position = "No position";
    private String timeAndSalary;

    public Worker(String cellphone,String password)
    {
        this.cellphone = cellphone;
        this.password = password; }}Copy the code

The annotations use Lombok, and the introduction of Lombok is here, and the full usage is here.

TimeAndSalary is a Map converted to String using Gson, with the key for the corresponding month and the value for salary. Please refer to the tool class module for specific conversion methods.

3.6 Log Module

The log module uses Log4j2, resources log4j2.xml as follows:

<configuration status="OFF">
    <appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="Time:%d{HH:mm:ss} Level:%-5level %nMessage:%msg%n"/>
        </Console>
    </appenders>
    <loggers>
        <logger name="test" level="info" additivity="false">
            <appender-ref ref="Console"/>
        </logger>
        <root level="info">
            <appender-ref ref="Console"/>
        </root>
    </loggers>
</configuration>
Copy the code

This is the most general configuration, where pattern is the output format, where

  • %d{HH:mm:ss}: Time format
  • level: Log level
  • n: a newline
  • msg: Log message

Front-end logs are simplified. If you need more configurations, search for them.

3.7 Network Module

The core of the network module is implemented using OkHttp, which is mainly divided into two packages:

  • request: encapsulates various requests sent to the back end
  • requestBuilder: Creates the Request Builder class
  • OKHTTP: encapsulates the OkHttp utility class, external only a static send method, only one parameter, request package class, using requestBuilder generation. The send method returns an Object. How does Object correspond to the return method where OKHTTP is used

3.7.1 request packet

Encapsulates a variety of network requests:

All requests inherit from BaseRequest, whose public methods include:

  • setUrl: Sets the URL to be sent
  • setCellphone: Adds cellphone parameters
  • setPassword: Add password. Note that the password will be encrypted by sha-512
  • setWorker: Adds the Worker parameter
  • setWorkers: Accepts a List<Worker> that the administrator uses to save all workers
  • setAvatar: Add profile picture parameters
  • setAvatars: accepts a HashMap<String,String> with a key of phone, identifying a unique Worker, and a value of String that the image has been Base64 converted to

The only abstract method is:

public abstract Object handleResult(ReturnCode code):Copy the code

According to the result returned by different request processing, the back end returns a ReturnCode, which encapsulates the status code, error message and return value, and converts it from Gson to String. After the front end gets String, Gson converts it to ReturnCode, and obtains the status code and return value from it.

The rest of the request classes inherit from BaseRequest and implement different processing result methods, such as Get requests:

public class GetOneRequest extends BaseRequest {
    @Override
    public Object handleResult(ReturnCode code)
    {
        switch (code)
        {
            case EMPTY_CELLPHONE:
                MessageBox.emptyCellphone();
                return false;
            case INVALID_CELLPHONE:
                MessageBox.invalidCellphone();
                return false;
            case CELLPHONE_NOT_MATCH:
                MessageBox.show("Fetch failed, phone number does not match");
                return false;
            case EMPTY_WORKER:
                MessageBox.emptyWorker();
                return false;
            case GET_ONE_SUCCESS:
                return Conversion.JSONToWorker(code.body());
            default:
                MessageBox.unknownError(code.name());
                return false; }}}Copy the code

To obtain a Worker, the possible return values are as follows (return enumeration values defined in ReturnCode, which need to be unified at the front and back ends) :

  • EMPTY_CELLPHOE: Indicates that the phone number in the sent GET request is empty
  • INVALID_CELLPHONE: Illegal phone number, the judging code is:String reg = "^[1][358][0-9]{9}$"; return ! (Pattern.compile(reg).matcher(cellphone).matches());
  • CELLPHONE_NOT_MATCH: The phone number does not match, that is, the database does not have the corresponding Worker
  • EMPTY_WORKER: The Worker exists in the database, but an empty Worker is returned due to backend processing failure when converting to String
  • GET_ONE_SUCCESSConvert String to Worker using the utility class
  • Others: Unknown error

3.7.2 requestBuilder package

Contains the Builder corresponding to request:

In addition to the default constructor and build methods, there are only set methods, such as:

public class GetOneRequestBuilder {
    private final GetOneRequest request = new GetOneRequest();
    public GetOneRequestBuilder(a)
    {
        request.setUrl(AllURL.GET_ONE_URL);
    }
    public GetOneRequestBuilder cellphone(String cellphone)
    {
        if(Check.isEmpty(cellphone))
        {
            MessageBox.emptyCellphone();
            return null;
        }
        request.setCellphone(cellphone);
        return this;
    }
    public GetOneRequest build(a)
    {
        returnrequest; }}Copy the code

With the URL set in the default constructor, all that remains is to set the phone to get the Worker.

3.7.3 OKHTTP

This is a static utility class that encapsulates OkHttp. The only public static method is as follows:

public static Object send(BaseRequest content)
{
    Call call = client.newCall(new Request.Builder().url(content.getUrl()).post(content.getBody()).build());
    try
    {
        ResponseBody body = call.execute().body();
        if(body ! =null)
            return content.handleResult(Conversion.stringToReturnCode(body.string()));
    }
    catch (IOException e)
    {
        L.error("Reseponse body is null");
        MessageBox.show("Server disconnected, response null.");
    }
    return null;
}
Copy the code

In the case of synchronous POST requests, BaseRequest is used as the base class because it is easy to get the URL and request body in the Call, and asynchronous requests can be considered if there is a large amount of data. In addition, as mentioned above, the back end returns a ReturnCode converted to String by Gson, so after obtaining the body, it is first converted to ReturnCode before processing.

3.7.4 HTTPS

As for HTTPS, since it is deployed on Tomcat, the certificate needs to be set up in Tomcat, and the following three parts need to be set up in OkHttp:

  • sslSocketFactory: SSL socket factory
  • HostnameVerifier: Verifies the host name
  • X509TrustManager: Certificate trust manager class

3.7.4.1 OkHttp configuration

There are three parts to set up, but let’s look at the simplest part, using the HostnameVerifier interface:

OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(1500, TimeUnit.MILLISECONDS)
.hostnameVerifier((hostname, sslSession) -> {
    if ("www.test.com".equals(hostname)) {
        return true;
    } else {
        HostnameVerifier verifier = HttpsURLConnection.getDefaultHostnameVerifier();
        return verifier.verify(hostname, sslSession);
    }
}).build();
Copy the code

Return true if the host name is www.test.com (which can also be used with a public IP address), otherwise use the default HostnameVerifier. If the service logic is complex, dynamic verification can be performed based on the configuration center and black/white list.

Next, X509TrustManager handles this:

private static X509TrustManager trustManagerForCertificates(InputStream in)
            throws GeneralSecurityException
{
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
    if (certificates.isEmpty()) {
        throw new IllegalArgumentException("expected non-empty set of trusted certificates");
    }
    char[] password = "www.test.com".toCharArray(); // Any password will work.
    KeyStore keyStore = newEmptyKeyStore(password);
    int index = 0;
    for (Certificate certificate : certificates) {
        String certificateAlias = Integer.toString(index++);
        keyStore.setCertificateEntry(certificateAlias, certificate);
    }
    // Use it to build an X509 trust manager.
    KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
            KeyManagerFactory.getDefaultAlgorithm());
    keyManagerFactory.init(keyStore, password);
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
            TrustManagerFactory.getDefaultAlgorithm());
    trustManagerFactory.init(keyStore);
    TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
    if(trustManagers.length ! =1| |! (trustManagers[0] instanceof X509TrustManager)){
        throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
    }
    return (X509TrustManager) trustManagers[0];
}

private static KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
    try {
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); // Add custom password here, default
        InputStream in = null; // By convention, 'null' creates an empty key store.
        keyStore.load(in, password);
        return keyStore;
    } catch (IOException e) {
        throw newAssertionError(e); }}Copy the code

Return a trust manager that trusts the certificate read from the input stream. SSLHandsakeException is thrown if the certificate is not signed. It is recommended to use third-party signed certificates rather than self-signed ones (such as those generated using OpenSSL or Acme.sh), especially in production environments. The example notes also say:

Finally, SSL socket factory processing:

private static SSLSocketFactory createSSLSocketFactory(a) {
    SSLSocketFactory ssfFactory = null;
    try {
        SSLContext sc = SSLContext.getInstance("TLS");
        sc.init(null.new TrustManager[]{trustManager}, new SecureRandom());
        ssfFactory = sc.getSocketFactory();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return ssfFactory;
}
Copy the code

The complete OkHttpClient construct is as follows:

X509TrustManager trustManager = trustManagerForCertificates(OKHTTP.class.getResourceAsStream("/key/pem.pem"));
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(1500, TimeUnit.MILLISECONDS)
.sslSocketFactory(createSSLSocketFactory(), trustManager)
.hostnameVerifier((hostname, sslSession) -> {
    if ("www.test.com".equals(hostname)) {
        return true;
    } else {
        HostnameVerifier verifier = HttpsURLConnection.getDefaultHostnameVerifier();
        return verifier.verify(hostname, sslSession);
    }
})
.readTimeout(10, TimeUnit.SECONDS).build();
Copy the code

Pem indicates the certificate file under resources.

3.7.4.2 Setting a Certificate for the Server

Use WAR for deployment. Search for JAR deployment mode, Tomcat server, and other Web servers by yourself.

Conf /server.xml in the Tomcat configuration file:

Go to

and copy it and change the name to the corresponding domain name:

Then download the file from the certificate vendor (usually with the document, according to the document deployment), Tomcat is two files, one is PFX, one is password file, continue to modify server.xml, search 8443, find the following location:


is HTTP/1.1 based on NIO implementation, and

is HTTP/2 based on APR implementation.

It is easier to use HTTP/1.1 and simply modify server. XML. It is more difficult to use HTTP/2 and install APR, apr-util and tomcat-native. Take HTTP/1.1 as an example and modify as follows:

<Connector port="8123" protocol="org.apache.coyote.http11.Http11NioProtocol"
	maxThreads="200" SSLEnabled="true" 
	scheme="https" secure="true"
	keystoreFile="/xxx/xxx/xxx/xxx.pfx" keystoreType="PKCS12"
	keystorePass="YOUR PASSWORD" clientAuth="false"
	sslProtocol="TLS">
</Connector>
Copy the code

Change the certificate location and password. To be more secure, you can specify a TLS version, such as TLS1.2:

<Connector .
sslProtocol="TLS" sslEnabledProtocols="TLSv1.2"
>
Copy the code

3.7.5 Image processing

Images were originally intended to be processed using OkHttp’s MultipartBody, but the images were not very good and it seemed unnecessary. Besides, the data of entity classes were all transmitted in the form of strings. Therefore, the author thought that we could transfer all the images in strings uniformly. The Base64 function, which requires external dependencies, has been changed to JDK Base64:

public static String avatarToString(Path path)
{
    try
    {
        return new String(encoder.encode(Files.readAllBytes(path)));
    }
    catch (IOException e)
    {
        MessageBox.avatarToStringFailed();
        L.error(e);
        return null; }}public static void stringToAvatar(String base64Code, String cellphone){
	try
	{
	    if(! Files.exists(TEMP_PATH)) Files.createDirectory(TEMP_PATH);if(! Files.exists(getPath(cellphone))) Files.createFile(getPath(cellphone)); Files.write(getPath(cellphone), decoder.decode(base64Code)); }catch(IOException e) { MessageBox.stringToAvatarFailed(); L.error(e); }}Copy the code

Base64 is a method of representing binary data based on 64 printable characters. It can convert binary data (pictures/videos, etc.) into characters or decode corresponding characters into original binary data.

The author measured that the conversion speed of this method is not slow. As long as there is a correct conversion function, the server side can easily convert, but the support for large files is not good:

This is good enough for general images, but for real files it is recommended to use MultipartBody.

3.8 Animation Module

Contains four types of animation:

  • Fade in/out
  • The displacement
  • The zoom
  • rotating

These four classes implement the CustomTransitionOperation interface:

import javafx.animation.Animation;

public interface CustomTransitionOperation {
    double defaultSeconds = 0.4;
    Animation build(a);
    void play(a);
}
Copy the code

Among them:

  • defaultSecondsRepresents the number of seconds the animation lasts by default
  • buildUsed forTransitionIn the various animation classes for unitybuildoperation
  • playFor playing animations

The four animation classes are similar, taking the rotating animation class as an example:

public class Rotate implements CustomTransitionOperation{
    private final RotateTransition transition = new RotateTransition(Duration.seconds(1));

    public Rotate(Node node)
    {
        transition.setNode(node);
    }

    public Rotate seconds(double seconds)
    {
        transition.setDuration(Duration.seconds(seconds));
        return this;
    }

    public Rotate to(double to)
    {
        transition.setToAngle(to);
        return this;
    }

    @Override
    public Animation build(a) {
        return transition;
    }

    @Override
    public void play(a) { transition.play(); }}Copy the code

Seconds Sets the number of seconds and to sets the rotation Angle. All animation classes are controlled by Transition:

public class Transition {
    private final ArrayList<Animation> animations = new ArrayList<>();

    public Transition add(CustomTransitionOperation animation)
    {
        animations.add(animation.build());
        return this;
    }

    public void play(a)
    { animations.forEach(Animation::play); }}Copy the code

Inside is a collection of animation classes. Each time you add the corresponding animation and then add it to the collection, and finally play it in a unified manner. Example usage is as follows:

new Transition()
.add(new Move(userImage).x(-70))
.add(new Fade(userLabel).fromTo(0.1)).add(new Move(userLabel).x(95))
.add(new Scale(userPolygon).ratio(1.8)).add(new Move(userPolygon).x(180))
.add(new Scale(workloadPolygon).ratio(1.8)).add(new Move(workloadPolygon).x(180))
.play();
Copy the code

3.9 Tool Module

  • AvatarUtils: used for local generation of temporary images and image conversion processing
  • Check: Checks whether it is empty and valid
  • Conversion: Conversion class, via Gson inWorker/String.Map/String.List/StringTo convert between
  • Utils: Encrypt, set the operating environment, centerStage, check network connectivity, etc

Utils and Conversion.

3.9.1 Conversion

Class to convert String to List/Worker/Map using Gson, e.g. String to Map:

public static Map<String,Double> stringToMap(String str)
{
    if(Check.isEmpty(str))
        return null; Map<? ,? > m = gson.fromJson(str,Map.class); Map<String,Double> map =new HashMap<>(m.size());
    m.forEach((k,v)->map.put((String)k,(Double)v));
    return map;
}
Copy the code

Most Conversion functions are similar, nullating first, and then performing the corresponding type Conversion. The Conversion here is basically the same as the back end, which also needs to use the Conversion class for Conversion operation.

3.9.2 Utils

Get the properties file as follows:

// Get the properties file
public static Properties getProperties(a)
{
    Properties properties = new Properties();
    // The project properties file is split into config_dev.properties,config_test.properties,config_prod.properties
    String fileName = "properties/config_"+ getEnv() +".properties";
    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    try(InputStream inputStream = loader.getResourceAsStream(fileName))
    {
        if(inputStream ! =null)
        {
        	// Prevent garbled characters
            properties.load(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
            return properties;
        }
        L.error("Can not load properties properly.InputStream is null.");
        return null;
    }
    catch (IOException e)
    {
        L.error("Can not load properties properly.Message:"+e.getMessage());
        return null; }}Copy the code

Another is a way to check network connectivity:

public static boolean networkAvaliable(a)
{
    try(Socket socket = new Socket())
    {
        socket.connect(new InetSocketAddress("www.baidu.com".443));
        return true;
    }
    catch (IOException e)
    {
        L.error("Can not connect network.");
        e.printStackTrace();
    }
    return false;
}

public static boolean backendAvaliable(a)
{
    try(Socket socket = new Socket())
    {
        if(isProdEnvironment())
            socket.connect(new InetSocketAddress("www.test.com".8888));
        else
            socket.connect(new InetSocketAddress("127.0.0.1".8080));
        return true;
    }
    catch (IOException e)
    {
        L.error("Can not connect back end server.");
        L.error(ExceptionUtils.getStackTrace(e));
    }
    return false;
}
Copy the code

The socket is used to determine whether the network is connected and whether the back end is connected.

Finally, there is the method of centering Stage. Although Stage is equipped with a centerOnScreen, the result is not good. The author’s actual measurement is horizontally centered but vertically upward, not vertically horizontally centered.

So manually set x and Y for the Stage based on the screen width and size of the Stage.

public static void centerMainStage(a)
{
    Rectangle2D screenRectangle = Screen.getPrimary().getBounds();
    double width = screenRectangle.getWidth();
	double height = screenRectangle.getHeight();
	
	Stage stage = GUI.getStage();
    stage.setX(width/2 - ViewSize.MAIN_WIDTH/2);
    stage.setY(height/2 - ViewSize.MAIN_HEIGHT/2);
}
Copy the code

3.10 View Module

  • GUI: global variable sharing and controlSceneThe switch of
  • MainScene: global controller that initializes and binds keyboard events
  • MessageBox: Information box, provided externallyshow()Static method, etc

The main method in the GUI is switchToXxx, for example:

public static void switchToSignInUp(a)
{
    if(GUI.isUserInformation())
    {
        AvatarUtils.deletePathIfExists();
        GUI.getUserInformationController().reset();
    }
    mainParent.requestFocus();
    children.clear();
    children.add(signInUpParent.lookup(PaneName.SIGN_IN_UP));
    scene.getStylesheets().add(CSSPath.SIGN_IN_UP);
    Label minimize = (Label) (mainParent.lookup("#minimize"));
    minimize.setText("-");
    minimize.setFont(new Font("System".20));
    minimize.setOnMouseClicked(v->minimize());
}
Copy the code

Jump to the login registration interface, which is a public static method, first determine whether it is a user information interface, then make the Parent get focus (for keyboard event response), then add the corresponding AnchorPane to Children, add CSS, and finally modify the button text and event.

We also added some keyboard event responses to the MainScene, such as Enter:

ObservableMap<KeyCombination,Runnable> keyEvent = GUI.getScene().getAcclerators();
keyEvent.put(new KeyCodeCombination(KeyCode.ENTER),()->
{
    if (GUI.isSignInUp())
        GUI.getSignInUpController().signInUp();
    else if (GUI.isRetrievePassword())
        GUI.getRetrievePasswordController().reset();
    else if(GUI.isWorker())
        GUI.switchToUserInformation();
    else if(GUI.isAdmin())
        GUI.switchToUserManagement();
    else if(GUI.isUserInformation())
    {
        UserInformationController controller = GUI.getUserInformationController();
        if(controller.isModifying())
            controller.saveInformation();
        else
            controller.modifyInformation();
    }
    else if(GUI.isSalaryEntry()) { GUI.getSalaryEntryController().save(); }});Copy the code

4 Front-end UI

4.1 FXML

The interface is basically controlled by these FXML files. There is not much content in this part. It is basically designed by the Scene Builder of IDEA, and a little part is controlled by code.

  • The root node is AnchorPane, one for each FXMLfx:idIn order to switch
  • The event is bound to the corresponding control, such as a mouse entry event bound to a Label, set on the LabelonMouseEntered="#xxx", where the method is the corresponding controller (fx:controller="xxx.xxx.xxx.xxxController").
  • <Image>The URL attribute in the@, such as<Image url="@.. /.. /image/xxx.png">

4.2 CSS

JFX integrates some CSS beautification features, such as:

-fx-background-radius: 25px;
-fx-background-color:#e2ff1f;
Copy the code

The usage requires that the ID be set in FXML first.

Notice the difference between the two ids:

  • fx:id
  • id

Fx :id refers to the control’s FX: ID, usually used in conjunction with @fXML in controllers, such as a Label with fx: ID set to label1

<Label fx:id="label1" layoutX="450.0" layoutY="402.0" text="Label">
   <font>
       <Font size="18.0" />
   </font>
</Label>
Copy the code

@fxml can be used in the corresponding Controller with the same name as fx:id:

@FXML
private Label label1;
Copy the code

Id refers to the ID of the CSS, and can be used in the CSS reference, for example, the above Label also set id (can be the same, can also be different) :

<Label fx:id="label1" id="label1" layoutX="450.0" layoutY="402.0" text="Label">
   <font>
       <Font size="18.0" />
   </font>
</Label>
Copy the code

Then refer to the CSS file as if it were a normal ID:

#label1
{
    -fx-background-radius: 20px; / * rounded corners * /
}
Copy the code

JFX also supports CSS pseudo-classes, such as the following minimized and closed mouse-over effects that are implemented using pseudo-classes:

#minimize:hover
{
    -fx-opacity: 1;
    -fx-background-radius: 10px;
    -fx-background-color: # 323232;
    -fx-text-fill: #ffffff;
}

#close:hover
{
    -fx-opacity: 1;
    -fx-background-radius: 10px;
    -fx-background-color: #dd2c00;
    -fx-text-fill: #ffffff;
}
Copy the code

Of course, some of the more complicated ones are not supported. I’ve tried things like Transition, but they’re not supported.

Finally, we need to introduce CSS into the Scene:

Scene scene = new Scene();
scene.getStylesheets().add("xxx/xxx/xxx/xxx.css");
Copy the code

The usage in the program is:

scene.getStylesheets().add(CSSPath.SIGN_IN_UP);
Copy the code

4.3 Stage Construction process

The following uses the prompt box as an example to illustrate the construction process of Stage.

try {
    Stage stage = new Stage();
    Parent root = FXMLLoader.load(getClass().getResource(FXMLPath.MESSAGE_BOX));
    Scene scene = new Scene(root, ViewSize.MESSAGE_BOX_WIDTH,ViewSize.MESSAGE_BOX_HEIGHT);
    scene.getStylesheets().add(CSSPath.MESSAGE_BOX);
    Button button = (Button)root.lookup("#button");
    button.setOnMouseClicked(v->stage.hide());
    Label label = (Label)root.lookup("#label");
    label.setText(message);
    stage.initStyle(StageStyle.TRANSPARENT);
    stage.setScene(scene);
    Utils.centerMessgeBoxStage(stage);
    stage.show();

    root.requestFocus();
    scene.getAccelerators().put(new KeyCodeCombination(KeyCode.ENTER), stage::close);
    scene.getAccelerators().put(new KeyCodeCombination(KeyCode.BACK_SPACE), stage::close);
} catch (IOException e) {
	/ /...
}
Copy the code

First, create a Stage, and then use FXMLLoader to load the FXML file on the corresponding path. After obtaining Parent, use the Parent to generate Scene, and then add style for Scene.

FindViewById (fx:id); findViewById (fx:id); findViewById (fx:id) Once the control is processed, center and display the Stage, bind the keyboard event and let the Parent get the focus.

5 Back-end

5.1 Back-end Overview

The back end takes Spring Boot framework as the core and is deployed in WAR mode. The whole end is divided into three layers:

  • Controller layer: Responsible for receiving requests from the front end and calling business layer methods
  • Business layer: handles the main business, such as CRUD, image processing, etc
  • Persistence layer: Data persistence, Hibernate+Spring Data JPA

Generally speaking, there are no lofty things used and the logic is relatively simple.

5.2 an overview

5.2.1 Code directory tree

5.2.2 rely on

The main dependence is as follows:

  • Spring Boot Starter Data JPA: Data persistence
  • Guava: used toIterable<Worker>Convert to set
  • Lombok: Same front-end
  • Gson: JSON conversion class
  • Apache Commons: For exception handling + random string generation
  • TencentCloud SDK Java: SMS verification code API
  • Jasypt Spring Boot Starter: encrypts configuration files

5.3 Controller Layer

The controller can be divided into three types: one processing picture, one processing CRUD request, and one processing SMS request. The controller accepts POST request and ignores GET request. The general processing flow is that after receiving the parameter, it first performs judgment operations, such as nulling and judging whether it is valid, etc., then calls the methods of the business layer and encapsulates the returned result, while logging, and finally converts the returned result into a string using Gson. Most of the code is relatively simple not to paste, say about the SMS verification code part.

Verification code module uses Tencent cloud interface, here on the official website, search SMS function.

New users are given 100 SMS messages by default:

You need to create a signature and body template before sending the template. You can use the template after passing the verification.

You can try the SMS function according to the quick Start. If you can receive the SMS successfully, you can click here to see the API (Java version).

The following example is simplified from the document example:

@PostMapping("sendSms")
public @ResponseBody
String sendSms(@RequestParam String cellphone)
{
    String randomCode = RandomStringUtils.randomNumeric(6);
    if(Check.isEmpty(cellphone))
    {
        L.sendSmsFailed("null",randomCode,"cellphone is empty");
        return toStr(ReturnCode.EMPTY_CELLPHONE);
    }
    if(Check.isInvalidCellphone(cellphone))
    {
        L.sendSmsFailed(cellphone,randomCode,"cellphone is not valid.");
        return toStr(ReturnCode.INVALID_CELLPHONE);
    }
    ReturnCode s = ReturnCode.SEND_SMS_SUCCESS;
    try
    {
        SmsClient client = new SmsClient(new Credential(secretId,secretKey),"");
        SendSmsRequest request = new SendSmsRequest();
        request.setSmsSdkAppid(appId);
        request.setSign(sign);
        request.setTemplateID(templateId);

        String [] templateParamSet = {randomCode};
        request.setTemplateParamSet(templateParamSet);

        String [] phoneNumbers = {"+ 86"+cellphone};
        request.setPhoneNumberSet(phoneNumbers);
        SendSmsResponse response = client.SendSms(request);

        if(response ! =null && response.getSendStatusSet()[0].getCode().equals("Ok")) { L.sendSmsSuccess(cellphone,randomCode); s.body(randomCode); }}catch (Exception e) {
        L.sendSmsFailed(cellphone,randomCode,e);
        s = ReturnCode.UNKNOWN_ERROR;
    }
    return toStr(s);
}
Copy the code

AppId,sign, and templateID are the corresponding appId, signature id, and body templateID respectively. After the application is approved, the application will be allocated, and a six-digit verification code will be generated randomly.

Request. SetPhoneNumberSet () parameters for the need to send cell phone number String array, pay attention to the need to add the area code. If the message is sent successfully, the mobile phone will receive it. If the message fails, modify it according to the abnormal information.

The only thing to notice is when data like appID is injected via @value, as in:

@Controller
@RequestMapping("/")
public class SmsController {
    @Value("${tencent.secret.id}")
    privateString secretId; . }Copy the code

However, since the sign part contains Chinese, encoding conversion is required:

@Value("${tencent.sign}")
private String sign;

@PostConstruct
public void init(a)
{
    sign = new String(sign.getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);
}
Copy the code

5.4 Service Layer and Persistence Layer

For example, the saveOne method of the business layer saves a Worker, and then converts it into a Worker directly using the save method provided by CrudRespository

:
,id>

public ReturnCode saveOne(String json) {
    ReturnCode s = ReturnCode.SAVE_ONE_SUCCESS;
    Worker worker = Conversion.JSONToWorker(json);
    if (Check.isEmpty(worker)) {
        L.emptyWorker();
        s = ReturnCode.EMPTY_WORKER;
    }
    else
        workerRepository.save(worker);
    return s;
}
Copy the code

The saveAll method of CrudRepository

is Iterable
, so you can save the List directly.
,id>

public ReturnCode saveAll(List<Worker> workers)
{
    workerRepository.saveAll(workers);
    return ReturnCode.SAVE_ALL_SUCCESS;
}
Copy the code

The String sent from the front end needs to be converted to List in the control layer.

5.5 log

The log uses Spring Boot’s own logging system, with a simple configuration of the log path. In addition, the log format is customized (for clean output, I felt that the configuration file was not implemented well enough, so I customized a utility class).

For example, log interception is as follows:

Custom headers and fixed output for each line, followed by a prompt for method, level, time, and other information.

In total, there are 7 classes except the formatter, of which L is the main class, the outer class only needs to call L’s methods, most of which are public static methods, and the remaining 6 are classes called by L:

If the backup is successful, call:

public Success
{
	public static void backup(a)
	{
	    l.info(new FormatterBuilder().title(getTitle()).info().position().time().build());
	}
	/ /...
}
Copy the code

FormatterBuilder FormatterBuilder FormatterBuilder FormatterBuilder FormatterBuilder FormatterBuilder FormatterBuilder FormatterBuilder FormatterBuilder FormatterBuilder FormatterBuilder FormatterBuilder FormatterBuilder FormatterBuilder FormatterBuilder FormatterBuilder

public FormatterBuilder info(a)
{
    return level("info");
}
public FormatterBuilder time(a)
{
    content("time",getCurrentTime());
    return this;
}
private FormatterBuilder level(String level)
{
    content("level",level);
    return this;
}
public FormatterBuilder cellphone(String cellphone)
{
    content("cellphone",cellphone);
    return this;
}
public FormatterBuilder message(String message)
{
    content("message",message);
    return this;
}
Copy the code

5.6 tools

Four:

  • Backup: Backup the periodic database
  • Check: Checks the validity and whether it is empty
  • Conversion: Conversion class, almost identical to the front-end, using Gson inStringwithList/Map/WorkerTo convert between
  • ReturnCode: ReturnCode enumeration class

To focus on backup, the code is not long and the entire class is posted directly:

@Component
@EnableScheduling
public class Backup {
    private static final long INTERVAL = 1000 * 3600 * 12;
    @Value("${backup.command}")
    private String command;
    @Value("${backup.path}")
    private String strPath;
    @Value("${spring.datasource.username}")
    private String username;
    @Value("${spring.datasource.password}")
    private String password;
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${backup.dataTimeFormat}")
    private String dateTimeFormat;

    @Scheduled(fixedRate = INTERVAL)
    public void startBackup(a)
    {
        try
        {
            String[] commands = command.split(",");
            String dbname = url.substring(url.lastIndexOf("/") +1);
            commands[2] = commands[2] + username + " --password=" + password + "" + dbname + ">" + strPath +
                    dbname + "_" + DateTimeFormatter.ofPattern(dateTimeFormat).format(LocalDateTime.now())+".sql";
            Path path = Paths.get(strPath);
            if(! Files.exists(path)) Files.createDirectories(path); Process process = Runtime.getRuntime().exec(commands); process.waitFor();if(process.exitValue() ! =0)
            {
                InputStream inputStream = process.getErrorStream();
                StringBuilder str = new StringBuilder();
                byte []b = new byte[2048];
                while(inputStream.read(b,0.2048) != -1)
                    str.append(new String(b));
                L.backupFailed(str.toString());
            }
            L.backupSuccess();
        }
        catch(IOException | InterruptedException e) { L.backupFailed(e.getMessage()); }}}Copy the code

First use @Value to get the Value in the configuration file, and then add @scheduled to the backup method. @scheduled is a Spring Boot annotation used to provide Scheduled tasks that are Scheduled to be executed at a specified time or at intervals (in this case, half a day). There are three main ways to configure the execution time:

  • cron
  • fixedRate
  • fixedDelay

I’m not going to expand it here, but I can use it here.

Add @enablesCheduling to the class before using it. During backup, first use URL to obtain database name, and then assemble backup command. Note that if local use Win to develop backup command will be different from Linux:

// Win (untested, developed by author on Linux)
command[0]=cmd
command[1]=/c
command[2]=mysqldump -u username --password=your_password dbname > backupPath+File.separator+dbname+datetimeFormmater+".sql"

// Linux (Manjaro+ server CentOS test passed)
command[0]=/bin/sh
command[1]=-c
command[2]=/usr/bin/mysqldump -u username --password=your_password dbname > backupPath+File.separator+dbname+datetimeFormmater+".sql"
Copy the code

If a backup path exists, Java’s own Process is used to do the backup. If errors occur, getErrorStream() is used to get error messages and log.

5.7 Configuration Files

5.7.1 Classification of configuration files

One total profile + three are profiles for a specific environment (development, test, production). You can switch profiles with spring.profiles. Active =dev, for example, spring.profiles. Additional custom configurations require additional fields in the addition-spring-configuration-metadata. json (optional, but prompted by the IDE), such as:

"properties": [
    {
        "name": "backup.path",
        "type": "java.lang.String",
        "defaultValue": null
    },
]
Copy the code

5.7.2 encryption

In 2020, it’s not a good idea to use plaintext passwords in configuration files, is it?

It’s time to encrypt.

The Jasypt Spring Boot component is used.

Usage is not described in detail here, see my other blog for details, poke here.

However, the author measured that the latest version 3.0.2 (this article was written in 2020.06.05, 2020.05.31, the author has updated version 3.0.3, but the author has not tested it) would have the following problems:

Description:

Failed to bind properties under 'spring.datasource.password' to java.lang.String:

    Reason: Failed to bind properties under 'spring.datasource.password' to java.lang.String

Action:

Update your application's configuration
Copy the code

The solution and detailed description of the problem are here.

6 Deployment and Packaging

6.1 Front-end Packaging

First, let’s talk about the front-end packaging process, simply say that the JAR can run cross-platform, but if it is a specific platform, such as Win, want to create an EXE without additional JDK environment still need some additional operations, here is a brief introduction to the packaging process.

JFX: Native can be packaged with MVN JFX :native for JDK8. This can be easily packaged as DMG or EXE. If you know how to use javafX-Maven-plugin or IDEA with an artifact type exe or DMG

6.1.1 IDEA Package at a time

Maven plug-in is used for packaging. The commonly used Maven package plug-in is as follows:

  • Mave-jar-plugin: the default package jar plugin. The generated JR is very small, but you need to place the lib in the same directory as the JAR
  • Maven-shade-plugin: Provides two basic functions: package dependent JAR packages into the current JAR package, rename dependent JAR packages and filter trade-off
  • Maven-assembly-plugin: Supports custom packaging, more of a reassembly of the project directory

This project is packaged using maven-shade-plugin.

The latest version of Maven can be found on Github:

<build>
	<plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-shade-plugin</artifactId>
            <version>3.2.4</version>
	            <executions>
	            	<execution>
	                    <phase>package</phase>
	                    <goals>
	                        <goal>shade</goal>
	                    </goals>
	                    <configuration>
	                        <transformers>
	                            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
	                                <mainClass>xxxx.xxx.xxx.Main</mainClass>
	                            </transformer>
	                        </transformers>
	                    </configuration>
	                </execution>
	            </executions>
          </plugin>
	</plugins>
</build>
Copy the code

Just modify the main class:

<mainClass>xxxx.xxx.xxx.Main</mainClass>
Copy the code

You can then pack with one click from Maven in the right pane of your IDEA:

This gives you JAR packages under Target that can run across platforms as long as you provide a JDK environment.

java -jar xxx.jar
Copy the code

The following two steps are to use Exe4J and Enigma Virtual Box into a single EXE method, only for Win, using Linux/Mac can skip or search for other methods.

6.1.2 Exe4J Secondary Packaging

6.1.2.1 exe4j

Exe4j can integrate Java applications into Java executable file generation tools under Win, whether for servers or for GUI or command line applications. In a nutshell, this project uses it to convert a JAR to an EXE. Exe4j requires a JRE, which needs to be generated from JDK9, so it needs to be packaged using Exe4j as a JRE.

6.1.2.2 generated jre

The functions of each module can be viewed here:

After testing the required modules of this program are as follows:

java.base,java.logging,java.net.http,javafx.base,javafx.controls,javafx.fxml,javafx.graphics,java.sql,java.management
Copy the code

Switch to the JDK directory and use jlink to generate the JRE:

jlink --module-path jmods --add-modules 
java.base,java.logging,java.net.http,javafx.base,javafx.controls,javafx.fxml,javafx.graphics,java.sql,java.management
--output jre
Copy the code

OpenJDK11 does not come with JavaFX, so download JFX jmods for Win platform and move it to JDK jMOds directory. The generated JRE is 91M in size:

It is not recommended to use all modules if it is really not clear which modules to use:

jlink --module-path jmods --add-modules java.base,java.compiler,java.datatransfer,java.xml,java.prefs,java.desktop,java.instrument,java.logging,java.management, java.security.sasl,java.naming,java.rmi,java.management.rmi,java.net.http,java.scripting,java.security.jgss,java.transac tion.xa,java.sql,java.sql.rowset,java.xml.crypto,java.se,java.smartcardio,jdk.accessibility,jdk.internal.vm.ci,jdk.manag ement,jdk.unsupported,jdk.internal.vm.compiler,jdk.aot,jdk.internal.jvmstat,jdk.attach,jdk.charsets,jdk.compiler,jdk.cry pto.ec,jdk.crypto.cryptoki,jdk.crypto.mscapi,jdk.dynalink,jdk.internal.ed,jdk.editpad,jdk.hotspot.agent,jdk.httpserver,j dk.internal.le,jdk.internal.opt,jdk.internal.vm.compiler.management,jdk.jartool,jdk.javadoc,jdk.jcmd,jdk.management.agen t,jdk.jconsole,jdk.jdeps,jdk.jdwp.agent,jdk.jdi,jdk.jfr,jdk.jlink,jdk.jshell,jdk.jsobject,jdk.jstatd,jdk.localedata,jdk. management.jfr,jdk.naming.dns,jdk.naming.rmi,jdk.net,jdk.pack,jdk.rmic,jdk.scripting.nashorn,jdk.scripting.nashorn.shell ,jdk.sctp,jdk.security.auth,jdk.security.jgss,jdk.unsupported.desktop,jdk.xml.dom,jdk.zipfs,javafx.web,javafx.swing,java fx.media,javafx.graphics,javafx.fxml,javafx.controls,javafx.base --output jreCopy the code

Size 238M:

6.1.2.3 exe4j packaging

Exe4j use reference here, the first interface should look like this:

Configuration files are not available for first run, next will do.

Select JAR in EXE mode:

Fill in name and output directory:

Here the type is GUI Application, fill in the name of the executable file, select the icon path, and check allow a single application instance to run:

Redirection here, you can select the output directory of standard output stream and standard error stream, default if you do not need:

64-bit Win needs to check generate 64-bit executable file:

Next, the Java class and JRE path Settings:

Select the JAR generated by IDEA and fill in the main classpath:

Set the lowest and highest supported jre versions:

The next step is to specify the JRE search path, first removing the default three locations:

Next, select the JRE generated earlier, place the JRE in the same directory as the JAR, and fill the path with the JRE in the current directory:

Exe4j has Finished exe4j has Finished

This is generated using exe4j:

If there are no missing modules, it should be able to start normally. If there are missing modules, it will generate an error. Log in the current exe path by default.

6.1.3 Enigma Virtual Box packaging for three times

When packaged with Exe4j, you can run it directly, but the JRE is too large and you have to install an EXE. Fortunately, I used the Enigma Virtual Box packaging tool to package all the files into a single EXE.

Exe4j: exe4j: exe4j: exe4j

Then create a new JRE directory and add the JRE generated in the previous step:

Finally select compressed file:

The packaged single EXE is 65MB in size, which saves space compared to the 89MB JRE that exe4J also carries with it.

6.2 Back-end Deployment

The back-end deployment mode is also simple. WAR deployment mode is adopted. If the project is packaged in JAR package, it can be converted into WAR package by itself. Since the Web server is Tomcat, you can directly place the WAR package under webapps. You can search for other Web servers by yourself.

You can also deploy using Docker, but you need to use a JAR instead of a WAR and search for it yourself.

7 run

This project has been packaged. The front end includes JAR and EXE, and the back end includes JAR and WAR. Run the back end first (start database service first) :

Use the jar:

java -jar Backend.jar
Copy the code

Use war to put it directly into Tomcat webapps and then into bin:

./startup.sh
Copy the code

For Windows, you can run exe directly. For Linux, you can also run jar:

java -jar Frontend.jar
Copy the code

If the operation fails, you can open the project with IDEA and run it directly in IDEA or package it by yourself.

8 Precautions

8.1 Path Problems

Never use relative or absolute paths directly for resource files, such as:

String path1 = "/xxx/xxx/xxx/xx.png";
String path2 = "xxx/xx.jpg";
Copy the code

This has many problems, such as the possibility of running in IDEA directly and into jar package results are inconsistent, the path can not be read, and there may be platform problems, Linux path separator is known to be inconsistent with Windows. Therefore, the following methods are used to obtain resource files:

String path = getClass().getResource("/image/xx.png");
Copy the code

Image is located directly under the Resources folder. Other things are similar, so the/here stands for resources.

8.2 the HTTPS

HTTPS is not provided by default, the certificate file is not mounted, and the local port 8080 is used.

If you want to customize HTTPS, modify the front-end

  • com.test.network.OKHTTP
  • resources/key/pem.pem

At the same time, the back-end needs to modify Tomcat server.xml.

There are a number of articles about OkHttp using HTTPS, but most of them only describe how to configure HTTPS on the front end, not how to deploy it on the back end. Please refer to this article, which includes Tomcat configuration tutorials.

8.3 Configuring File Encryption

The configuration file is encrypted by the jasypt-spring-boot open-source component. The password can be set in three ways:

  • Command line arguments
  • Application environment variables
  • System environment variable

At present, the latest version is 3.0.3 (2020.05.31 updated 3.0.3. When the author used 3.0.2 version for encryption before, there was no problem in the local test, but when deployed to the server, there was always a message that the password could not be found, so we had no choice but to use the older 2.x version. However, after the new version was released, I tried to deploy Tomcat to the local server. There was no problem, but Tomcat was not deployed to the server.) It is recommended to deploy the latest version:

After all, the span is quite large, although this is a small bug fix, but IT is still recommended to try, it is estimated that there will not be a 3.0.2 problem.

In addition, remember to encode and convert fields containing Chinese:

str = new String(str.getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);)
Copy the code

In addition, the author has written the test file, directly replace the original ciphertext of the configuration file first, and fill in the plaintext to encrypt again:

Note if there is no set in the configuration file jasypt encryptor. If the password can be set in the run configuration VM Options (not recommended write password directly in the configuration file and, of course, the default is to use PBE encryption, Asymmetric encryption can be used with jasypt.encryptor.private-key-string or jasypt.encryptor.private-key-location) :

8.4 Keyboard Events

Keyboard events can be added using the following code:

scene.getAccelerators().put(new KeyCodeCombination(KeyCode.ENTER), ()->{xxx});
//getAccelerators returns ObservableMap
      ,>
Copy the code

Parent needs to get focus before responding:

parent.requestFocus();
Copy the code

8.5 database

MySQL > alter database app_test (test_user); MySQL > alter database test_password (test_password);

8.6 authentication code

By default, there is no built-in verification code function, which is not open due to privacy issues.

If you use Tencent cloud SMS API like the author, directly modify the corresponding attributes in the configuration file, it is recommended to encrypt.

If you use other apis, interconnect them by yourself. The front-end components need to be modified include:

  • com.test.network.OKHTTP
  • com.test.network.request.SendSmsRequest
  • com.test.network.requestBuilder.SendSmsRequestBuilder
  • com.test.controller.start.RetrievePasswordController

The backend needs to be modified:

  • com.test.controller.SmsController

If necessary, you can refer to the author’s Tencent cloud SMS API or search for other SMS verification API. Some apis written in configuration files require strong information such as keys

Nine source

Front and back end complete code and packaging:

  • Github
  • Yards cloud

10 Deficiencies of the project

In fact, the whole project still has many shortcomings, such as:

  • Part of the front end Scene switch is faulty
  • You can use Jackson instead of Gson in exchange for faster conversion speed
  • No caching mechanism
  • Front-end logs cannot be sent to the back-end for analysis
  • You can use binary instead of JSON for faster transport

However, update is not considered at present. If there are readers who have their own ideas, they can modify as needed. Here are the ideas for modification.

11 reference

1. Introduction and use of CSDN-Maven-shade-plugin

2. Use of maven-assembly-plugin, one of the three packaging methods of CSDN-MAVEN

3. Zhihu – Make JRE including Java 11 and JavaFX

4. CSDN- Use Exe4J to convert Java files into exe files to run the detailed tutorial

5, making – jasypt – spring – the boot issue

6, w3cschool – deployment headaches

7. Linux Tomcat+Openssl one-way/two-way authentication

If you think the article looks good, please like it.

At the same time, welcome to pay attention to wechat public number: Lingzhi Road.