1. 程式人生 > 其它 >用 IDEA 部署一個 Servlet 使用者登入的 JavaWeb 專案到遠端 Tomcat 伺服器(虛擬機器 Ubuntu 環境)

用 IDEA 部署一個 Servlet 使用者登入的 JavaWeb 專案到遠端 Tomcat 伺服器(虛擬機器 Ubuntu 環境)

這是一篇記錄模擬遠端部署使用者登入專案的筆記。這是一個測試專案,目的是熟悉相關部署流程,並沒有使用真實的伺服器做專案部署,而是以虛擬機器 Ubuntu 系統作為服務端。在本地(主機)的 IDEA 上遠端部署專案到遠端(虛擬機器Ubuntu) 的 Tomcat 伺服器上。環境配置為:

  • 本地環境:Windows 10、Tomcat 8.5.34、Java 11.0.13、IDEA 2020.2.3
  • 伺服器環境: Ubuntu 18.04.6 LTS、Tomcat 8.5.34、Java 11.0.13、MySQL 8.0

注意:須保證本地環境中的 Tomcat 版本和伺服器環境中一致,來自 IDEA 官方提醒

(When working with a remote server, the same server version must be available locally.)且 Tomcat 須在 5 或以上的版本才能支援遠端部署(Deployment Tab-Note that deployment to a remote server is supported only for Tomcat 5 or later versions.)。Java 版本也最好保持一致會少踩很多坑。

1. 配置與啟動 Tomcat

遠端部署需要修改配置,找到 Tomcat 的安裝路徑(.../apache-tomcat-8.5.34),在該路徑下的 bin 目錄下,找到 catalina.sh 指令碼,這就是遠端部署需要的 Tomcat 啟動指令碼( 不需要遠端部署的情況下一般是通過執行 startup.sh 指令碼啟動 Tomcat)。

接下來需要向 catalina.sh 新增配置,IDEA 官方文件中同樣給出了說明(Deployment Tab- Also note that to be able to deploy applications to a remote Tomcat server, enable JMX support on the server. To do that, pass the following VM options to the server Java proces),配置如下。

CATALINA_OPTS="-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=1099 \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=false \
-Djava.rmi.server.hostname=<IPAddress>"
export CATALINA_OPTS

其中 替換為遠端伺服器的 IP 地址,比如這裡我的虛擬機器伺服器地址為 192.168.137.111。1099 是埠號,配置前需要檢查下是否被佔用,可以使用netstat命令檢視netstat-tunlp|grep1099,如果被佔用則換其他埠號。注意使用\連線換行的字串,表示它們屬於名為 CATALINA_OPTS 的同一段字串。

我們可以直接將這段程式碼寫到 catalina.sh 指令碼檔案中,就像下面這樣,

不過在 catalina.sh 中的說明文字中給出了配置環境變數的建議方式:不要直接放在該指令碼中,為了分開自定義配置,應該將你的配置程式碼放在 CATALINA_BASE/bin 路徑下的 setenv.sh 指令碼中,其中CATALINA_BASE 變數在預設情況下指的就是 Tomcat 的安裝路徑,如下圖。

預設情況下在 bin 目錄下沒有 setenv.sh 指令碼,那麼直接建立一個就好,注意新增可執行許可權(chmod),並將上面的配置程式碼複製到該指令碼檔案中,使 catalina.sh 指令碼保持其預設內容。

這樣就配置好了,接下來執行 catalina.sh 指令碼啟動 Tomcat,啟動命令為:

./catalina.sh run > /dev/null 2>&1 &

其中> /dev/null 2>&1 &的作用是把標準輸出和出錯處理都丟棄掉,這樣就免得一大堆輸出佔領你的螢幕。我們可以使用 jps 命令檢視當前 Java 程序,檢查是否啟動成功。

此時說明啟動成功了,這是就可以到本地(主機)瀏覽器中輸入“http://192.168.137.111:8080”訪問 Tomcat 主頁,這樣就可以看到了那隻小公貓了。

2. 專案資源準備(資料庫、網頁、Servlet)

