Самостоятельно усилить функцию интернационализации на Spring-Boot

задняя часть внешний интерфейс GitHub Spring

предисловие

Компания будет проектировать от Struts2 до SpringMVC, потому что бизнес компании является зарубежным сервисом, так высокий на международные функциональные требования. Struts2 поставляется с интернационализацией SpringMVC относительно более совершенным более совершенным, но весна может быть отличной особенностью - это настраиваемый набор сильного сопротивления, поэтому, когда компания перенесла проектом SpringMVC, увеличила свои международные возможности. Настоящим документирует и немного улучшаю.

Основные функции реализованные в данной статье:

  1. Загружайте несколько файлов интернационализации прямо из папки
  2. Установите файл, который отображает информацию об интернационализации на странице внешнего интерфейса в фоновом режиме.
  3. Используйте перехватчики и аннотации, чтобы автоматически настроить главную страницу для отображения файлов международной информации.

Примечание. В этой статье не описывается, как настроить интернационализацию, региональный парсер и т. д.

выполнить

Инициализация проекта интернационализации

Сначала создайте базовый проект информации об интернационализации 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).

hello.html

Поскольку языком браузера по умолчанию является китайский, он перейдет к messages_zh_CN.properties, чтобы найти его по умолчанию, а если нет, он перейдет к messages.properties, чтобы найти интернационализированные слова.

Затем набираем в браузереhttp://localhost:9090/hello?locale=en_US, язык переключится на английский. Точно так же, если параметр после URL-адреса установлен наlocale=zh_CH, язык переключится на китайский.

hello.html_en_US

Загружайте несколько файлов интернационализации прямо из папки

На нашей странице 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;
    }
}

Объясните несколько методов по очереди.

  1. init()Есть метод@PostConstructаннотация, это вызывается автоматически после создания экземпляра класса MessageResourceExtensioninit()метод. Этот метод получаетbaseFolderВсе файлы интернационализации в каталоге и установлены вbasenameSetсередина. и установитьParentMessageSource, который вызовет родительский MessageSource для поиска информации об интернационализации, если информация об интернационализации не найдена.
  2. getAllBaseNames()способ получитьbaseFolderпуть, затем позвонитеgetAllFile()Метод получает имена всех файлов интернационализации в каталоге.
  3. getAllFile()Пройдите по каталогу, если это папка, продолжайте обход, если это файл, вызовитеgetI18FileName()Преобразуйте имя файла в интернационализированное имя ресурса в формате «i18n/basename/».

Так что просто вставьтеMessageResourceExtensionПосле создания экземпляра загрузите имя файла ресурсов в папке «i18n» вBasenamesсередина. Теперь посмотрите на эффект.

Сначала мы добавляемspring.messages.baseFolder=i18n, который присваивает значение 'i18n' дляMessageResourceExtensionсерединаbaseFolder.

После запуска я увидел информацию об инициализации, напечатанную в консоли, что указывало на то, что@PostConstructВыполнен метод init() аннотации.

image

Затем мы создаем два набора файлов с информацией об интернационализации: «dashboard» и «merchant», каждый из которых содержит только одну информацию об интернационализации: «dashboard.hello» и «merchant.hello».

image

Затем измените файл hello.html и перейдите на страницу приветствия.

...
<body>
<h1>国际化页面!</h1>
<p th:text="#{hello}"></p>
<p th:text="#{merchant.hello}"></p>
<p th:text="#{dashboard.hello}"></p>
</body>
...

image
image

Вы можете видеть, что информация об интернационализации в «сообщении», «панели инструментов» и «продавце» загружается на веб-страницу, что указывает на то, что мы успешно загрузили файлы в папку «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.