\begingroup

这个 JavaFX 程序只是允许用户注册用户名和密码,然后将其存储在 SQL 数据库中。

有人批评它不够干净、易读或不可维护,但它似乎仍然有效,所以我希望寻求真正从事软件开发行业的人士而不是高中教师的批评。

public class MainController {
    // region Variables
    @FXML
    private Label formText, welcomeText;
    @FXML
    private Button login, signup;
    @FXML
    private TextField username, email, password, confirmPassword;
    @FXML
    private Button forgotPassword, formButton, resetPasswordButton;
    @FXML
    private AnchorPane formPage, dashboardPage;
    // endregion

    // region Form
    @FXML
    private void ChangeForm() {
        ObservableList<String> shortLogin = login.getStyleClass(), shortSignUp = signup.getStyleClass();
        if (shortLogin.contains("active")) { // switching to signup
            formText.setText("Signup Form");
            shortLogin.remove("active");
            shortLogin.add("notActive");
            shortSignUp.remove("notActive");
            shortSignUp.add("active");
            confirmPassword.setVisible(true);
            formButton.setText("Sign Up");
            forgotPassword.setVisible(false);
        } else /*if (shortSignUp.contains("active"))*/ { // switching to login
            formText.setText("Login Form");
            formButton.setText("Login");
            shortSignUp.remove("active");
            if(!shortSignUp.contains("notActive"))
                shortSignUp.add("notActive");
            shortLogin.remove("notActive");
            shortLogin.add("active");
            confirmPassword.setVisible(false);
            formButton.setText("Login");
            password.setPromptText("Password:");
            forgotPassword.setVisible(true);
        }
        ClearForm();
    }

    @FXML
    private void FormSubmit() {
        if (ValidForm()) {
            try {
                String name = (signup.getStyleClass().contains("active")) ? SQLUtils.Register(username.getText(), password.getText(), email.getText()) : SQLUtils.Login(username.getText(), password.getText(), email.getText());
                formPage.setVisible(false);
                dashboardPage.setVisible(true);
                welcomeText.setText("Welcome, " + name);
                ClearForm();
            } catch (Exception ignored) {
                ErrorAlert(Alert.AlertType.ERROR, "SQL Error", "Error Retrieving SQL Information from MainController", "There was an error retrieving the SQL information, or that user doesn't exist.");
            }
        }
    }
    
    @FXML
    private void Forgot() {
        forgotPassword.setVisible(false);
        resetPasswordButton.setVisible(true);
        forgotPassword.setVisible(true);
        formText.setText("Forgot Password");
        formButton.setVisible(false);
        password.setPromptText("Enter New Password:");
        
        ObservableList<String> shortLogin = login.getStyleClass();
        if(shortLogin.contains("active") && !shortLogin.contains("notActive")) {
            shortLogin.remove("active");
            shortLogin.add("notActive");
        }
    }
    @FXML
    private void ResetPassword() {
        if(ValidForm()) {
            resetPasswordButton.setVisible(false);
            formButton.setVisible(true);
            forgotPassword.setVisible(true);
            formButton.setVisible(true);
            password.setPromptText("Password:");
            
            ObservableList<String> shortLogin = login.getStyleClass();
            formText.setText("Login Form");
            shortLogin.remove("notActive");
            shortLogin.add("active");
            SQLUtils.ResetPassword(username.getText(), password.getText(), email.getText());
            ClearForm();
        }
    }

    // endregion
    // region Utils
    private void ClearForm() {
        username.clear();
        email.clear();
        password.clear();
        confirmPassword.clear();
    }
    