其實不一定要等到專案資源都準備好了才進行遠端部署,可以先部署一個簡單的靜態網頁,然後再慢慢新增功能。所以可以先跳到第 3 步做遠端部署配置,配置好了再回來準備專案資源。

2.1 資料庫(MySQL)

使用 IDEA 遠端連線服務端 MySQL,為此需要先建立一個數據庫新使用者 dabule 用於遠端登入,然後在資料庫 testdb 中建立一個 users 表存放登入資訊(賬號和密碼),並給予 dabule 操作 users 表的查詢許可權。

CREATE USER dabule@'%' IDENTIFIED BY 'your password';
GRANT SELECT ON testdb.users TO dabule;

這樣就可以遠端通過 dabule 使用者訪問資料庫中 users 表中的賬戶資訊資料,在收到前端的登入請求時,比對賬號密碼確定登入是否有效,並返回前端登入結果(成功或失敗)。

我們可以從本地環境的命令列中登入 MySQL 測試遠端連線的有效性,如下圖,

也可以從本地 IDEA 中配置並資料庫連線,這樣可以很方便地在 IDEA 中測試 SQL 語句,配置過程如下圖,

資料庫連線失敗可以參考文末的連結進行檢查。接下來就可以在 IDEA 中測試資料庫語句了,點選這裡即可,

接下里就可以使用 JDBC 建立資料庫訪問模組,確保 JDBC 驅動版本同樣也是 8.x 版本的,否則很可能會出現驅動載入問題。驅動配置檔案 db.properties 內容如下:

driverclass=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://192.168.137.111:3306/testdb?serverTimezone=UTC&characterEncoding=utf-8&userSSL=false
username=dabule
password=xxxxxxx

接下來編寫 JDBC 與資料庫的連線程式,

public class DBUtils {
    // 1. 定義變數
    private static String url;
    private static String username;
    private static String password;
    private Connection connection;
    private PreparedStatement pps;
    private ResultSet resultSet;

    // 2. 載入驅動
    static {
        try {
            InputStream is = DBUtils.class.getClassLoader().getResourceAsStream("db.properties");
            Properties properties = new Properties();
            properties.load(is);
            String driverName = properties.getProperty("driverclass");
            url = properties.getProperty("url");
            username = properties.getProperty("username");
            password = properties.getProperty("password");
            Class.forName(driverName);
        } catch (ClassNotFoundException | IOException e) {
            e.printStackTrace();
        }
    }

    // 3. 獲得連線
    protected Connection getConnection() throws SQLException {
        connection = DriverManager.getConnection(url, username, password);
        return connection;
    }

    // 4. 得到預狀態通道
    protected void getPreparedStatement(String sql) throws SQLException {
        pps = getConnection().prepareStatement(sql);
    }

    // 5. 確定引數
    protected void setParameters(List list) throws SQLException {
        if (list != null && list.size() != 0) {
            for (int i = 0; i < list.size(); i++) {
                pps.setObject(i + 1, list.get(i));
            }
        }
    }

    // 7. 查詢
    protected ResultSet query(String sql, List list) throws SQLException {
        getPreparedStatement(sql);
        setParameters(list);
        resultSet = pps.executeQuery();
        return resultSet;
    }

    // 8. 關閉資源
    protected void closeAll(){
        try {
            Objects.requireNonNull(resultSet).close();
            Objects.requireNonNull(pps).close();
            Objects.requireNonNull(connection).close();
        } catch (SQLException e) {
            // e.printStackTrace();
            System.out.println("Closing failed!");
        } catch (NullPointerException ignored) {
        }
    }
}

在 DBUtils 類的基礎上,編寫獲取使用者資訊的資料訪問物件(DAO)如下,

public class UserInfoDaoImpl extends DBUtils implements UserInfoDao {
    @Override
    public User getUserInfoByName(String name, String password) {
        try {
            String sql = "select * from users where username=? and password=?";
            List<Object> list = Arrays.asList(name, password);
            ResultSet res = query(sql, list);
            if (res.next()) {
                return new User(res.getInt("user_id"), name, res.getString("password"));
            }
            return null;
        } catch (SQLException e) {
            // e.printStackTrace();
            System.out.println("Querying failed!");
        } finally {
            closeAll();
        }
        return null;
    }
    @Override
    public User getUesrInfoByTele(String telephoneNo, String password) {
        ...
    }
	@Override
    public User getUesrInfoByEmail(String emailAddress, String password) {
       	...
    }
}
public interface UserInfoDao {
    User getUserInfoByName(String name, String password);
    User getUesrInfoByTele(String telephoneNo, String password);
    User getUesrInfoByEmail(String emailAddress, String password);
}

