這是一篇記錄模擬遠程部署用戶登錄項目的筆記。這是一個測試項目,目的是熟悉相關部署流程,並沒有使用真實的服務器做項目部署,而是以虛擬機 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
其中
netstat
命令查看
netstat -tunlp | grep 1099
,如果被占用則換其他端口號。注意使用
\
連接換行的字符串,表示它們屬於名為 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 目錄下。
參考
-
Run/Debug Configuration: Tomcat Server
這是官方幫助文檔,根據 IDEA 的版本有所區別,可以到自己 IDEA 的 Run/Debug Configuration 界面的點擊下方❔(help)到對應版本的幫助頁,或者直接把連接中的 2021.2 改成需要的版本號即可。
-
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received
-
tomcat上運行servlet使用jdbc java.lang.ClassNotFoundException: com.mysql.jdbc.Driver