頁面國際化
國際化,英文名叫internationalization,因爲中間有18個字母,又叫i18n。
我們平時工作或者開發一些網站時,尤其是國際網站,國際化是必須要做的事情。
準備工作
首先需要保證配置文件的編碼格式是UTF-8,否則可能出現頁面亂碼的情況。選擇File->Settings
,搜索File Encodings
進行設置。
測試使用
1、建立不同語言的配置文件。在resources目錄下建立目錄名稱爲i18n,添加xxx.properties
國際化配置文件。使用xxx_語言代號_國家代號.properties
的命名格式來添加不同語言的國際化配置文件。當我們添加了指定語言的國際化配置文件後,可以發現IDEA幫我們自動識別了,並幫我們生成了Resource Bundle 'xxx'
目錄。
我們再來編寫一個英文的國際化配置文件。在國際化配置目錄上右鍵選擇New->Add Properties Files to Resource Bundle
可以快速創建國際化配置文件。
點進其中一個國際化配置文件,點擊下方的Resource Bundle
,切換窗口之後點擊上方的+
號可以快速配置屬性。
最終配置完成的效果如下圖所示。
如果是一整個完整的頁面或者文檔頁的文章量十分大的時候,沒有必要做這些細節化的國際化操作。
Thymeleaf配置國際化
在前臺頁面配置這些國際化屬性需要用到的表達式是Message Expressions,就是用#{...}
這種格式來獲取後臺配置的國際化屬性。
讓項目識別國際化配置
首先需要配置綁定國際化的映射路徑:
#配置綁定國際化的login路徑
spring.messages.basename=i18n.login
配置完映射路徑後,在前端使用Message Expressions去獲取國際化配置屬性時會有提示。
把前端內容填寫完畢後,重啓項目,訪問localhost:8080/,跳到登錄頁面如下圖所示。登錄頁的文字會跟隨着瀏覽器的語言環境而變化。
原理:谷歌瀏覽器中右鍵檢查->NetWork
,查看localhost請求我們會發現,在Request Headers
中Accept-Language
參數的值爲zh_CN
,說明SpringBoot框架會根據瀏覽器請求中的語言來使用對應語言的國際化配置。
只不過,以上效果並不是最理想的,我們想要的理想效果是可以通過點擊按鈕或者鏈接實現語言的動態切換。
通過點擊按鈕實現語言動態切換
我們可以參考Dubbo官網,點擊網站右上方的按鈕可以切換中英文,仔細觀察會發現中文和英文的請求地址是不一樣的:
中文:http://dubbo.apache.org/zh-cn/
英文:http://dubbo.apache.org/en-us/
也就是說,它是通過不同鏈接來實現國際化映射的。
在Spring中,國際化對象是Locale。在SpringBoot中,框架幫我們自動注入了國際化的組件。相關源碼如下:
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() { // 國際化對象解析LocaleResolver
// 如果用戶配置了 就使用用戶配置的,如果沒有用戶配置就使用默認的!
if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
// 接收請求頭信息關於國際化的
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}
在我們的接收請求解析中發現了一個重寫的方法,源碼如下:
@Override
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = getDefaultLocale();
// 從 request 中獲得 Accept-Language 語言信息
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
return defaultLocale;
}
// 得到請求中國際化的信息
Locale requestLocale = request.getLocale();
List<Locale> supportedLocales = getSupportedLocales();
if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
return requestLocale;
}
Locale supportedLocale = findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
}
return (defaultLocale != null ? defaultLocale : requestLocale);
}
通過分析以上源碼,我們就可以知道怎麼去自定義一個國際化請求解析器了。
分析完源碼之後,我們就可以開始實現我們想要的功能了:
1、修改前端頁面鏈接,訪問後臺請求並傳遞一個參數l
,該參數表示當前需要切換的是哪種語言。
<!--
localhost:8080/index?l=zh_CN
thymeleaf中傳遞參數不使用? 而是使用()
-->
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
2、處理上述請求,我們可以去自定義一個國際化處理器來解析請求,獲得傳遞參數並構造國際化對象。
//自定義國際化處理器
public class MyLocaleResolver implements LocaleResolver {
//實際上還是Java Web的基礎操作
@Override
public Locale resolveLocale(HttpServletRequest request) {
//獲取請求中的參數
String language = request.getParameter("l");
Locale locale=Locale.getDefault();//如果沒有配置就使用默認的
if(!StringUtils.isEmpty(language)){
//分割參數,第一個參數表示語言,第二個參數表示國家或地區
String[] split=language.split("_");
//分析國家和地區
locale=new Locale(split[0],split[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
}
3、註冊到Ioc容器中。
//註冊我們的國際化組件
//方法名(id)只能爲localeResolver,因爲SpringBoot會去掃描並識別
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}
4、重啓項目,點擊測試即可。
登錄實現
1、修改登錄表單。
<form class="form-signin" th:action="@{/user/login}" method="post">
...
</form>
2、編寫處理請求。
//登錄請求
@PostMapping("/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Model model, HttpSession session){
//從數據庫中查找用戶
SysUser user=sysUserMapper.getByUsernameAndPassword(username,password);
if(user!=null){
//登錄成功,放入session
session.setAttribute("loginUser",username);
//轉發和重定向
return "redirect:/main.html";
}
model.addAttribute("msg","用戶名或密碼錯誤!");
return "login";//返回登錄頁
}
3、啓動項目並測試。
攔截器實現
在實際網站中,用戶在沒有登錄的情況下是不允許進入主頁的,這就需要使用攔截器來對請求進行攔截了。
1、編寫自定義攔截器實現HandlerInterceptor接口。我們這裏只在請求處理前進行攔截,因此只需要重寫preHandle方法即可。
public class LoginInterceptor implements HandlerInterceptor {
//false 攔截
//true 放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String user= (String) request.getSession().getAttribute("loginUser");
if(user==null){//未登錄,不放行,並且跳轉到登錄頁
request.setAttribute("msg","未登錄,請先登錄!");
request.getRequestDispatcher("/index.html").forward(request,response);//轉發
return false;
}else{
return true;
}
}
}
2、註冊攔截器。只需要在自定義Mvc配置類裏面重寫addInterceptors方法即可。
//註冊攔截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//攔截器需要放行第一次登錄的請求,否則就永遠無法登錄成功
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/index.html","/","index","/asserts/**","/user/login");
}
3、啓動項目進行測試。
在前端頁面可以使用th:text=${session.xxx}
或者[[${session.xxx}]]
來獲取session的屬性。
員工列表實現
員工列表頁面跳轉
1、前端提交請求。
<li class="nav-item">
<a class="nav-link" th:href="@{/emps}">員工管理</a>
</li>
2、後端處理請求。
//跳轉到員工管理頁面
@GetMapping("/emps")
public String list(Model model){
List<Employee> employees=employeeMapper.getEmployees();
model.addAttribute("emps",employees);
return "emp/list";
}
抽取頁面公共部分
跳轉之後,頁面存在公共部分,我們應該抽取成模板。我們可以把公共部分抽取到一個文件中,放在common目錄下。
模板代碼如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--存放頁面模板-->
<!--頭部-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
<!--直接獲取thymeleaf的值,註釋中不能寫兩個中括號-->
<a class="navbar-brand col-sm-3 col-md-2 mr-0" style="color:white" >[[${session.loginUser}]]</a>
<input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
<ul class="navbar-nav px-3">
<li class="nav-item text-nowrap">
<a class="nav-link" th:href="@{/user/logout}">註銷</a>
</li>
</ul>
</nav>
<!--側邊欄-->
<nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">
<div class="sidebar-sticky">
<!--通過不同的參數實現不同的class樣式-->
<ul class="nav flex-column">
<li class="nav-item">
<a th:class="${activeUrl=='main.html'?'nav-link active':'nav-link'}" th:href="@{/main.html}">首頁</a>
</li>
<li class="nav-item">
<!--如果需要知道當前是員工管理頁面,通過傳遞一個activeUrl參數來判斷-->
<a th:class="${activeUrl=='emps'?'nav-link active':'nav-link'}" th:href="@{/emps}">員工管理</a>
</li>
</ul>
</div>
</nav>
在其它頁面中複用該模板:
<!-- 頭部-->
<div th:replace="~{common/bar::topbar}"></div>
<!--側邊欄-->
<div th:replace="~{common/bar::sidebar(activeUrl='emps')}"></div>
員工列表展示
1、修改前端頁面樣式。
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<h2>
<!--添加員工-->
<a type="button" class="btn btn-success" th:href="@{/emp}"> 添加員工</a>
</h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>id</th>
<th>lastName</th>
<th>email</th>
<th>gender</th>
<th>department</th>
<th>birth</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<!--遍歷後臺傳遞的數據-->
<tr th:each="emp:${emps}">
<td th:text="${emp.getId()}"></td>
<td>[[${emp.getLastName()}]]</td>
<td th:text="${emp.getEmail()}"></td>
<td th:text="${emp.getGender()==0?'女':'男'}"></td>
<td th:text="${emp.getEDepartment().getDepartmentName()}"></td>
<td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td>
<td>
<!--編輯或刪除某個用戶,IDEA不識別,不是錯誤-->
<a th:href="@{/emp/}+${emp.id}" type="button" class="btn btn-sm btn-primary">編輯</a>
<a th:href="@{/delEmp/}+${emp.id}" type="button" class="btn btn-sm btn-danger">刪除</a>
</td>
</tr>
</tbody>
</table>
</div>
</main>
最終效果如下圖所示。
添加員工實現
1、新建一個添加員工頁面。
<!--主要內容區域-->
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<!--修改的表單-->
<!--有時候表單需要設置一些隱藏域-->
<form th:action="@{/emp}" method="post">
<div class="form-group">
<label>LastName</label>
<input type="text" class="form-control" name="lastName" placeholder="wunian">
</div>
<div class="form-group">
<label>Email</label>
<input type="text" class="form-control" name="email" placeholder="[email protected]">
</div>
<div class="form-group form-inline">
<label>Gender </label>
<div class="form-check form-check-inline">
<input type="radio" name="gender" value="1">
<label>男</label>
</div>
<div class="form-check form-check-inline">
<input type="radio" name="gender" value="0">
<label>女</label>
</div>
</div>
<div class="form-group">
<label>Department</label>
<select class="form-control" name="department">
<option th:each="department:${departments}"
th:text="${department.getDepartmentName()}"
th:value="${department.getId()}"></option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input type="text" class="form-control" name="birth" placeholder="2020-03-21">
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
</main>
2、後端處理請求。
//添加員工
@PostMapping("/emp")
public String addEmp(Employee employee){
//保存員工信息
employeeMapper.save(employee);
return "redirect:/emps";
}
這裏要特別注意提交時間的格式問題。我們需要在配置文件中配置前端頁面的日期格式。
#前端提交的日期格式問題(和前端的日期格式對應即可)
spring.mvc.date-format=yyyy-MM-dd
修改員工實現
1、新建一個修改頁面。
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<!--修改的表單-->
<!--有時候表單需要設置一些隱藏域-->
<form th:action="@{/updateEmp}" method="post">
<!--id應設置爲隱藏域-->
<input type="hidden" class="form-control" name="id" th:value="${employee.getId()}">
<div class="form-group">
<label>LastName</label>
<input type="text" class="form-control" name="lastName" th:value="${employee.getLastName()}">
</div>
<div class="form-group">
<label>Email</label>
<input type="text" class="form-control" name="email" th:value="${employee.getEmail()}">
</div>
<div class="form-group form-inline">
<label>Gender </label>
<div class="form-check form-check-inline">
<input type="radio" name="gender" value="1"
th:checked="${employee.getGender()==1}">
<label>男</label>
</div>
<div class="form-check form-check-inline">
<input type="radio" name="gender" value="0"
th:checked="${employee.getGender()==0}">
<label>女</label>
</div>
</div>
<div class="form-group">
<label>Department</label>
<!--應該從數據庫中讀取部門信息-->
<select class="form-control" name="department">
<!--需要選中用戶所在的部門-->
<option th:selected="${employee.getDepartment()==dept.getId()}"
th:each="dept:${departments}"
th:text="${dept.getDepartmentName()}"
th:value="${dept.getId()}"></option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input type="text" class="form-control" name="birth" th:value="${#dates.format(employee.getBirth(),'yyyy-MM-dd')}">
</div>
<button type="submit" class="btn btn-primary">修改</button>
</form>
</main>
2、跳轉到修改頁面(注意頁面回顯和設置id的隱藏域),後端處理跳轉請求。
//跳轉到員工修改頁面
@GetMapping("/emp/{id}")
public String toUpdateEmp(@PathVariable("id")Integer id,Model model){
Employee employee = employeeMapper.get(id);
model.addAttribute("employee",employee);
//查詢部門數據
List<Department> departments = departmentMapper.getDepartments();
model.addAttribute("departments",departments);
return "emp/update";
}
3、後端處理修改請求。
//修改員工信息
@PostMapping("/updateEmp")
public String updateEmp(Employee employee){
employeeMapper.update(employee);
return "redirect:/emps";
}
4、啓動項目並測試。
刪除員工和註銷實現
1、前端提交刪除請求。
<a th:href="@{/delEmp/}+${emp.id}" type="button" class="btn btn-sm btn-danger">刪除</a>
2、後端處理刪除請求。
//刪除員工信息
@GetMapping("/delEmp/{id}")
public String delEmp(@PathVariable("id")Integer id){
employeeMapper.delete(id);
return "redirect:/emps";
}
3、前端提交註銷請求。
<li class="nav-item text-nowrap">
<a class="nav-link" th:href="@{/user/logout}">註銷</a>
</li>
4、後端處理註銷請求。
//註銷請求
@GetMapping("/user/logout")
public String logout(HttpSession session){
session.invalidate();
return "login";
}
5、啓動項目並測試。
基本的錯誤處理方式
當我們在網站上訪問了一個錯誤的請求頁面之後,一般會跳到一個錯誤處理頁面。在SpringBoot中,我們只需要在templates目錄下建立一個error目錄,在裏面放入對應的錯誤頁面即可,錯誤頁面的名稱爲對應的錯誤狀態碼,比如4xx.html
或者5xx.html`等。
上線網站的步驟
1、將項目打包到服務器上運行。打包命令:mvn package
2、配置服務器數據庫。
3、運行項目。直接運行jar包,命令:java -jar xxx.jar
或nohup java -jar xxx.jar &
(後臺運行)。
4、開啓Linux防火牆。
5、如果使用的是阿里雲服務器,還需要在阿里雲上配置安全組規則,開放相應的端口。
錯誤處理機制
錯誤處理的自動配置類是ErrorMvcAutoConfiguration,該類註冊瞭如下幾個bean:
- DefaultErrorAttributes
- BasicErrorController
- ErrorPageCustomizer
- DefaultErrorViewResolver
錯誤處理的大致步驟:
1、如果系統出現瞭如404、500錯誤,錯誤將被ErrorPageCustomizer進行處理,並跳轉到/error請求進行處理。
2、默認的發生錯誤,它會將請求轉發到BasicErrorController控制器來處理請求。若是網頁客戶端請求就會返回頁面,如果是api調用就會返回JSON數據。源碼如下:
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)// 如果是網頁請求就返回視圖
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
// 錯誤的頁面會被誰處理呢?
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping // 其與的請求都會返回錯誤的狀態
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
}