UserInfoDaoImpl 中的三個方法實現方式基本相同,差別只在查詢語句和返回的 User 物件包含的資訊。User 類是一個簡單的 Java 物件(POJO),即只包含屬性及其 getter/setter 方法的類。

關於資料庫連線方面的其他可能出現的問題以及在 Ubuntu 上 MySQL 的解除安裝安裝(8.x)可以參考文末的連結。

2.2 前端網頁

編寫一個簡單的登入頁面,像這樣,

當用戶填寫賬號(名稱/電話/郵箱)和密碼,點選登入後,後端會返回簡單的登入結果,像這樣,

或者像這樣,

登入頁 index.jsp 如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>使用者登入</title>
        <script src="./js/jquery-1.11.1.js"></script>
        <script type="text/JavaScript" src="./js/bootstrap.js"></script>
        <script src="./js/loginValidation.js"></script>
        <link rel="stylesheet" href="./css/bootstrap.css" />
        <link rel="stylesheet" href="./css/login.css" />
    </head>

    <body>
        <div class="container login" style="width: 500px; margin: 24px auto">
        <div class="form-group">
        	<h2 class="title">使用者登入</h2>
        </div>
        <form id="regForm" class="form-horizontal" role="form" action="login_request"
            method="post" autocomplete="on">
            <div class="form-group">
                <div class="col-sm-2 col-xs-2" style="padding: 0 0 0 30px">
                    <select id="ac_type" class="form-control input-element"
                    style="padding: 6px 0" name="account_type">
                    <option value="0">名稱</option>
                    <option value="1">電話</option>
                    <option value="2">郵箱</option>
                    </select>
                </div>
                <div class="col-sm-9 col-xs-6">
                    <input id="ac" class="form-control input-element" type="text"
                    name="account" placeholder="名稱" />
                </div>
            </div>
            <div class="form-group">
                <label class="col-sm-2 col-xs-2 control-label" for="ps">密碼</label>
                <div class="col-sm-9 col-xs-6">
                	<input id="ps" class="form-control input-element" type="password"
                name="password" placeholder="密碼" />
                </div>
            </div>
            <div class="form-group item-align">
                <button type="submit" class="btn btn-info" value="登入">登入</button>
                <button name="clear" class="btn btn-warning" value="清除">清除</button>
            </div>
        </form>
        </div>
    </body>
</html>

自定義樣式檔案 ./css/login.css 程式碼如下:

a {
    text-decoration: none;
    color: grey;
    font-size: 16px;
}

.login {
    padding: 0 0;
    background-color: rgb(235, 235, 235);
    border-radius: 5px;
}

.title {
    height: 60px;
    line-height: 60px;
    margin: 0 auto;
    padding: 0;
    color: #2c2c2c;
    text-align: center;
    background-color: rgb(127, 214, 255);
    border-radius: 5px 5px 0 0;
}

.select-font {
    font-size: 16px;
}

.input-element {
    background: rgb(235, 235, 235);
}

.item-align {
    text-align: center;
}

button:hover {
    font-size: large;
}

.text-format {
    display: block;
    /* width: 20%; */
    margin: 0 auto;
    font-size: 16px;
    text-align: center;
}

.text-format:hover {
    color: rgb(88, 173, 145);
    font-size: 18px;
}

自定義 JavaScript 檔案 ./js/loginValidation.js 程式碼如下:

function addErrorStyle(ele) {
    ele.css("color", "red");
    ele.parent().parent().addClass("has-error");
}
function rmErrorStyle(ele) {
    ele.parent().parent().removeClass("has-error");
    ele.css("color", "#666666");
}