    private boolean ValidForm() {       
        String emailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9._]+\\.[a-zA-Z]{2,6}$";
        String passwordRegex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[/~`!@#$%^&*()_+{};:',<.>? =]).{8,}$";

        if (username.getText().isEmpty() || email.getText().isEmpty() || password.getText().isEmpty() || (signup.getStyleClass().contains("active") && confirmPassword.getText().isEmpty())) {
            ErrorAlert(Alert.AlertType.INFORMATION, "Form Validation", "Invalid Fields", "All Fields Must Be Filled In");
            return false;
        } else if (!Pattern.compile(emailRegex).matcher(email.getText()).matches()) {
            ErrorAlert(Alert.AlertType.INFORMATION, "Form Validation", "Invalid Email", "Please Enter A Valid Email That Contains An '@' And A '.com'");
            return false;
        } else if (!Pattern.compile(passwordRegex).matcher(password.getText()).matches()) {
            ErrorAlert(Alert.AlertType.INFORMATION, "Form Validation", "Invalid Password", "Please Enter A Valid Password That Contains At Least 8 Characters, 1 Uppercase, 1 Lowercase, 1 Number, and 1 Special Character");
            return false;
        } else if (signup.getStyleClass().contains("active") && !password.getText().equals(confirmPassword.getText())) {
            ErrorAlert(Alert.AlertType.INFORMATION, "Form Validation", "Passwords Must Match", "Password And Confirm Password Must Match");
            return false;
        } else if (!SQLUtils.ValidInfo(username.getText(), password.getText(), email.getText())) {
            ErrorAlert(Alert.AlertType.ERROR, "Invalid Info", "That User Does Not Exist", "Please enter valid information for a user that does already exist.");
            return false;
        }
        return true;
    }
    public static void ErrorAlert(Alert.AlertType type, String title, String headerText, String contentText) {
        Alert alert = new Alert(type);
        alert.setTitle(title);
        alert.setHeaderText(headerText);
        alert.setContentText(contentText);
        alert.showAndWait();
    }
    @FXML
    private void LogOut() {
        formPage.setVisible(true);
        dashboardPage.setVisible(false);
        welcomeText.setText("Welcome, NAME HERE");
    }

    // endregion
    // region Window Settings
    @FXML
    private void Minimize(ActionEvent event) {
        ((Stage) ((Button) event.getSource()).getScene().getWindow()).setIconified(true);
    }

    @FXML
    private void Close() {
        System.exit(0);
    }

    // endregion
}

public class SQLUtils {
    // region Main Methods
    public static String Login(String username, String password, String email) {
        String sql = "select * from users_table where username = ? and password = ? and email = ?";
        RunSQL(sql, username, password, email, true);
        return username;
    }
    public static String Register(String username, String password, String email) {
        String sql = "insert into users_table (username, password, email) values (?, ?, ?)";
        RunSQL(sql, username, password, email, false);
        return username;
    }
    public static void ResetPassword(String username, String newPassword, String email) {
        String sql = "update users_table set password=? where username=? and email=?;";
        RunSQL(sql, newPassword, username, email, false);
    }
    // endregion
    // region Utils
    private static Connection ConnectDB() {
        try {
            return DriverManager.getConnection("jdbc:mysql://localhost:3306/login_and_register", "root", "password");
        } catch (Exception ignored) {
            MainController.ErrorAlert(Alert.AlertType.ERROR, "SQL Error", "Error Retrieving SQL Information", "Information could not be retrieved");
        }
        return null;
    }
    public static boolean ValidInfo(String username, String password, String email) {
        String sql = "select * from users_table where username = ? and password = ? and email = ?";
        Connection connect = ConnectDB();
        if (connect == null)
            return false;
        
        try (PreparedStatement prepared = connect.prepareStatement(sql)) {
            prepared.setString(1, username);
            prepared.setString(2, password);
            prepared.setString(3, email);
            prepared.executeQuery();
            System.out.println("working");
            // FORM ALWAYS RESULTS IN WORKING, EVEN WHEN USER IS INVALID, DOES NOT ADD TO TABLE THO
            return true;
        } catch (Exception ignored) {
            MainController.ErrorAlert(Alert.AlertType.ERROR, "Error", "Error Running SQL", "There was an error running the SQL information, or that user doesn't exist.");
        }
        System.out.println("not working");
        return false;
    }
    private static void RunSQL(String sql, String username, String password, String email, boolean query) {
        Connection connect = ConnectDB();
        if (connect == null)
            return;
        
        try (PreparedStatement prepared = connect.prepareStatement(sql)) {
            prepared.setString(1, username);
            prepared.setString(2, password);
            prepared.setString(3, email);
            if (query)
                prepared.executeQuery();
            else
                prepared.executeUpdate();
        } catch (SQLException ignored) {
            MainController.ErrorAlert(Alert.AlertType.ERROR, "SQL Error", "Error Retrieving SQL Information, from RUNSQL", "There was an error retrieving the SQL information.");
        } catch (Exception ignored) {
            MainController.ErrorAlert(Alert.AlertType.ERROR, "Error", "Error Running SQL", "There was an error running the SQL information, or that user doesn't exist.");
        }
    }
    // endregion
}

\endgroup

0


最佳答案
2

\begingroup

多余的评论

    // region Main Methods
    ...
    // endregion [Main Methods]
    // region Utils
    ...
    // endregion [private utility methods]

请删除这些评论。它们毫无帮助,我担心未来的维护者会在不进行区域调整的情况下更改 {public, private} 范围。让语言自己说话吧。这些评论对于范围关键字来说是多余的。

哈希

这太疯狂了:

        String sql = "select * from users_table
                      where username = ? and password = ? and email = ?";

关于这个主题有很多文献;这里有一个
。它说,“存储 argon2id 哈希,而不是用户输入的明文密码。”

请不要设计和操作存储明文密码的安全代码。并且绝对不要鼓励学生这样做。

如果我们试图“为了教学目的而简化”,那么要么调用一些超出课程范围的不透明辅助库,要么从课程中删除“密码”的概念。

nit:我通常希望用户名是
。对于我和未来的维护者来说,让 ( alice, alice@example.com) 使用“password123”登录,同时让 ( alice, alice@example.net) 使用“password456”登录,这似乎有点冒险。写一条解释性注释,或者从签名中删除第三个参数。

误导性案例

这比它应该的要难读:

        RunSQL(sql, username, password, email, true);

有标准的 Java

可以解释这一点

方法应该是动词,……首字母小写,

请选择其他名称,或许query()。 类似评论适用于login()register()等。

在助手程序中,我们在预期的query位置看到了布尔值。isQuery

nit:...from RUNSQL诊断不准确。

公共 API 设计不良

私有助手无条件地进行三次prepared.setString()调用,这显然促使我们做出需要用户名和电子邮件的设计决定login()。不要让内部实现细节决定公共 API 的形状。首先设计它,然后让助手根据需要发现下游需求,例如在查询中提供可变数量的参数。

异常使用不当

validInfo()助手中,
遵循

prepared.executeQuery()是它应返回零行或多行。或者它根本就不完成,例如如果有人对 users_table 执行了 DROP。

您的助手做得太少 —— 它忽略了计算单个结果行是否返回。

        } catch (Exception ignored) {
            MainController.ErrorAlert(Alert.AlertType.ERROR, "Error", ...

在调用处我们看到了这一点:

        } else if (!SQLUtils.ValidInfo(username.getText(), password.getText(), email.getText())) {
            ErrorAlert(Alert.AlertType.ERROR, "Invalid Info", ...

助手试图抓太多东西。

事情可能会以多种方式变得糟糕,validForm()
调用者会在需要时通知用户。让各种异常在调用堆栈中冒泡,这样
validForm()就可以根据需要处理它们。低级例程通常不是从错误中“恢复”的正确地方。如果 Java 是你第一次遇到异常的地方,那么所谓的“检查异常”往往会使学习这个概念变得困难。

不适当的三元运算符

请将formSubmit()这一行代码修改为:

                String name = (signup.getStyleClass().contains("active")) ? SQLUtils.Register(username.getText(), password.getText(), email.getText()) : SQLUtils.Login(username.getText(), password.getText(), email.getText());

这简直难以阅读。else拆分多行时,请充分利用关键字。

不适当的正则表达式

        String emailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9._]+\\.[a-zA-Z]{2,6}$";
        String passwordRegex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[/~`!@#$%^&*()_+{};:',<.>? =]).{8,}$";

电子邮件地址如下:localpart@globalpart

我还没有看过您的要求文档,所以我还不明白为什么您禁止在 localpart 中使用 unicode 字符。有很多人的名字,因此也有很多电子邮件地址,其中有重音元音和文字,例如印地语、阿拉伯语、中文。如果您选择仅支持现代电子邮件技术的子集,请使用restrictedEmail或之类的标识符limitedEmail,并记录限制。通常,您需要询问收件人电子邮件服务器给定的 localpart 是否“良好”。

globalpart 表达式很愚蠢。TLD 不止

{com、net、org}。由于我尚不了解的原因,您禁止

,好吧,我还可以继续。RFC 7085 描述了一组支持电子邮件的无点域
.academy.accountant.apartments.associates

与其使用正则表达式,不如对 globalpart 发出三个 DNS 查询。询问域是否有 MX、A 或 AAAA 记录。这可以很快完成,并且无需联系端口 25 进行此检查。它可以帮助用户确定他们犯了拼写错误。

验证用户地址的唯一真正方法是通过电子邮件向他们发送 GUID 链接,并查看他们是否点击您的链接,以证明他们了解 GUID。

如果您在这个项目中的真正目标是检测常见错误,例如将错误的内容粘贴到错误的框中,那么一个简单的正则表达式就.+@.+可以帮助解决大部分问题。

状态机

changeForm()一些约束条件,例如“只有一个窗格可见”,诸如此类。通过写下 /** 一个或多个句子 */ 使其明确。

如果您认为这会对未来的维护者有所帮助,请考虑绘制

\endgroup

\begingroup

连接管理

SQLUtils创建新连接的所有方法都Connection没有考虑如何关闭它,这是一个严重的问题。您应该尽快释放数据库连接,而不是在服务器端积累空闲连接。

仅仅关闭aStatement是不够的,它不会导致标的Connection被关闭。

只会关闭ResultSet与其关联的(除非遇到不符合链接规范的 JDBC 驱动程序的古怪行为,这种情况有时会发生)。

因此,每次获取 Connection 实例时,都应在try-with-resourcestry语句中将其声明为资源,以确保它将被关闭。

设计问题

主要问题是您的组件之间没有明确的关注点分离

  • 抽象不够发达/抽象缺失

您需要适当的抽象来负责与数据库通信(而不是一堆静态方法)。为了改进您的设计,SQLUtils您可以实现具有基本 CRUD 功能的数据访问对象,或者针对特定领域用例的存储库。

而且你显然缺少了一项服务login()这不是数据层抽象所负责的。单独的抽象(应用服务)应该负责用户管理。

请注意,每种域类型都应该有自己专用的持久性相关抽象,无论是存储库还是普通 DAO。

并且该服务将依赖于用户存储库和负责密码哈希处理的组件。

J_H 已经您不应该以纯文本形式存储密码,我谈论的组件将封装加密函数(例如 BCrypt 或 Argon2)并公开用于生成哈希值和验证给定密码是否与从数据库检索到的哈希值匹配的方法。您可以从Spring Security 项目中获得灵感。

  • 方法缺乏明确意图

方法login()validInfo()在数据层中是非常奇怪的怪物,它们的名字在这里看起来很混乱。它们执行的查询与业务用例不匹配。

它们都应该被替换为类似的东西findUserByUsername()如果用户名是唯一的,或者通过电子邮件,取决于应用程序的不变量),它需要一个参数User并返回基于获取的数据构造的实例(其余的不应该是这个类的责任)。

如果您彻底实施每个用例,就会发现该方法runSQL()不再有用,并且本质上它是绝对任意的。如果您想摆脱原始 JDBC 的冗长,请考虑引入 JDBC 或 JPA 框架。

