2020.07.22更新
1 概述
1.1 簡介
一個簡單的小型薪酬管理系統,前端JavaFX+后端Spring Boot,功能倒沒多少,主要精力放在了UI和前端的一些邏輯上面,后端其實做得很簡單。
主要功能:
- 用戶注冊/登錄
- 驗證碼找回密碼
- 用戶修改信息,修改頭像
- 柱狀圖形式顯示薪酬
- 管理員管理用戶,錄入工資
1.2 響應流程
1.3 演示
登錄界面:
用戶界面:
管理員界面:
2 環境
2.1 本地開發環境
- Manjaro 20.0.3
- IDEA 2020.1.1
- OpenJDK 11.0.7.u10-1
- OepnJFX 11.0.3.u1-1
- Spring Boot 2.3.0
- MySQL 8.0.20
2.2 服務器環境
- CentOS 8.1.1911
- OpenJDK 11
- Tomcat 9.0.33
- MySQL 8.0.17
3 前端代碼部分
3.1 前端概述
前端主要分為5個部分實現:控制器模塊,視圖模塊,網絡模塊,動畫模塊還有工具類模塊。
- 控制器模塊:負責交互事件
- 視圖模塊:負責更新UI
- 網絡模塊:向后台發送數據請求
- 動畫模塊:位移、縮放、淡入/淡出、旋轉動畫
- 工具類模塊:加密,檢查網路連通,居中界面等
3.2 概覽
3.2.1. 代碼目錄樹
說明:
constant
包:項目所需要的字符串常量以及一些枚舉常量controller
包:控制器類,負責UI與用戶的交互entity
包:實體類log
包:日志類network
包:負責網絡請求,包括請求生成以及請求發送transition
包:負責處理動畫utils
包:工具類view
包:負責UI的初始化預計更新
3.2.2 資源目錄樹
說明:
css
:界面所用到的樣式fxml
:一個特殊的xml文件,用於定義界面與綁定Controller中的函數,也就是綁定事件image
:靜態圖片key
:證書文件,用於OkHttp中的HTTPS連接properties
:項目中的一些常量屬性
3.2.3 項目依賴
主要依賴如下:
- Gson:用於在實體類以及Map與JSON字符串之間進行轉換
- Log4j2:日志
- Lombok:神器不解釋,但是有一些聲音說不要使用,可以參考這里或這里,看個人啦
- OkHttp3:網路請求
- Apache Commons:工具類
- OpenJFX11:OpenJFX核心
3.3 常量模塊
包含程序所需要的字符串以及枚舉常量:
CSSPath
:CSS路徑,用於給Scene添加樣式,如scene.getStylesheets.add(path)
FXMLPath
:FXML路徑,用於FXMLLoader
加載FXML
文件,如FXMLLoader.load(getClass.getResource(path).openStream())
AllURL
:發送請求到后端的URLBuilderKeys
:OkHttp中的FormBody.Builder
中使用的常量鍵名PaneName
:Pane名字,用於在同一個Scene切換不同的PaneReturnCode
:后端返回碼,需要與后端協商ViewSize
:界面尺寸
重點說一下路徑問題,筆者的css與fxml文件都放在resources下:
其中fxml路徑在項目中的用法如下:
URL url = getClass().getResource(FXMLPath.xxxx);
FXMLLoader loader = new FXMLLoader();
loader.setLocation(url);
loader.load(url.openStream());
獲取路徑從根路徑獲取,比如上圖中的MessageBox.fxml:
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;
若fxml文件直接放在resources根目錄下,可以使用:
getClass().getResource("/xxx.fxml");
直接獲取。
css同理:
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;
網絡請求的URL建議把路徑寫到配置文件中,比如這里的從配置文件讀取:
Properties properties = Utils.getProperties();
if (properties != null)
{
String baseUrl = properties.getProperty("baseurl") + properties.getProperty("port") + "/" + properties.getProperty("projectName");
SIGN_IN_UP_URL = baseUrl + "signInUp";
//...
}
3.4 控制器模塊
控制器模塊用於處理用戶的交互事件,分為三類:
- 登錄注冊界面控制器(start包)
- 用戶界面控制器(worker包)
- 管理員界面控制器(admin包)
3.4.1 登錄注冊界面
這是程序一開始進入的界面,會在這里綁定一些基本的關閉,最小化,標題欄拖拽事件:
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()
{
GUI.close();
}
public void minimize()
{
GUI.minimize();
}
登錄界面的控制器也很簡單,就一個登錄/注冊功能加一個跳轉到找回密碼界面,代碼就不貼了。
至於找回密碼界面,需要做的比較多,首先需要判斷用戶輸入的電話是否在后端數據庫存在,另外還要檢查兩次輸入的密碼是否一致,還要判斷短信是否發送成功,並且檢查用戶輸入的驗證碼與后端返回的驗證碼是否一致(短信驗證碼部分其實不需要后端處理,原本是放在前端的,但是考慮到可能會泄漏一些重要的信息就放到后端處理了)。
3.4.2 用戶界面
接着是用戶登錄后進入的界面,加了漸隱與移動動畫:
public void userEnter()
{
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()
{
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();
}
效果如下:
實際處理是把<Image>
以及<Label>
放進一個<AnchorPane>
中,然后為這個<AnchorPane>
添加鼠標移入與移出事件。從代碼中可以知道圖片加上了位移動畫,文字同時加上了淡入與位移動畫,多邊形同時加上了縮放與位移動畫。以左下的<AnchorPane>
事件為例,當鼠標移入時,首先把圖片左移:
.add(new Move(userImage).x(-70))
x表示橫向位移。
接着是淡入與位移文字:
.add(new Fade(userLabel).fromTo(0,1)).add(new Move(userLabel).x(95))
fromTo表示透明度的變化,從0到1,相當於淡入效果。
最后放大多邊形1.8倍同時右移多邊形:
.add(new Scale(userPolygon).ratio(1.8)).add(new Move(userPolygon).x(180))
ratio表示放大的倍率,這里是放大到原來的1.8倍。
同理右上方同樣需要進行放大與移動:
.add(new Scale(queryPolygon).ratio(1.8)).add(new Move(queryPolygon).x(180))
其中用到的Transition
,Scale
,Fade
是自定義的動畫處理類,詳情請看"3.8 動畫模塊"。
3.5 實體類模塊
簡單的一個Worker:
@Getter
@Setter
@NoArgsConstructor
public class Worker {
private String cellphone;
private String password;
private String name = "無姓名";
private String department = "無部門";
private String position = "無職位";
private String timeAndSalary;
public Worker(String cellphone,String password)
{
this.cellphone = cellphone;
this.password = password;
}
}
注解使用了Lombok,Lombok介紹請戳這里,完整用法戳這里。
timeAndSalary
是一個使用Gson轉換為String的Map,鍵為對應的年月,值為工資。具體轉換方法請到工具類模塊查看。
3.6 日志模塊
日志模塊使用了Log4j2,resources
下的log4j2.xml
如下:
<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>
這是最一般的配置,pattern
里面是輸出格式,其中
%d{HH:mm:ss}
:時間格式level
:日志等級n
:換行msg
:日志信息
這里前端的日志進行了簡化處理,需要更多配置請自行搜索。
3.7 網絡模塊
網絡模塊的核心使用了OkHttp實現,主要分為兩個包:
request
:封裝發送到后端的各種請求requestBuilder
:創建request的Builder類OKHTTP
:封裝OkHttp的工具類,對外只有一個靜態send方法,參數只有一個,request包中的類,使用requestBuilder生成。send方法返回一個Object,Object怎么處理需要在用到OKHTTP的地方與返回方法對應
3.7.1 request包
封裝了各種網絡請求:
所有請求繼承自BaseRequest,BaseRequest的公有方法包括:
setUrl
:設置發送的URLsetCellphone
:添加cellphone參數setPassword
:添加password參數,注意會經過前端的SHA-512加密setWorker
:添加Worker參數setWorkers
:接受一個List<Worker>,管理員保存所有Worker時使用setAvatar
:添加頭像參數setAvatars
:接受一個HashMap<String,String>,鍵為電話,標識唯一的Worker,值為圖片經過Base64轉換為的String
唯一一個抽象方法是:
public abstract Object handleResult(ReturnCode code):
根據不同的請求處理返回的結果,后端返回一個ReturnCode,其中封裝了狀態碼,錯誤信息與返回值,由Gson轉為String,前端得到String后經Gson轉為ReturnCode,從里面獲取狀態碼以及返回值。
其余的請求類繼承自BaseRequest
,並且實現不同的處理結果方法,以Get請求為例:
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("獲取失敗,電話號碼不匹配");
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;
}
}
}
獲取一個Worker,可能的返回值有(返回的是在ReturnCode中定義的枚舉值,需要前后端統一):
EMPTY_CELLPHOE
:表示發送的get請求中電話為空INVALID_CELLPHONE
:非法電話號碼,判斷的代碼為:String reg = "^[1][358][0-9]{9}$";return !(Pattern.compile(reg).matcher(cellphone).matches());
CELLPHONE_NOT_MATCH
:電話號碼不匹配,也就是數據庫沒有對應的WorkerEMPTY_WORKER
:數據庫中存在這個Worker,但由於轉換為String時后端處理失敗,返回一個空的WorkerGET_ONE_SUCCESS
:獲取成功,使用工具類轉換String為Worker- 其他:未知錯誤
3.7.2 requestBuilder包
包含了對應於request的Builder:
除了默認的構造方法與build方法外,只有set方法,比如:
public class GetOneRequestBuilder {
private final GetOneRequest request = new GetOneRequest();
public GetOneRequestBuilder()
{
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()
{
return request;
}
}
在默認構造方法里面設置了URL,剩下就只需設置電話即可獲取Worker。
3.7.3 OKHTTP
這是一個封裝了OkHttp的靜態工具類,唯一一個公有靜態方法如下:
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("服務器無法連通,響應為空");
}
return null;
}
采用同步POST請求的方式,用BaseRequest
作為基類是因為能在Call
中方便地獲取URL以及請求體,若數據量大可以考慮異步請求。
另外上面也提到后端返回的是經由Gson轉換為String
的ReturnCode
,所以獲取body后,先轉換為ReturnCode
再處理。
3.7.4 HTTPS
至於HTTPS,由於在Tomcat上進行部署,需要在Tomcat里設置證書,同時也需要在OkHttp中設置以下三部分:
sslSocketFactory
:ssl套接字工廠HostnameVerifier
:驗證主機名X509TrustManager
:證書信任器管理類
3.7.4.1 OkHttp配置
上面提到了需要設置三部分,下面來看看最簡單的一個驗證主機名部分,利用的是HostnameVerifier
接口:
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();
這里驗證主機名為www.test.com
就返回true(也可是使用公網ip驗證),否則使用默認的HostnameVerifier。業務邏輯復雜的話可以結合配置中心,黑/白名單等進行動態校驗。
接着是X509TrustManager
的處理(來源Java Code Example):
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()); // 這里添加自定義的密碼,默認
InputStream in = null; // By convention, 'null' creates an empty key store.
keyStore.load(in, password);
return keyStore;
} catch (IOException e) {
throw new AssertionError(e);
}
}
返回一個信任由輸入流讀取的證書的信任管理器,若證書沒有被簽名則拋出SSLHandsakeException
,證書建議使用第三方簽名的而不是自簽名的(比如使用OpenSSL
或者acme.sh
生成),特別是在生產環境中千萬不要使用自簽名的,例子的注釋也提到:
最后是SSL套接字工廠的處理:
private static SSLSocketFactory createSSLSocketFactory() {
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;
}
完整的OkHttpClient
構造如下:
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();
其中/key/pem.pem
為resources
下的證書文件。
3.7.4.2 服務器設置證書
使用WAR進行部署,JAR部署的方式請自行搜索,服務器Tomcat,其他web服務器請自行搜索。
首先在Tomcat配置文件中的conf/server.xml
修改域名:
找到<Host>
並復制,直接修改其中的name
為對應域名:
接着從證書廠商下載文件(一般都帶文檔,根據文檔部署),Tomcat的是兩個文件,一個是pfx,一個是密碼文件,繼續修改server.xml
,搜索8443, 找到如下位置:
其中上面的<Connector>
是HTTP/1.1協議的,基於NIO實現,下面的<Connector>
是HTTP/2的,基於APR實現。
使用HTTP/1.1會比較簡單一些,僅僅是修改server.xm
l即可,使用HTTP/2的話會麻煩一點,如果基於APR(Apache Portable Runtime)實現需要安裝APR,APR-util以及Tomcat-Native,可以參考這里,下面以HTTP/1.1的為例,修改如下:
<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>
修改證書位置以及密碼。如果想要更加安全的話可以指定使用某個TLS版本,比如使用TLS1.2版本:
<Connector ...
sslProtocol="TLS" sslEnabledProtocols="TLSv1.2"
>
3.7.5 圖片處理
圖片原本是想使用OkHttp的MultipartBody處理的,但是處理的圖片都不太,貌似沒有必要,而且實體類的數據都是以字符串的形式傳輸的,因此,筆者的想法是能不能統一都用字符串進行傳輸,於是找到了圖片和String互轉的函數,稍微改動,原來的函數需要外部依賴,現在改為了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);
}
}
Base64是一種基於64個可打印字符來表示二進制數據的方法,可以把二進制數據(圖片/視頻等)轉為字符,或把對應的字符解碼變為原來的二進制數據。
筆者實測這種方法轉換速度不慢,只要有了正確的轉換函數,服務器端可以輕松進行轉換,但是對於大文件的支持不好:
這種方法對一般的圖片來說足夠了,但是對於真正的文件還是建議使用MultipartBody進行處理。
3.8 動畫模塊
包含了四類動畫:
- 淡入/淡出
- 位移
- 縮放
- 旋轉
這四個類都實現了CustomTransitionOperation
接口:
import javafx.animation.Animation;
public interface CustomTransitionOperation {
double defaultSeconds = 0.4;
Animation build();
void play();
}
其中:
defaultSeconds
表示動畫默認持續的秒數build
用於Transition
中對各個動畫類進行統一的build
操作play
用於播放動畫
四個動畫類類似,以旋轉動畫類為例:
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() {
return transition;
}
@Override
public void play() {
transition.play();
}
}
seconds設置秒數,to設置旋轉的角度,所有動畫類統一由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()
{
animations.forEach(Animation::play);
}
}
里面是一個動畫類的集合,每次add操作時先生成對應的動畫再添加進集合,最后統一播放,示例用法如下:
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();
3.9 工具類模塊
AvatarUtils
:用於本地生成臨時圖片以及圖片轉換處理Check
:檢查是否為空,是否合法等Conversion
:轉換類,通過Gson在Worker/String
,Map/String
,List/String
之間進行轉換Utils
:加密,設置運行環境,居中Stage
,檢查網絡連通等
這里說一下Utils
與Conversion
。
3.9.1 Conversion
轉換類,利用Gson在String
與List
/Worker
/Map
之間進行轉換,比如String
轉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;
}
大部分的轉換函數類似,首先判空,接着進行對應的類型轉換,這里的Conversion與后端的基本一致,后端也需要使用Conversion類進行轉換操作。
3.9.2 Utils
獲取屬性文件方法如下:
//獲取屬性文件
public static Properties getProperties()
{
Properties properties = new Properties();
//項目屬性文件分成了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)
{
//防止亂碼
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;
}
}
另一個是檢查網路連通的方法:
public static boolean networkAvaliable()
{
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()
{
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;
}
采用socket進行判斷,准確來說是包含檢查網絡連通以及后端是否連通。
最后是居中Stage
的方法,盡管Stage中自帶了一個centerOnScreen,但是出來的效果並不好,筆者的實測是水平居中但是垂直偏上的,並不是垂直水平居中。
因此根據屏幕高寬以及Stage的大小手動設置Stage的x和y。
public static void centerMainStage()
{
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);
}
3.10 視圖模塊
GUI
:全局變量共享以及以及控制Scene
的切換MainScene
:全局控制器,負責初始化以及綁定鍵盤事件MessageBox
:提示信息框,對外提供show()
等的靜態方法
GUI中的方法主要為switchToXxx
,比如:
public static void switchToSignInUp()
{
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());
}
跳轉到登錄注冊界面,是公有靜態方法,首先判斷是否為用戶信息界面,如果是進行一些清理操作,接着是讓Parent
獲取焦點(為了讓鍵盤事件響應),然后將對應的AnchorPane
添加到Children
,並添加css,最后修改按鈕文字與事件。
另外還在MainScene
中加了一些鍵盤事件響應,比如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();
}
});
4 前端UI部分
4.1 fxml
界面基本上靠這些fxml文件控制,這部分沒太多內容,基本上靠IDEA自帶的Scene Builder設計,少部分靠代碼控制,下面說幾個注意事項:
- 根節點為AnchorPane,每個fxml設置一個獨立的
fx:id
以便切換 - 事件綁定在對應的控件中,比如在一個Label綁定鼠標進入事件,在這個Label上設置
onMouseEntered="#xxx"
,其中里面的方法為對應的控制器(fx:controller="xxx.xxx.xxx.xxxController"
)中的方法 <Image>
中的URL屬性需要帶上@
,比如<Image url="@../../image/xxx.png">
4.2 css
JFX中集成了部分css的美化功能,比如:
-fx-background-radius: 25px;
-fx-background-color:#e2ff1f;
用法是需要先在fxml中設置id。
這里注意一下兩個id的不同:
fx:id
id
fx:id
指的是控件的fx:id
,通常配合Controller中的@FXML
使用,比如一個Label設置了fx:id
為label1
<Label fx:id="label1" layoutX="450.0" layoutY="402.0" text="Label">
<font>
<Font size="18.0" />
</font>
</Label>
則可以在對應Controller中使用@FXML
獲取,名字與fx:id
一致:
@FXML
private Label label1;
而id
指的是css的id
,用法是在css引用即可,比如上面的Label又同時設置了id
(可以相同,也可不同):
<Label fx:id="label1" id="label1" layoutX="450.0" layoutY="402.0" text="Label">
<font>
<Font size="18.0" />
</font>
</Label>
然后在css文件中像引用普通id
一樣引用:
#label1
{
-fx-background-radius: 20px; /*圓角*/
}
同時JFX還支持css的偽類,比如下面的最小化與關閉的鼠標移入效果是使用偽類實現的:
#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;
}
當然一些比較復雜的是不支持的,筆者嘗試過使用transition之類的,不支持。
最后需要在對應的Scene里面引入css:
Scene scene = new Scene();
scene.getStylesheets().add("xxx/xxx/xxx/xxx.css");
程序中的用法是:
scene.getStylesheets().add(CSSPath.SIGN_IN_UP);
4.3 Stage構建過程
下面以提示框為例,說明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) {
//...
}
首先新建一個Stage
,接着利用FXMLLoader
加載對應路徑上的fxml文件,獲取Parent
后,利用該Parent
生成Scene
,再為Scene
添加樣式。
接着是控件的處理,這里的lookup
類似Android中的findViewById
,根據fx:id
獲取對應控件,注意需要加上#
。處理好控件之后,居中並顯示Stage
,同時,綁定鍵盤事件並讓Parent
獲取焦點。
5 后端部分
5.1 后端概述
后端以Spring Boot框架為核心,部署方式為WAR,整體分為三層:
- 控制器層:負責接受前端的請求並調用業務層方法
- 業務層:處理主要業務,如CRUD,圖片處理等
- 持久層:數據持久化,Hibernate+Spring Data JPA
總的來說沒有用到什么高大上的東西,邏輯也比較簡單。
5.2 概覽
5.2.1 代碼目錄樹
5.2.2 依賴
主要依賴如下:
- Spring Boot Starter Data JPA:數據持久化
- Guava:用於將
Iterable<Worker>
轉換為集合 - Lombok:同前端
- Gson:JSON轉換類
- Apache Commons:用於異常處理+隨機字符串生成
- TencentCloud SDK Java:短信驗證碼API
- Jasypt Spring Boot Starter:加密配置文件
5.3 控制器層
控制器分為三類,一類處理圖片,一類處理CRUD請求,一類處理短信發送請求,統一接受POST忽略GET請求。大概的處理流程是接收參數后首先進行判斷操作,比如判空以及判斷是否合法等等,接着調用業務層的方法並對返回結果進行封裝,同時進行日志記錄,最后利用Gson把返回結果轉為字符串。代碼大部分比較簡單就不貼了,說一下短信驗證碼的部分。
驗證碼模塊使用了騰訊雲的接口,官網這里,搜索短信功能即可。
新用戶默認贈送100條短信:
發送之前需要創建簽名與正文模板,審核通過即可使用。
可以先根據快速開始試用一下短信功能,若能成功收到短信,可以戳這里查看API(Java版)。
下面的例子由文檔例子簡化而來:
@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);
}
其中appId,sign,templateID
分別是對應的appid,簽名id與正文模板id,申請通過之后會分配的,然后隨機生成六位數字的驗證碼。
request.setPhoneNumberSet()
的參數為需要發送的手機號碼String數組,注意需要加上區號。發送成功的話手機會收到,失敗的話請根據異常信息自行判斷修改。
唯一要注意一下的是appid之類的數據通過@Value
進行屬性注入時,如:
@Controller
@RequestMapping("/")
public class SmsController {
@Value("${tencent.secret.id}")
private String secretId;
...
}
但是由於sign部分含有中文,所以需要進行編碼轉換:
@Value("${tencent.sign}")
private String sign;
@PostConstruct
public void init()
{
sign = new String(sign.getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);
}
5.4 業務層與持久層
由於程序中的業務層與持久層都比較簡單就合並一起說了,比如業務層的saveOne方法,保存一個Worker,先利用Gson轉換為Worker后直接利用CrudRespository<T,ID>
提供的save方法保存:
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;
}
另外由於CrudRepository<T,ID>
的saveAll方法參數為Iterable<S>
,因此可以直接保存List<S>
,比如:
public ReturnCode saveAll(List<Worker> workers)
{
workerRepository.saveAll(workers);
return ReturnCode.SAVE_ALL_SUCCESS;
}
需要在控制層中把前端發送的String轉換為List<S>
。
5.5 日志
日志用的是Spring Boot自帶的日志系統,只是簡單地配置了一下日志路徑,除此之外,日志的格式自定義(因為追求整潔輸出,感覺配置文件實現得不夠好,因此自定義了一個工具類)。
比如日志截取如下:
自定義了標題以及每行固定輸出,前后加上了提示符,內容包括方法,級別,時間以及其他信息。
總的來說,除了格式化器外總共有7個類,其中L是主類,外部類只需要調用L的方法,大部分是公有靜態方法,其余6個是L調用的類:
如備份成功時調用:
public Success
{
public static void backup()
{
l.info(new FormatterBuilder().title(getTitle()).info().position().time().build());
}
//...
}
其中FormatterBuilder
是格式化器,用來格式化輸出的字符串,方法包括時間,位置,級別以及其他信息:
public FormatterBuilder info()
{
return level("info");
}
public FormatterBuilder time()
{
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;
}
5.6 工具類
四個:
- Backup:定時數據庫備份
- Check:檢查合法性,是否為空等
- Conversion:轉換類,與前端的幾乎一致,利用Gson在
String
與List/Map/Worker
之間進行轉換 - ReturnCode:返回碼枚舉類
重點說一下備份,代碼不長就直接整個類貼出來了:
@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()
{
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());
}
}
}
首先利用@Value
獲取配置文件中的值,接着在備份方法加上@Scheduled
。@Scheduled
是Spring Boot用於提供定時任務的注解,用於控制任務在某個指定時間執行或者每隔一段時間執行(這里是半天一次),主要有三種配置執行時間的方式:
- cron
- fixedRate
- fixedDelay
這里不展開了,詳細用法可以戳這里。
另外在使用前需要在類上加上@EnableScheduling
。備份時首先利用URL獲取數據庫名,接着拼合備份命令,注意如果本地使用win開發備份命令會與linux不同:
//win(未經測試,筆者在Linux上開發)
command[0]=cmd
command[1]=/c
command[2]=mysqldump -u username --password=your_password dbname > backupPath+File.separator+dbname+datetimeFormmater+".sql"
//linux(本地Manjaro+服務器CentOS測試通過)
command[0]=/bin/sh
command[1]=-c
command[2]=/usr/bin/mysqldump -u username --password=your_password dbname > backupPath+File.separator+dbname+datetimeFormmater+".sql"
再判斷備份路徑是否存在,接着利用Java自帶的Process
進行備份處理,若出錯則利用其中的getErrorStream()
獲取錯誤信息並記錄日志。
5.7 配置文件
5.7.1 配置文件分類
一個總的配置文件+三個是特定環境下(開發,測試,生產)的配置文件,可以使用spring.profiles.active
切換配置文件,比如spring.profiles.active=dev
,注意命名有規則,中間加一杠。另外自定義的配置需要在additional-spring-configuration-metadata.json
中添加字段(非強制,只是IDE會提示),比如:
"properties": [
{
"name": "backup.path",
"type": "java.lang.String",
"defaultValue": null
},
]
5.7.2 加密
都2020年了,還在配置文件中使用明文密碼就不太好吧?
該加密了。
使用的是Jasypt Spring Boot組件,官方github請戳這里。
用法這里就不詳細介紹了,詳情看筆者的另一篇博客,戳這里。
但是筆者實測目前最新的3.0.2版本(本文寫於2020.06.05,2020.05.31作者已更新3.0.3版本,但是筆者沒有測試過)會有如下問題:
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
解決方案以及問題詳細描述戳這里。
6 部署與打包
6.1 前端打包
先說一下前端的打包過程,簡單地說打成JAR即可跨平台運行,但是如果是特定平台的話比如Win,想打成無需額外JDK環境的EXE還是需要一些額外操作,這里簡單介紹一下打包過程。
(如果是JDK8可以使用mvn jfx:native
打包,這個可以很方便地直接打成DMG或者EXE,但可惜JFX11行不通,反正筆者嘗試失敗了,如果有大神知道如何使用JavaFX-Maven-Plugin
或者在IDEA中使用artifact
直接打成exe或dmg歡迎留言補充)
6.1.1 IDEA一次打包
打包需要用到Maven插件,常用的Maven打包插件如下:
- mave-jar-plugin:默認的打包jar插件,生成的JR很小,但是需要把lib放置與jar相同目錄下,用來打普通的JAR包
- maven-shade-plugin:提供了兩大基本功能,將依賴的jar包打包到當前jar包,能對依賴的JAR包進行重命名以及取舍過濾
- maven-assembly-plugin:支持定制化的打包方式,更多的是對項目目錄的重新組裝
本項目使用maven-shade-plugin打包。
需要先引入(引入之后可以把原來的Maven插件去掉),最新版本戳這里的官方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>
只需要修改主類即可:
<mainClass>xxxx.xxx.xxx.Main</mainClass>
接着就可以從IDEA右側欄的Maven中一鍵打包:
這樣在target下就有JAR包了,可以跨平台運行,只需提供JDK環境。
java -jar xxx.jar
下面的兩步是使用exe4j與Enigma Virtual Box打成一個單一EXE的方法,僅針對Win,使用Linux/Mac可以跳過或自行搜索其他方法。
6.1.2 exe4j二次打包
6.1.2.1 exe4j
exe4j能集成Java應用程序到Win下的java可執行文件生成工具,無論是用於服務器還是用於GUI或者命令行的應用程序。簡單地說,本項目用其將jar轉換為EXE。exe4j需要JRE,從JDK9開始模塊化,需要自行生成JRE,因此,需要先生成JRE再使用exe4j打包。
6.1.2.2 生成jre
各個模塊的作用可以這里查看:
經測試本程序所需要的模塊如下:
java.base,java.logging,java.net.http,javafx.base,javafx.controls,javafx.fxml,javafx.graphics,java.sql,java.management
切換到JDK目錄下,使用jlink
生成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
由於OpenJDK11不自帶JavaFX,需要戳這里自行下載Win平台的JFX jmods,並移動到JDK的jmods
目錄下。生成的JRE大小為91M:
如果實在不清楚使用哪一些模塊可以使用全部模塊,但是不建議:
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.transaction.xa,java.sql,java.sql.rowset,java.xml.crypto,java.se,java.smartcardio,jdk.accessibility,jdk.internal.vm.ci,jdk.management,jdk.unsupported,jdk.internal.vm.compiler,jdk.aot,jdk.internal.jvmstat,jdk.attach,jdk.charsets,jdk.compiler,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.crypto.mscapi,jdk.dynalink,jdk.internal.ed,jdk.editpad,jdk.hotspot.agent,jdk.httpserver,jdk.internal.le,jdk.internal.opt,jdk.internal.vm.compiler.management,jdk.jartool,jdk.javadoc,jdk.jcmd,jdk.management.agent,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,javafx.media,javafx.graphics,javafx.fxml,javafx.controls,javafx.base
--output jre
大小為238M:
6.1.2.3 exe4j打包
exe4j使用參考這里,首先一開始的界面應該是這樣的:
配置文件首次運行是沒有的,next即可。
選擇JAR in EXE mode:
填入名稱與輸出目錄:
這里的類型為GUI application,填上可執行文件的名稱,選擇圖標路徑,勾選允許單個應用實例運行:
重定向這里可以選擇標准輸出流與標准錯誤流的輸出目錄,不需要的話默認即可:
64位Win需要勾選生成64位的可執行文件:
接着是Java類與JRE路徑設置:
選擇IDEA生成的JAR,接着填上主類路徑:
設置jre的最低支持與最高支持版本:
下一步是指定JRE搜索路徑,首先把默認的三個位置刪除:
接着選擇之前生成的JRE,把JRE放在與JAR同一目錄下,路徑填上當前目錄下的JRE:
接下來全next即可,完成后會提示exe4j has finished,直接運行測試一遍:
首先會提示一遍這是用exe4j生成的:
若沒有缺少模塊應該就可以正常啟動了,有缺少模塊的話會默認在當前exe路徑生成一個error.log,查看並添加對應模塊再次使用jlink生成jre,並使用exe4j再次打包。
6.1.3 Enigma Virtual Box三次打包
使用exe4j打包后,雖然是也可以直接運行了,但是JRE太大,而且筆者這種有強迫症非得裝進一個EXE。所幸筆者之前用過Enigma Virtual Box這個打包工具,能把所有文件打包為一個獨立的EXE。
使用很簡單,首先添加exe4j打包出來的EXE:
接着新建一個jre目錄,添加上一步生成的jre:
最后選擇壓縮文件:
打包出來的單獨exe大小為65M,相比起exe4j還要帶上的89M的jre,已經節省了空間。
6.2 后端部署
后端部署的方式也簡單,采用WAR部署的方式,若項目為JAR包打包可以自行轉換為WAR包,具體轉換方式不難請自行搜索。由於Web服務器為Tomcat,因此直接把WAR包放置於webapps下即可,其他Web服務器自請自行搜索。
當然也可以使用Docker部署,但需要使用JAR而不是WAR,具體方式自行搜索。
7 運行
本項目已經打包,前端包括jar與exe,后端包括jar與war,首先把后端運行(先開啟數據庫服務):
使用jar:
java -jar Backend.jar
使用war直接放到Tomcat的webapps下然后到bin下:
./startup.sh
接着運行前端,Windows的話可以直接運行exe,當然也可以jar,Linux的話jar:
java -jar Frontend.jar
若運行失敗可以用IDEA打開項目直接在IDEA中運行或者自行打包運行。
8 注意事項
8.1 路徑問題
對於資源文件千萬千萬不要直接使用什么相對路徑或絕對路徑,比如:
String path1 = "/xxx/xxx/xxx/xx.png";
String path2 = "xxx/xx.jpg";
這樣會有很多問題,比如有可能在IDEA中直接運行與打成jar包運行的結果不一致,路徑讀取不了,另外還可能會出現平台問題,眾所周知Linux的路徑分隔符與Windows的不一致。所以,對於資源文件,統一使用如下方式獲取:
String path = getClass().getResource("/image/xx.png");
其中image
直接位於resources
資源文件夾下。其他類似,也就是說這里的/
代表在resources
下。
8.2 HTTPS
默認沒有提供HTTPS,證書文件沒有擺上去,走的是本地8080端口。
如果需要自定義HTTPS請修改前端部分的
com.test.network.OKHTTP
resources/key/pem.pem
同時后端需要修改Tomcat的server.xml
。
有關OkHttp使用HTTPS的文章有不少,但是大部分都是僅僅寫了前端如何配置HTTPS的,沒有提到后端如何部署,可以參考筆者的這篇文章,包含Tomcat的配置教程。
8.3 配置文件加密
配置文件使用了jasypt-spring-boot開源組件進行加密,設置口令可以有三種方式設置:
- 命令行參數
- 應用環境變量
- 系統環境變量
目前最新的版本為3.0.3(2020.05.31更新3.0.3 ,筆者之前使用3.0.2的版本進行加密時本地測試沒問題,但是部署到服務器上老是提示找不到口令,無奈只好使用舊一點的2.x版本,但是新版本出了后筆者嘗試過部署到本地Tomcat沒有問題但是沒有部署到服務器上),建議使用最新版本進行部署:
畢竟前后跨度挺大的,雖然說這是小的bug修復,但是還是建議試試,估計不會有3.0.2的問題了。
另外對於含有中文的字段記得進行編碼轉換:
str = new String(str.getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);)
另外筆者已寫好了測試文件,直接首先替換掉配置文件原來的密文,填上明文重新加密:
注意如果沒有在配置文件中設置jasypt.encryptor.password
的話可以在運行配置中設置VM Options(建議不要把口令直接寫在配置文件中,當然這個默認是使用PBE加密,非對稱加密可以使用jasypt.encryptor.private-key-string
或jasypt.encryptor.private-key-location
):
8.4 鍵盤事件
添加鍵盤事件可以使用如下代碼:
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.ENTER), ()->{xxx});
//getAccelerators返回ObservableMap<KeyCombination, Runnable>
響應之前需要讓parent獲取焦點:
parent.requestFocus();
8.5 數據庫
默認使用的數據庫名為app_test
,用戶名test_user
,密碼test_password
,resources
下有一個init.sql
,直接使用MySQL導入即可。
8.6 驗證碼
默認沒有自帶驗證碼功能,由於涉及隱私問題故沒有開放。
如果像筆者一樣使用騰訊雲的短信API,直接修改配置文件中的對應屬性即可,建議加密。
如果使用其他API請自行對接,前端需要修改的部分包括:
com.test.network.OKHTTP
com.test.network.request.SendSmsRequest
com.test.network.requestBuilder.SendSmsRequestBuilder
com.test.controller.start.RetrievePasswordController
后端需要修改的部分:
com.test.controller.SmsController
需要的話可以參考筆者的騰訊雲短信API使用或者自行搜索其他短信驗證API。一些寫在配置文件中的API需要的密鑰等信息強烈
9 源碼
前后端完整代碼以及打包程序:
10 項目不足之處
其實整個項目還有很多的不足之處,比如:
- 前端的部分Scene切換有問題
- 可以使用Jackson代替Gson來換取更快的轉換速度
- 沒有緩存機制
- 前端日志不能發送到后端分析
- 可以使用二進制代替JSON實現更快的傳輸
不過目前暫時不考慮更新,如果有讀者有自己的想法可以按需修改,這里提一下修改的思路。
11 參考
1、CSDN-maven-shade-plugin介紹及使用
2、CSDN-Maven3種打包方式之一maven-assembly-plugin的使用
4、CSDN-使用exe4j將java文件打成exe文件運行詳細教程
5、Github-jasypt-spring-boot issue
7、簡書-Linux Tomcat+Openssl單向/雙向認證
如果覺得文章好看,歡迎點贊。
同時歡迎關注微信公眾號:氷泠之路。