const acInvalid = "不能為空!";
let isAcWrong = false; // 賬號資訊為空時,認為資訊錯誤
function inputAccount() {
    const account = $(this);
    if (isAcWrong) {
        isAcWrong = false;
        account.val("");
        account.attr("placeholder", $("#ac_type option[value="+acTypeVal+"]").text());
        rmErrorStyle(account);
    }
}
function isValidAccount() {
    const account = $("#ac");
    if (account.val() === "") {
        isAcWrong = true;
        account.attr("placeholder", $("#ac_type option[value="+acTypeVal+"]").text()+acInvalid);
        addErrorStyle(account);
        return false;
    }
    return !isAcWrong;
}

let acTypeVal = 0;
function changeAcType() {
    const acType = $("#ac_type");
    acTypeVal = acType.val();
    const option = acType.find("option[value=" + acTypeVal + "]");
    $("#ac").attr("placeholder", option.text());
}

const psInvalid = "密碼不能為空!";
let isPsWrong = false; // 密碼為空時,認為出現錯誤
function inputPassword() {
    const password = $(this);
    if (isPsWrong) {
        isPsWrong = false;
        password.val("");
        password.attr("placeholder", "密碼");
        rmErrorStyle(password);
    }
}
function isValidPassword() {
    const password = $("#ps");
    if (password.val() === "") {
        isPsWrong = true;
        password.attr("placeholder", psInvalid);
        addErrorStyle(password);
        return false;
    }
    return !isPsWrong;
}

const regForTelCode = /^1[0-9]{10}$/;
const regForEmail = /^\w+@[a-zA-Z0-9]{2,10}(?:\.[a-z]{2,4}){1,3}$/;
function isTeleCode(str) {
    return regForTelCode.test(str)
}
function isEmailAdress(str) {
    return regForEmail.test(str);
}

$(function() {
    const acType = $("#ac_type");
    const account = $("#ac");
    const password = $("#ps");
    acType.change(changeAcType);
    acType.blur(isValidAccount);
    account.click(inputAccount);
    account.blur(isValidAccount);
    password.click(inputPassword);
    password.blur(isValidPassword);
    $("#regForm").submit(function() {
        if (!isValidAccount() || !isValidPassword()) {
            return false;
        }
        if (isTeleCode(acVal)) {
            $("#ps").val("T-" + password.val());
        } else if (isEmailAdress(acVal)) {
            $("#ps").val("E-" + password.val());
        }
        return true;
    });
    $("button[name='clear']").click(function() {
        acType.get(0).selectedIndex = acTypeVal;
        account.val("");
        password.val("");
        rmErrorStyle(account);
        rmErrorStyle(password);
    });
});

2.3 Servlet 使用

前面準備好了資料庫的連線程式和前端網頁檔案,現在需要將他們放到一個 Java 專案中,並使用 Servlet 將它們關聯起來。在 IDEA 中通過建立一個 JavaWeb 專案的方式將它們整合在一起。但較新版本 IDEA 的 New Project 中預設情況下沒有建立 Web Application 專案的選項,這是需要將其重新加入到專案型別列表中,方法如下圖,到 Help->Find Action 中輸入 “Maintenance”(可以看到由相應的快捷鍵ctrl+alt+shift+/),勾選圖中項,就可以建立 Web Application 專案了。

得到的專案相比於普通的 Java 專案多出了一個 web 目錄,像這樣,

接下來將之前的檔案加入到專案中,資料庫訪問類仍然放到 src 目錄下,網頁檔案放入 web 目錄下,如下圖,

其中 control 目錄下的 LoginControl 類中實現了 Servlet 的請求處理,即接收前端請求,訪問資料庫,實現請求處理與返回邏輯,程式碼如下,

@WebServlet(urlPatterns="/login_request",
            initParams = {@WebInitParam(name="encoding", value="utf-8")}
)
public class LoginControl extends HttpServlet {
    private final UserInfoDao uid;
    private String encoding;

    public LoginControl() {
        uid = new UserInfoDaoImpl();
    }