  • 数据访问逻辑受到演示问题的污染

与数据库对话的类应该知道任何有关 UI 的信息。你的数据库中不应该出现类似这样的内容:

MainController.ErrorAlert("display some message to the user"); 

为了避免这种耦合,你可以使用全局异常处理程序来处理异常。像 Spring 这样的框架可以简化这项任务,但你也可以使用以下方法来完成

Thread
    .currentThread()
    .setUncaughtExceptionHandler(<ClassName>::handleUncaught)
private static void handleUncaught(Thread thread, Throwable throwable) {
        // error-handling logic here; 
        // use Java 21 Pattern Matching for switch expressions 
        // to determine what action to perform based on the exception type
    }
}

代码风格

请不要这样做,这很容易出错。{ }在这种情况下使用:

...
formButton.setText("Login");
shortSignUp.remove("active");
if(!shortSignUp.contains("notActive"))
    shortSignUp.add("notActive");
shortLogin.remove("notActive");
shortLogin.add("active");
...

按照惯例,关键字(例如if)和括号之间总是应该有一个空格。

不要强迫代码阅读器向右滚动:

String name = (signup.getStyleClass().contains("active")) ? SQLUtils.Register(username.getText(), password.getText(), email.getText()) : SQLUtils.Login(username.getText(), password.getText(), email.getText());

\endgroup