предисловие
Компания будет проектировать от Struts2 до SpringMVC, потому что бизнес компании является зарубежным сервисом, так высокий на международные функциональные требования. Struts2 поставляется с интернационализацией SpringMVC относительно более совершенным более совершенным, но весна может быть отличной особенностью - это настраиваемый набор сильного сопротивления, поэтому, когда компания перенесла проектом SpringMVC, увеличила свои международные возможности. Настоящим документирует и немного улучшаю.
Основные функции реализованные в данной статье:
- Загружайте несколько файлов интернационализации прямо из папки
- Установите файл, который отображает информацию об интернационализации на странице внешнего интерфейса в фоновом режиме.
- Используйте перехватчики и аннотации, чтобы автоматически настроить главную страницу для отображения файлов международной информации.
Примечание. В этой статье не описывается, как настроить интернационализацию, региональный парсер и т. д.
выполнить
Инициализация проекта интернационализации
Сначала создайте базовый проект информации об интернационализации Spring-Boot+thymeleaf+ (message.properties), при необходимости вы можете скачать его с моегоGithubскачать.
Краткий обзор каталогов и файлов проекта
где i18naplication.java устанавливаетCookieLocaleResolver
, использует файл cookie для управления языком интернационализации. также установитьLocaleChangeInterceptor
Перехватчик для перехвата интернационализированных языковых изменений.
@SpringBootApplication
@Configuration
public class I18nApplication {
public static void main(String[] args) {
SpringApplication.run(I18nApplication.class, args);
}
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver slr = new CookieLocaleResolver();
slr.setCookieMaxAge(3600);
slr.setCookieName("Language");//设置存储的Cookie的name为Language
return slr;
}
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
//拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor()).addPathPatterns("/**");
}
};
}
}
Давайте еще раз посмотрим, что написано в hello.html:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:text="#{i18n_page}"></h1>
<h3 th:text="#{hello}"></h3>
</body>
</html>
Теперь запустите проект и посетитеhttp://localhost:9090/hello
(Я установил порт 9090 в application.properties).
Поскольку языком браузера по умолчанию является китайский, он перейдет к messages_zh_CN.properties, чтобы найти его по умолчанию, а если нет, он перейдет к messages.properties, чтобы найти интернационализированные слова.
Затем набираем в браузереhttp://localhost:9090/hello?locale=en_US
, язык переключится на английский. Точно так же, если параметр после URL-адреса установлен наlocale=zh_CH
, язык переключится на китайский.
Загружайте несколько файлов интернационализации прямо из папки
На нашей странице hello.html есть только две информации об интернационализации: «i18n_page» и «hello». Однако в реальных проектах информации об интернационализации определенно не так мало, как несколько, обычно тысячи или сотни. Мы, конечно, не можем помещать так много интернационализированной информации вmessages.properties
В одном файле интернационализированная информация обычно классифицируется по нескольким файлам. Но когда проект станет больше, интернационализированных файлов будет все больше и больше.application.properties
Также недоброятно настроить этот файл один за другим в файл, поэтому теперь мы реализуем функцию для автоматической загрузки всех интернационализационных файлов в указанный каталог.
Наследовать ResourceBundleMessageSource
Создать наследование классов под проектResourceBundleMessageSource
илиReloadableResourceBundleMessageSource
, по имениMessageResourceExtension
. И вводится в бин с именемmessageSource
, здесь мы наследуем ResourceBundleMessageSource.
@Component("messageSource")
public class MessageResourceExtension extends ResourceBundleMessageSource {
}
Обратите внимание, что имя нашего Компонента должно быть «messageSource», потому что во время инициализацииApplicationContext
, будет искать bean-компонент с именем «messageSource». Этот процессAbstractApplicationContext.java
, давайте взглянем на исходный код
/**
* Initialize the MessageSource.
* Use parent's if none defined in this context.
*/
protected void initMessageSource() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {
this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
...
}
}
...
В этом методе инициализации MessageSource beanFactory ищет инъекцию с именемMESSAGE_SOURCE_BEAN_NAME(messageSource)
Если компонент не найден, он будет искать компонент с таким именем в своем родительском классе.
Реализовать загрузку файлов
Теперь мы можем приступить к созданиюMessageResourceExtension
Напишите метод для загрузки файла.
@Component("messageSource")
public class MessageResourceExtension extends ResourceBundleMessageSource {
private final static Logger logger = LoggerFactory.getLogger(MessageResourceExtension.class);
/**
* 指定的国际化文件目录
*/
@Value(value = "${spring.messages.baseFolder:i18n}")
private String baseFolder;
/**
* 父MessageSource指定的国际化文件
*/
@Value(value = "${spring.messages.basename:message}")
private String basename;
@PostConstruct
public void init() {
logger.info("init MessageResourceExtension...");
if (!StringUtils.isEmpty(baseFolder)) {
try {
this.setBasenames(getAllBaseNames(baseFolder));
} catch (IOException e) {
logger.error(e.getMessage());
}
}
//设置父MessageSource
ResourceBundleMessageSource parent = new ResourceBundleMessageSource();
parent.setBasename(basename);
this.setParentMessageSource(parent);
}
/**
* 获取文件夹下所有的国际化文件名
*
* @param folderName 文件名
* @return
* @throws IOException
*/
private String[] getAllBaseNames(String folderName) throws IOException {
Resource resource = new ClassPathResource(folderName);
File file = resource.getFile();
List<String> baseNames = new ArrayList<>();
if (file.exists() && file.isDirectory()) {
this.getAllFile(baseNames, file, "");
} else {
logger.error("指定的baseFile不存在或者不是文件夹");
}
return baseNames.toArray(new String[baseNames.size()]);
}
/**
* 遍历所有文件
*
* @param basenames
* @param folder
* @param path
*/
private void getAllFile(List<String> basenames, File folder, String path) {
if (folder.isDirectory()) {
for (File file : folder.listFiles()) {
this.getAllFile(basenames, file, path + folder.getName() + File.separator);
}
} else {
String i18Name = this.getI18FileName(path + folder.getName());
if (!basenames.contains(i18Name)) {
basenames.add(i18Name);
}
}
}
/**
* 把普通文件名转换成国际化文件名
*
* @param filename
* @return
*/
private String getI18FileName(String filename) {
filename = filename.replace(".properties", "");
for (int i = 0; i < 2; i++) {
int index = filename.lastIndexOf("_");
if (index != -1) {
filename = filename.substring(0, index);
}
}
return filename;
}
}
Объясните несколько методов по очереди.
-
init()
Есть метод@PostConstruct
аннотация, это вызывается автоматически после создания экземпляра класса MessageResourceExtensioninit()
метод. Этот метод получаетbaseFolder
Все файлы интернационализации в каталоге и установлены вbasenameSet
середина. и установитьParentMessageSource
, который вызовет родительский MessageSource для поиска информации об интернационализации, если информация об интернационализации не найдена. -
getAllBaseNames()
способ получитьbaseFolder
путь, затем позвонитеgetAllFile()
Метод получает имена всех файлов интернационализации в каталоге. -
getAllFile()
Пройдите по каталогу, если это папка, продолжайте обход, если это файл, вызовитеgetI18FileName()
Преобразуйте имя файла в интернационализированное имя ресурса в формате «i18n/basename/».
Так что просто вставьтеMessageResourceExtension
После создания экземпляра загрузите имя файла ресурсов в папке «i18n» вBasenames
середина. Теперь посмотрите на эффект.
Сначала мы добавляемspring.messages.baseFolder=i18n
, который присваивает значение 'i18n' дляMessageResourceExtension
серединаbaseFolder
.
После запуска я увидел информацию об инициализации, напечатанную в консоли, что указывало на то, что@PostConstruct
Выполнен метод init() аннотации.
Затем мы создаем два набора файлов с информацией об интернационализации: «dashboard» и «merchant», каждый из которых содержит только одну информацию об интернационализации: «dashboard.hello» и «merchant.hello».
Затем измените файл hello.html и перейдите на страницу приветствия.
...
<body>
<h1>国际化页面!</h1>
<p th:text="#{hello}"></p>
<p th:text="#{merchant.hello}"></p>
<p th:text="#{dashboard.hello}"></p>
</body>
...
Вы можете видеть, что информация об интернационализации в «сообщении», «панели инструментов» и «продавце» загружается на веб-страницу, что указывает на то, что мы успешно загрузили файлы в папку «i18n» за один раз.
Установите файл, который отображает информацию об интернационализации на странице внешнего интерфейса в фоновом режиме.
s В предыдущем разделе мы успешно загрузили несколько файлов интернационализации и отобразили информацию об их интернационализации. Но информация об интернационализации в «dashboard.properties» — «dashboard.hello», а в «merchant.properties» — «merchant.hello», поэтому написать префикс для каждого не очень сложно, теперь я хочу Файлы интернационализации для и «приборная панель», и «продавец» пишут только «привет», но отображают информацию об интернационализации из «приборной панели» или «продавца».
существуетMessageResourceExtension
переписатьresolveCodeWithoutArguments
метод (переопределить, если есть необходимость в форматировании символовresolveCode
метод).
@Component("messageSource")
public class MessageResourceExtension extends ResourceBundleMessageSource {
...
public static String I18N_ATTRIBUTE = "i18n_attribute";
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
// 获取request中设置的指定国际化文件名
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
final String i18File = (String) attr.getAttribute(I18N_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
if (!StringUtils.isEmpty(i18File)) {
//获取在basenameSet中匹配的国际化文件名
String basename = getBasenameSet().stream()
.filter(name -> StringUtils.endsWithIgnoreCase(name, i18File))
.findFirst().orElse(null);
if (!StringUtils.isEmpty(basename)) {
//得到指定的国际化文件资源
ResourceBundle bundle = getResourceBundle(basename, locale);
if (bundle != null) {
return getStringOrNull(bundle, code);
}
}
}
//如果指定i18文件夹中没有该国际化字段,返回null会在ParentMessageSource中查找
return null;
}
...
}
в нашем переписыванииresolveCodeWithoutArguments
В методе получите «I18N_ATTRIBUTE» из HttpServletRequest (мы поговорим о том, где его установить позже), это соответствует интернационализированному имени файла, которое мы хотим отобразить, а затем мы устанавливаем его вBasenameSet
найти файл вgetResourceBundle
Получите ресурсы, и, наконец,getStringOrNull
Получите соответствующую информацию об интернационализации.
Теперь идем к нашемуHelloController
Рига двумя способами.
@Controller
public class HelloController {
@GetMapping("/hello")
public String index(HttpServletRequest request) {
request.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, "hello");
return "system/hello";
}
@GetMapping("/dashboard")
public String dashboard(HttpServletRequest request) {
request.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, "dashboard");
return "dashboard";
}
@GetMapping("/merchant")
public String merchant(HttpServletRequest request) {
request.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, "merchant");
return "merchant";
}
}
Обратите внимание, что мы устанавливаем соответствующий «I18N_ATTRIBUTE» в каждом методе, который будет устанавливать соответствующий файл интернационализации в каждом запросе, а затем вMessageResourceExtension
получено в.
На этом этапе мы смотрим на наш файл интернационализации и видим, что все ключевые слова «привет», но информация другая.
В то же время добавлены два новых HTML-файла, а именно «dashboard.html» и «merchant.html», которые содержат только информацию об интернационализации «hello» и название, используемое для дифференциации.
<!-- 这是hello.html -->
<body>
<h1>国际化页面!</h1>
<p th:text="#{hello}"></p>
</body>
<!-- 这是dashboard.html -->
<body>
<h1>国际化页面(dashboard)!</h1>
<p th:text="#{hello}"></p>
</body>
<!-- 这是merchant.html -->
<body>
<h1>国际化页面(merchant)!</h1>
<p th:text="#{hello}"></p>
</body>
Теперь давайте запустим проект и посмотрим.
Можно видеть, что хотя интернационализированное слово на каждой странице — «привет», мы отображаем информацию, которую хотим отобразить на соответствующей странице.
Используйте перехватчики и аннотации для автоматической настройки интерфейсных страниц для отображения интернационализированных информационных файлов.
Хотя можно указать соответствующую информацию об интернационализации, слишком сложно установить файл интернационализации в HttpServletRequest в каждом контроллере, поэтому теперь мы реализуем автоматическое определение для отображения соответствующего файла.
Сначала мы создаем аннотацию, которую можно поместить в класс или метод.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface I18n {
/**
* 国际化文件名
*/
String value();
}
Затем мы создаем этоI18n
Сейчас аннотация находится в методе Controller. Чтобы отобразить ее эффект, мы создаем еще одинShopController
иUserController
, а также создать соответствующие файлы интернационализации «магазин» и «пользователь», содержимое также является «привет».
@Controller
public class HelloController {
@GetMapping("/hello")
public String index() {
return "system/hello";
}
@I18n("dashboard")
@GetMapping("/dashboard")
public String dashboard() {
return "dashboard";
}
@I18n("merchant")
@GetMapping("/merchant")
public String merchant() {
return "merchant";
}
}
@I18n("shop")
@Controller
public class ShopController {
@GetMapping("shop")
public String shop() {
return "shop";
}
}
@Controller
public class UserController {
@GetMapping("user")
public String user() {
return "user";
}
}
мы кладемI18n
Аннотации помещаются вHelloController
внизdashboard
иmerchant
метод иShopController
класс. и удалил оригиналdashboard
иmerchant
Установите оператор I18N_ATTRIBUTE в методе.
Все приготовления завершены, теперь давайте посмотрим, как автоматически указать файлы интернационализации на основе этих аннотаций.
public class MessageResourceInterceptor implements HandlerInterceptor {
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse rep, Object handler, ModelAndView modelAndView) {
// 在方法中设置i18路径
if (null != req.getAttribute(MessageResourceExtension.I18N_ATTRIBUTE)) {
return;
}
HandlerMethod method = (HandlerMethod) handler;
// 在method上注解了i18
I18n i18nMethod = method.getMethodAnnotation(I18n.class);
if (null != i18nMethod) {
req.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, i18nMethod.value());
return;
}
// 在Controller上注解了i18
I18n i18nController = method.getBeanType().getAnnotation(I18n.class);
if (null != i18nController) {
req.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, i18nController.value());
return;
}
// 根据Controller名字设置i18
String controller = method.getBeanType().getName();
int index = controller.lastIndexOf(".");
if (index != -1) {
controller = controller.substring(index + 1, controller.length());
}
index = controller.toUpperCase().indexOf("CONTROLLER");
if (index != -1) {
controller = controller.substring(0, index);
}
req.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, controller);
}
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse rep, Object handler) {
// 在跳转到该方法先清除request中的国际化信息
req.removeAttribute(MessageResourceExtension.I18N_ATTRIBUTE);
return true;
}
}
Кратко объясните этот перехватчик.
Прежде всего, если в запросе уже есть 'I18N_ATTRIBUTE', это означает, что настройка указана в методе контроллера, и дальнейшая оценка не производится.
Затем судите, есть ли способ войти в перехватчикI18n
Аннотация, если есть, установить I18N_ATTRIBUTE на запрос и выйти из перехватчика, если нет, продолжить.
Затем оцените, есть ли какой-либо класс, который входит в перехватI18n
Аннотация, если есть, установить I18N_ATTRIBUTE на запрос и выйти из перехватчика, если нет, продолжить.
Наконец, если нет метода или классаI18n
Обратите внимание, что тогда мы можем автоматически установить указанный файл интернационализации в соответствии с именем контроллера, например «UserController», тогда он найдет файл интернационализации «пользователя».
Перехватчик готов, теперь настройте его в системе. ИсправлятьI18nApplication
Стартовый класс:
@SpringBootApplication
@Configuration
public class I18nApplication {
...
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
//拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor()).addPathPatterns("/**");
registry.addInterceptor(new MessageResourceInterceptor()).addPathPatterns("/**");
}
};
}
}
Теперь давайте запустим его еще раз, чтобы увидеть эффект и увидеть, что каждая ссылка отображает содержимое соответствующей информации об интернационализации.
Наконец
Я только что завершил основные функции всего нашего улучшения интернационализации. Наконец, я разобрал все коды и интегрировал bootstrap4, чтобы показать эффект реализации функций.
Подробный код можно найти на моем GithubSpring-Boot-I18n-Proкод
Оригинальный адрес:zzzzbw.cn
2018/8/30 обновление
После того, как статья была опубликована, кто-то упомянул мне, что когда проект упакован в jar-пакет, он выполняетсяjava -jar i18n-0.0.1.jar
Способ запуска программы выдаст ошибку. Увидев такого рода отзывы, я сразу понял, что при чтении интернационализированных файлов i18n имя файла читается в виде File.Если он запакован в jar-пакет, то все файлы находятся в сжатой папке. Просто получить все файлы в папке в виде File. Поскольку проект компании работает под управлением Tomcat в виде военного пакета, этой проблемы обнаружено не было.
Основная проблема вMessageResourceExtension
Класс вызывается чтением файла конфигурации при запуске spring-boot, поэтому изменитеMessageResourceExtension
:
@Component("messageSource")
public class MessageResourceExtension extends ResourceBundleMessageSource {
...
/**
* 获取文件夹下所有的国际化文件名
*/
private String[] getAllBaseNames(final String folderName) throws IOException {
URL url = Thread.currentThread().getContextClassLoader()
.getResource(folderName);
if (null == url) {
throw new RuntimeException("无法获取资源文件路径");
}
List<String> baseNames = new ArrayList<>();
if (url.getProtocol().equalsIgnoreCase("file")) {
// 文件夹形式,用File获取资源路径
File file = new File(url.getFile());
if (file.exists() && file.isDirectory()) {
baseNames = Files.walk(file.toPath())
.filter(path -> path.toFile().isFile())
.map(Path::toString)
.map(path -> path.substring(path.indexOf(folderName)))
.map(this::getI18FileName)
.distinct()
.collect(Collectors.toList());
} else {
logger.error("指定的baseFile不存在或者不是文件夹");
}
} else if (url.getProtocol().equalsIgnoreCase("jar")) {
// jar包形式,用JarEntry获取资源路径
String jarPath = url.getFile().substring(url.getFile().indexOf(":") + 2, url.getFile().indexOf("!"));
JarFile jarFile = new JarFile(new File(jarPath));
List<String> baseJars = jarFile.stream()
.map(ZipEntry::toString)
.filter(jar -> jar.endsWith(folderName + "/")).collect(Collectors.toList());
if (baseJars.isEmpty()) {
logger.info("不存在{}资源文件夹", folderName);
return new String[0];
}
baseNames = jarFile.stream().map(ZipEntry::toString)
.filter(jar -> baseJars.stream().anyMatch(jar::startsWith))
.filter(jar -> jar.endsWith(".properties"))
.map(jar -> jar.substring(jar.indexOf(folderName)))
.map(this::getI18FileName)
.distinct()
.collect(Collectors.toList());
}
return baseNames.toArray(new String[0]);
}
/**
* 把普通文件名转换成国际化文件名
*/
private String getI18FileName(String filename) {
filename = filename.replace(".properties", "");
for (int i = 0; i < 2; i++) {
int index = filename.lastIndexOf("_");
if (index != -1) {
filename = filename.substring(0, index);
}
}
return filename.replace("\\", "/");
}
...
}
существуетgetAllBaseNames()
Метод сначала определит, является ли URL-форма проекта формой файла или пакета jar.
Если он в виде файла, то он будет читаться как обычная папка, а здесь тоже используется в java8Files.walk()
Метод получает все файлы в папке, что гораздо удобнее, чем писать рекурсивно для самостоятельного чтения.
Если он представлен в виде пакета jar, то для обработки файла используется JarEntry.
Во-первых, нужно получить каталог, в котором находится пакет jar проекта, напримерE:/workspace/java/Spring-Boot-I18n-Pro/target/i18n-0.0.1.jar
Этот, а потом новый по справочникуJarFile
.
Тогда пройдите через этоJarFile
Ресурсы в пакете, это будет читать все файлы в пакете jar нашего проекта, поэтому мы должны сначала найти каталог, в котором находятся наши файлы ресурсов i18n, через.filter(jar -> jar.endsWith(folderName + "/"))
Получить каталог, в котором находится ресурс.
Следующий шаг - судитьJarFile
Находится ли файл в пакете в каталоге ресурсов i18n, если да, вызовитеgetI18FileName()
способ отформатировать его в нужную нам форму имени.
После этой операции получается имя файла ресурсов i18n в пакете jar.