    @Override
    public void init(ServletConfig config) {
        encoding = config.getInitParameter("encoding");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        req.setCharacterEncoding(encoding);
        User user;
        String account = req.getParameter("account");
        String password = req.getParameter("password");
        int accountType = Integer.parseInt(req.getParameter("account_type"));
        if (accountType == 1) {
            // 客戶端輸入的是電話號碼
            user = uid.getUesrInfoByTele(account, password);
        } else if(accountType == 2) {
            // 客戶端輸入的是郵箱
            user = uid.getUesrInfoByEmail(account, password);
        } else  {
            // accountType == 0,客戶端輸入的是使用者名稱
            user = uid.getUserInfoByName(account, password);
        }
        if (user == null) {
            resp.sendRedirect("./failure.html");
        } else {
            resp.sendRedirect("./success.html");
        }
    }
}

這裡採取了 @WebServlet 註解的方式配置 Servlet,還有一種方式是使用 web.xml 檔案進行配置,它在 web/WEB_INF 目錄下,

上面的註解配置等價於在 web.xml 中進行如下 6~17 行的配置,

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>name1</servlet-name>
        <servlet-class>xxx.LoginControl</servlet-class>
        <context-param>
            <param-name>encoding</param-name>
            <param-value>utf-8</param-value>
    	</context-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>name1</servlet-name>
        <url-pattern>/login_request</url-pattern>
    </servlet-mapping>
</web-app>

兩者可以同時存在,當在 頭中新增metadata-complete="true"屬性值時,web.xml 就會不支援同時使用註釋配置,該引數不些的情況些預設為 “false”。

注意到上圖中 Servlet 和 JDBC 的依賴包被新增到了 WEB_INF 目錄下,這樣做可以避開一個在 Servlet 中使用 JDBC 出現找不到 Driver 的異常,詳情參考這裡。當然移動依賴包後記得在專案配置一下依賴路徑,像這樣:

3. IDEA 遠端部署 Tomcat 專案

專案檔案準備好了,接下來就剩下部署了。進入 IDEA 的 Run/Debug Configuration 配置 Tomcat 遠端伺服器,進入後選擇新增遠端 Tomcat,如下圖:

然後開始在 Run/Debug Configuration->Server 中配置具體內容,如下圖;

其中,點選 Remote staging->Host 後面的配置鍵,進入遠端伺服器連線的配置,如下圖。選擇 SFTP 連線(基於 SSH 協議),正確填入你的遠端伺服器 IP 地址、使用者名稱以及登入密碼,點選 Test Connection 測試連線,彈窗提示成功連線就 OK 了。

接下來到 Run/Debug Configuration->Deployment 中配置需要部署的專案包,本地專案檔案通過 war 包的傳送到遠端 Tomcat 伺服器中的指定位置,即 Run/Debug Configuration->Server 中配置的 webapps 路徑下。

在這裡我們可以設定專案的訪問路徑,它會被新增在在“http://192.168.137.111:8080”後面,組成完整的外部訪問路徑,預設情況為 IDEA 專案的名稱,這裡我配置為“/login”表示這是一個登入入口。當只有一個專案需要部署時,可以直接簡化為“/”或空字串,這樣“http://192.168.137.111:8080”訪問到的就是我們的專案主頁而不是之前的 Tomcat 預設主頁了。

最後,來看看部署到遠端 Tomcat 伺服器上的專案檔案結構是什麼樣的,

可以看到原本的專案結構被調整了,前端頁面被放到了一級目錄下,後端檔案被放到了 WEB_INF 目錄下。

參考

  1. Run/Debug Configuration: Tomcat Server

    這是官方幫助文件,根據 IDEA 的版本有所區別,可以到自己 IDEA 的 Run/Debug Configuration 介面的點選下方❔(help)到對應版本的幫助頁,或者直接把連線中的 2021.2 改成需要的版本號即可。

  2. idea部署專案到遠端tomcat

  3. 在 Idea 中配置遠端 tomcat 並部署

  4. 遠端連線mysql失敗了怎麼辦

  5. The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received

  6. Ubuntu徹底解除安裝MySQL,徹底!親測!

  7. Ubuntu安裝MySQL8.0

  8. IDEA 中沒有 web Application

  9. tomcat上執行servlet使用jdbc java.lang.ClassNotFoundException: com.mysql.jdbc.Driver