一個簡單的小型薪酬管理系統,前端JavaFX+後端Spring Boot,功能倒沒多少,主要精力放在了UI和前端的一些邏輯上面,後端其實作得很簡單。css
主要功能:html
登陸界面:前端
用戶界面:java
管理員界面:node
前端主要分爲5個部分實現:控制器模塊,視圖模塊,網絡模塊,動畫模塊還有工具類模塊。mysql
css
:界面所用到的樣式fxml
:一個特殊的xml文件,用於定義界面與綁定Controller中的函數,也就是綁定事件image
:程序用到的默認圖片key
:證書文件,用於OkHttp中的HTTPSproperties
:項目一些常量屬性主要依賴以下:linux
程序所須要的常量:git
CSSPath
:CSS路徑,用於scene.getStylesheets.add(path)
FXMLPath
:FXML路徑,用於FXMLLoader.load(getClass.getResource(path).openStream())
AllURL
:發送網絡請求的路徑BuilderKeys
:OkHttp中的FormBody.Builder
中使用的常量鍵名PaneName
:Pane名字,用於在同一個Scene切換不一樣的PaneReturnCode
:後端返回碼ViewSize
:界面尺寸重點說一下路徑問題,筆者的css與fxml文件都放在resources下:github
其中fxml路徑在項目中的用法以下:web
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"; //... }
控制器模塊用於處理用戶的交互事件,分爲三類:
這是程序一開始進入的界面,會在這裏綁定一些基本的關閉,最小化,標題欄拖拽事件:
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(); }
登陸界面的控制器也很簡單,就一個登陸/註冊功能加一個跳轉到找回密碼界面,代碼就不貼了。
至於找回密碼界面,須要作的比較多,首先須要判斷用戶輸入的電話是否在後端數據庫存在,另外還有檢查兩次輸入的密碼是否一致,還有判斷短信是否發送成功與用戶輸入的驗證碼與後端返回的驗證碼是否一致(短信驗證碼部分其實不須要後端處理,本來是放在前端的,可是考慮到可能會泄漏一些重要的信息就放到後端處理了)。
接着是用戶登陸後進入的界面,加了漸隱與移動動畫:
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 動畫模塊"。
簡單的一個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,鍵爲對應的年月,值爲工資。具體轉換方法請到工具類模塊查看。
日誌模塊使用了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
:換行這裏前端的日誌進行了簡化處理,須要更多配置請自行搜索。
網絡模塊的核心使用了OkHttp實現,主要分爲兩個包:
request
:封裝發送到後端的各類請求requestBuilder
:建立request的Builder類OKHTTP
:封裝OkHttp的工具類,對外只有一個send方法,參數只有一個,request包中的類,使用requestBuilder生成,返回一個Object,至於Object怎麼處理須要在用到OKHTTP的地方與返回方法對應封裝了各類網絡請求:
全部請求繼承自BaseRequest,BaseRequest的公有方法包括:
setUrl
:設置發送的urlsetCellphone
:添加cellphone參數setPassword
:添加password參數,注意password通過前端的SHA512加密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包含了對應與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。
這是一個封裝了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請求的方式,其中call中使用的url與body正是使用BaseRequest
做爲基類的緣由,能夠方便地獲取url與body,若數據量大能夠考慮異步請求。上面也提到後端返回的是經由Gson轉換爲String的ReturnCode,因此獲取body後,先轉換爲ReturnCode再處理。
至於HTTPS,採用了war包部署,後端服務器Tomcat,須要在Tomcat裏設置證書,同時也須要在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]; }
返回一個信任由輸入流讀取的證書的信任管理器,若證書沒有被簽名則拋出SSLHandsakeException,證書建議使用第三方簽名的而不是自簽名的(好比使用openssl生成),特別是在生產環境中,例子的註釋也提到:
最後是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下的證書文件。
使用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.xml便可,使用HTTP/2的話會麻煩一點,若是基於APR實現須要安裝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版本:
<Connector ... sslProtocol="TLS" sslEnabledProtocols="TLSv1.2" >
圖片本來是想使用OkHttp的MultipartBody處理的,可是處理的圖片都不太,貌似沒有必要,並且實體類的數據都是以字符串的形式傳輸的,所以,筆者的想法是能不能統一都用字符串進行傳輸,因而找到了圖片和String互轉的函數,稍微改動,原來的函數須要外部依賴,如今改成了JDK自帶的Base64:
public static String fileToString(String path) { File file = new File(path); FileInputStream fis = null; StringBuilder content = new StringBuilder(); try { fis = new FileInputStream(file); int length = 3 * 1024 * 1024; byte[] byteAttr = new byte[length]; int byteLength; while ((byteLength = fis.read(byteAttr, 0, byteAttr.length)) != -1) { String encode; if (byteLength != byteAttr.length) { byte[] temp = new byte[byteLength]; System.arraycopy(byteAttr, 0, temp, 0, byteLength); encode = Base64.getEncoder().encodeToString(temp); content.append(encode); } else { encode = Base64.getEncoder().encodeToString(byteAttr); content.append(encode); } } } catch (IOException e) { e.printStackTrace(); } finally { try { assert fis != null; fis.close(); } catch (IOException e) { e.printStackTrace(); } } return content.toString(); } public static void stirngToFile(String base64Code, String targetPath) { byte[] buffer; FileOutputStream out = null; try { buffer = Base64.getDecoder().decode(base64Code); out = new FileOutputStream(targetPath); out.write(buffer); } catch (IOException e) { e.printStackTrace(); } finally { if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } }
Base64是一種基於64個可打印字符來表示二進制數據的方法,能夠把二進制數據(圖片/視頻等)轉爲字符,或把對應的字符解碼變爲原來的二進制數據。
筆者實測這種方法轉換速度不慢,只要有了正確的轉換函數,服務器端能夠輕鬆進行轉換,可是對於大文件的支持很差:
這種方法對通常的圖片來講足夠了,可是對於真正的文件仍是建議使用MultipartBody進行處理。
包含了四類動畫:淡入/淡出,位移,縮放,旋轉,這四個類都實現了CustomTransitionOperation
接口:
import javafx.animation.Animation; public interface CustomTransitionOperation { double defaultSeconds = 0.4; Animation build(); void play(); }
其中defaultSeconds表示默認持續的秒數,build用於Transition
中對各個動畫類進行統一的生成操做,最後的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();
AvatarUtils
:用於本地生成臨時圖片以及圖片轉換處理Check
:檢查是否爲空,是否合法等Conversion
:轉換類,經過Gson在Worker/String,Map/String,List/String之間進行轉換Utils
:加密,設置運行環境,居中Stage,檢查網絡連通等這裏說一下Utils
與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類進行轉換操做。
獲取屬性文件方法以下:
//獲取屬性文件 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; }
採用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); }
GUI
:全局變量共享以及以及控制Scene的切換MainScene
:全局控制器,負責初始化以及綁定鍵盤事件MessBox
:提示信息框,對外提供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(); } });
界面基本上靠這些fxml文件控制,這部分沒太多內容,基本上靠IDEA自帶的Scene Builder設計,少部分靠代碼控制,下面說幾個注意事項:
fx:id
以便切換onMouseEntered="#xxx"
,其中裏面的方法爲對應的控制器(fx:controller="xxx.xxx.xxx.xxxController"
)中的方法<Image>
中的url屬性須要帶上@
,好比<Image url="@../../image/xxx.png">
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);
下面以提示框爲例,說明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獲取焦點。
後端以Spring Boot框架爲核心,部署方式爲war,總體分爲三層:
總的來講沒有用到什麼高大上的東西,邏輯也比較簡單。
主要依賴以下:
控制器分爲三類,一類處理圖片,一類處理CRUD請求,一類處理短信發送請求,統一接受POST忽略GET請求。大概的處理流程是接收參數後首先進行判斷操做,好比判空以及判斷是否合法等等,接着調用業務層的方法並對返回結果進行封裝,同時進行日誌記錄,最後利用Gson把返回結果轉爲字符串。代碼大部分比較簡單就不貼了,說一下短信驗證碼的部分。
驗證碼模塊使用了騰訊雲的功能,官網這裏,搜索短信功能便可。
新用戶默認贈送100條短信:
發送以前須要建立簽名與正文模板,審覈經過便可使用。
能夠先根據快速開始試用一下短信功能,若能成功收到短信,能夠戳這裏查看API(Java版)。
下面的例子由文檔例子簡化而來:
private void sendCode() { try { SmsClient client = new SmsClient(new Credential(TencentSDK.id,TencentSDK.key),""); SendSmsRequest request = new SendSmsRequest(); request.setSmsSdkAppid(TencentSDK.appId); request.setSign(TencentSDK.sign); request.setTemplateID(TencentSDK.templateId); randomCode = RandomStringUtils.randomNumeric(6); String [] templateParamSet = {randomCode}; request.setTemplateParamSet(templateParamSet); String [] phoneNumbers = {"+86"+cellphone.getText()}; request.setPhoneNumberSet(phoneNumbers); response = client.SendSms(request); } catch (Exception e) { L.error("Not send code or send code failed"); AlertView.show("驗證碼未發送或發送驗證碼失敗"); } }
其中TencentSDK.appId,TencentSDK.sign,TencentSDK.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); }
因爲程序中的業務層與持久層都比較簡單就合併一塊兒說了,好比業務層的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; }
另外因爲CurdRepository<T,ID>
的saveAll方法參數爲Iterable<S>
,所以能夠直接保存List<S>
,好比:
public ReturnCode saveAll(List<Worker> workers) { workerRepository.saveAll(workers); return ReturnCode.SAVE_ALL_SUCCESS; }
須要在控制層中把前端發送的String轉換爲List<S>
。
日誌用的是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; }
四個:
重點說一下備份,代碼不長就直接整個類貼出來了:
@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用於提供定時任務的註解,用於控制任務在某個指定時間執行或者每隔一段時間執行(這裏是半天一次),主要有三種配置執行時間的方式:
這裏不展開了,詳細用法能夠戳這裏。
另外在使用前須要在類上加上@EnableScheduling
。備份的方法首先利用url獲取數據庫名,接着拼合備份命令,注意若是本地使用win開發備份命令會與linux不一樣:
//win 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()
獲取錯誤信息並記錄日誌。
一個總的配置文件+三個是特定環境下(開發,測試,生產)的配置文件,可使用spring.profiles.active
切換配置文件,好比spring.profiles.active=dev
,注意命名有規則,中間加一槓。另外自定義的配置須要在additional-spring-configuration-metadata.json
中添加字段(非強制,只是IDE會提示),好比:
"properties": [ { "name": "backup.path", "type": "java.lang.String", "defaultValue": "null" }, ]
都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
解決方案以及問題詳細描述戳這裏。
先說一下前端的打包過程,簡單地說打成jar便可跨平臺運行,可是若是是特定平臺的話好比win,想打成無需額外JDK環境的exe仍是須要一些額外操做,這裏簡單介紹一下打包過程。
(若是是JDK8可使用mvn jfx:native
打包,這個能夠很方便地直接打成dmg或者exe,但惋惜JFX11行不通,反正筆者嘗試失敗了,若是有大神知道如何使用JavaFX-Maven-Plugin
或者在IDEA中使用artifact
直接打成exe或dmg歡迎留言補充)
打包須要用到Maven插件,經常使用的Maven打包插件以下:
本項目使用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能夠跳過或自行搜索其餘方法。
exe4j能集成Java應用程序到Win下的java可執行文件生成工具,不管是用於服務器仍是用於GUI或者命令行的應用程序。簡單地說,本項目用其將jar轉換爲exe。exe4j須要jre,從JDK9開始模塊化,須要自行生成jre,所以,須要先生成jre再使用exe4j打包。
各個模塊的做用能夠這裏查看:
經測試本程序所須要的模塊以下:
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:
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再次打包。
使用exe4j打包後,雖然是也能夠直接運行了,可是jre太大,並且筆者這種有強迫症非得裝進一個exe。所幸筆者以前用過Enigma Virtual Box這個打包工具,能把全部文件打包爲一個獨立的exe。
使用很簡單,首先添加exe4j打包出來的exe:
接着新建一個jre目錄,添加上一步生成的jre:
最後選擇壓縮文件:
打包出來的單獨exe大小爲65M,相比起exe4j還要帶上的89M的jre,已經節省了空間。
後端部署的方式也簡單,採用war部署的方式,若項目爲jar包打包能夠自行轉換爲war包,具體轉換方式不難請自行搜索。因爲Web服務器爲Tomcat,所以直接把war包放置於webapps下便可,其餘Web服務器自請自行搜索。
固然也可使用Docker部署,但須要使用jar而不是war,具體方式自行搜索。
本項目已經打包,前端包括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中運行或者自行打包運行。
對於資源文件千萬千萬不要直接使用什麼相對路徑或絕對路徑,好比:
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
下。
默認沒有提供HTTPS,證書文件沒有擺上去,走的是本地8080端口。
若是須要自定義HTTPS請修改前端部分的
com.test.network.OKHTTP
resources/key/pem.pem
同時後端須要修改Tomcat的server.xml
。
有關OkHttp使用HTTPS的文章有很多,可是大部分都是僅僅寫了前端如何配置HTTPS的,沒有提到後端如何部署,能夠參考筆者的這篇文章,包含Tomcat的配置教程。
配置文件使用了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
):
添加鍵盤事件可使用以下代碼:
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.ENTER), ()->{xxx}); //getAccelerators返回ObservableMap<KeyCombination, Runnable>
響應以前須要讓parent獲取焦點:
parent.requestFocus();
默認使用的數據庫名爲app_test
,用戶名test_user
,密碼test_password
,resources
下有一個init.sql
,直接使用MySQL導入便可。
默認沒有自帶驗證碼功能,因爲涉及隱私問題故沒有開放。
若是像筆者同樣使用騰訊雲的短信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須要的密鑰等信息強烈
先後端完整代碼以及打包程序:
一、CSDN-maven-shade-plugin介紹及使用
二、CSDN-Maven3種打包方式之一maven-assembly-plugin的使用
四、CSDN-使用exe4j將java文件打成exe文件運行詳細教程
五、Github-jasypt-spring-boot issue
七、簡書-Linux Tomcat+Openssl單向/雙向認證
若是以爲文章好看,歡迎點贊。
同時歡迎關注微信公衆號:氷泠之路。