XMLλ‘ λ€κ΅μ΄ λ©μμ§ κ΄λ¦¬νκΈ°
λ³Έ κΈμ 벨리λ°μ΄μ μ€λ₯ λ©μμ§κ° μ¬μ©μ μΈμ΄λ‘ μ²λ¦¬λμ§ μμ μ΄μ λ₯Ό μμ±νλ©΄μ 보μλμμ΅λλ€.
λ€κ΅μ΄ λ©μμ§
μ€νλ§μ΄λ μ€νλ§ λΆνΈμμ λ€κ΅μ΄ λ©μμ§λ₯Ό μ μ©νκΈ° μν΄μλ Propertiesλ₯Ό κΈ°λ³ΈμΌλ‘ μ¬μ©ν΄μΌν©λλ€. κ·Έλ¬λ, messages-en.properties λλ messages-ko.propertiesμ κ°μ΄ μΈμ΄λ³λ‘ νλ‘νΌν° νμΌμ ꡬλΆνμ¬ λ©μμ§λ₯Ό κ΄λ¦¬ν΄μΌλ§ ν©λλ€. μ΄μ²λΌ νλ‘νΌν° νμΌλ‘ λ©μμ§λ₯Ό κ΄λ¦¬νλ€λ³΄λ©΄ ν΄λΉ μΈμ΄μμ νΉμ λ©μμ§λ₯Ό ν€λ₯Ό μ¬μ©νλμ§ νμ νλκ² μλΉν μ΄λ ΅μ΅λλ€. νμ¬ μ‘°μ§μ²λΌ νμ¬ λ΄ νλ‘μ νΈλ₯Ό μ§νν λ λ©μμ§ ν€μ λν΄ μ μλ λ¬Έμκ° μλ κ²½μ°μλ κ°λ°μκ° λ©μμ§ μ½λλ₯Ό κ΄λ¦¬ν΄μΌνλ―λ‘ λ§€λ² κ²μν΄μ μ¬μ©νκ³ μλμ§ νμ ν΄μΌλ§ ν©λλ€.
λμ λ°©μ
νλ‘νΌν° νμΌλ‘ λ€κ΅μ΄ λ©μμ§λ₯Ό κ΄λ¦¬νλ κ²μ 보μνκΈ° μν λ°©λ²μ λ€μν©λλ€.
YAML
μ ν리μΌμ΄μ νλ‘νΌν° νμΌμ μΌλ―(Yaml) νμΌλ‘ λ체νλ κ²μ²λΌ YAML νμΌμ μ¬μ©ν΄μ λ€κ΅μ΄ λ©μμ§λ₯Ό κ΄λ¦¬νλ λ°©λ²μ κΈ°μ΅νκΈ° μν κ°λ°λ ΈνΈ:μ€νλ§λΆνΈμμ λ€κ΅μ΄ κΈ°λ₯ μ¬μ©νκΈ°λ₯Ό ν΅ν΄ νμΈν μ μμ΅λλ€. κ·Έλ¬λ μ΄ λ°©μμ νμΌλ§ λ체ν λΏ λ©μμ§λ₯Ό κ΄λ¦¬νκΈ° μν YAML νμΌμ μΈμ΄λ³λ‘ λ§λ€μ΄μΌν¨μΌλ‘ νλ‘νΌν°μ λ¬Έμ μ μ λμΌνκ² κ°μ§κ³ μμ΅λλ€.
ISO-8859-1 μΈμ½λ© νμμΌλ‘ μ μ₯λλ νλ‘νΌν°μλ λ€λ₯΄κ² νκΈμ΄ μ λμ½λλ‘ νμλμ§ μλλ€λ μ₯μ μ μ‘΄μ¬ν©λλ€.
XML
νμ¬ μ‘°μ§μμλ νλ‘μ νΈμμ μ¬μ©νλ λ€κ΅μ΄ λ©μμ§λ₯Ό XML νμΌλ‘ ꡬμ±νμ¬ κ΄λ¦¬νκ³ μμ΅λλ€. λ€μμ λ€κ΅μ΄ λ©μμ§λ₯Ό κ΄λ¦¬νκΈ° μν XML νμΌμ κ°λ¨ν μμμ λλ€.
<?xml version="1.0" encoding="UTF-8"?>
<messages>
<entry key="btn.signIn">
<ko_KR><![CDATA[λ‘κ·ΈμΈ]]></ko_KR>
<en_US><![CDATA[Login]]></en_US>
</entry>
</messages>
컀μ€ν 리μμ€ λ²λ€
νλ‘νΌν° νμΌ λμ μ XMLλ‘ λ©μμ§ μμ€λ₯Ό λ§λ€κΈ° μν΄μλ 리μμ€ λ²λ€λΆν° λ§λ€μ΄μΌν©λλ€. ResourceBundle ν΄λμ€μ getBundle ν¨μλ₯Ό μ¬μ©ν΄μ XML νμΌμ μ½μ΄ 리μμ€ λ²λ€λ‘ λ³νν μ μμ΅λλ€. 리μμ€ λ²λ€λ‘ λ©μμ§ μμ€λ₯Ό λ§λλ ꡬ쑰λ XML κΈ°λ°μ Resource Bundle, PropertyPlaceHolder μ¬μ©νκΈ°μμ νμΈν μ μμ΅λλ€.
XmlResourceBundle
κ·Έλ¬λ μμ μμλ³Έ λ€κ΅μ΄ λ©μμ§μ λν XML νμΌμ νλ‘νΌν° ꡬ쑰λ₯Ό λ°λ₯΄μ§ μμ΅λλ€. κ·Έλμ Properties.loadFromXML ν¨μλ₯Ό ν΅ν΄ XMLμ νλ‘νΌν° κΈ°μ€μΌλ‘ μ½μΌλ©΄ μλ©λλ€. λ€μμ²λΌ XML ꡬμ±μ λ°λΌμ λ©μμ§ μ 보λ₯Ό λ§λ€μ΄μΌν©λλ€.
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
public class XmlResourceBundle extends ResourceBundle {
private Map<String, Map<String, String>> messages;
private Locale i18n;
public XmlResourceBundle(InputStream is, Locale i18n) throws IOException, ParserConfigurationException, SAXException {
try (is) {
this.i18n = i18n;
messages = new HashMap<>();
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(is);
doc.getDocumentElement().normalize();
NodeList entries = doc.getElementsByTagName("entry");
for (int i = 0; i < entries.getLength(); i++) {
Element entry = (Element) entries.item(i);
String key = entry.getAttribute("key");
NodeList childNodes = entry.getChildNodes();
for (int j = 0; j < childNodes.getLength(); j++) {
Node n = childNodes.item(j);
if (n.getNodeType() == Node.ELEMENT_NODE) {
String locale = n.getNodeName();
String message = n.getTextContent();
if (!messages.containsKey(locale)) {
messages.put(locale, new HashMap<>());
}
messages.get(locale).put(key, message);
}
}
}
}
}
@Override
protected Object handleGetObject(String key) {
return messages.get(i18n).get(key);
}
@Override
public Enumeration<String> getKeys() {
Set<String> handleKeys = messages.keySet();
return Collections.enumeration(handleKeys);
}
public void setLocale(Locale locale) {
this.i18n = locale;
}
public Map<String, Map<String, String>> getMessages() {
return messages;
}
public Map<String, String> getMessages(Locale locale) {
return messages.get(locale.toString());
}
}
κΈ°μ‘΄μ handleGetObject ν¨μλ ν€ νλΌλ―Έν°λ§ λ°λλ‘ λμ΄μκΈ° λλ¬Έμ λ©μμ§λ₯Ό κ°μ Έμ¬ κ²½μ°μ μΈμ΄λ₯Ό μ§μ ν μ μμΌλ―λ‘ λ¦¬μμ€ λ²λ€μ μμ±νλ μμ μ μΈμ΄λ₯Ό μ§μ ν μ μκ² νμμ΅λλ€.
XmlResourceBundleLoader
XmlResourceBundleλ₯Ό λ‘λνκΈ° μν ν΄λμ€λ₯Ό λ§λ€κΈ° μν΄μ The Strings.xml Resource Bundleμ μ°Έκ³ νμ¬ μ½λλ₯Ό μμ±ν©λλ€.
import lombok.extern.slf4j.Slf4j;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
@Slf4j
public class XmlResourceBundleLoader extends ResourceBundle.Control {
private static final List<String> formats = Collections.singletonList("xml");
@Override
public List<String> getFormats(String baseName) {
return formats;
}
@Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException, InstantiationException, IOException {
ResourceBundle resourceBundle = null;
String bundleName = toBundleName(baseName, locale);
String resourceName = toResourceName(bundleName, format);
URL url = loader.getResource(resourceName);
if (url == null) {
return null;
}
URLConnection connection = url.openConnection();
if (connection == null) {
return null;
}
if (reload) {
connection.setUseCaches(false);
}
InputStream stream = connection.getInputStream();
if (stream == null) {
return null;
}
try (BufferedInputStream bis = new BufferedInputStream(stream)) {
if (locale == Locale.ROOT) {
locale = Locale.getDefault();
}
resourceBundle = new XmlResourceBundle(bis, locale);
} catch (SAXException | ParserConfigurationException e) {
log.error(e.getMessage());
}
return resourceBundle;
}
}
λ©μμ§ μμ€
μ€λΉλ 리μμ€ λ²λ€μ μ¬μ©νκΈ° μν΄μ λ©μμ§ μμ€λ‘ λ³νν΄μΌν©λλ€. μ¬μ©μ μ μ λ©μμ§ μμ€λ₯Ό λ§λ€κΈ° μν΄μλ AbstractMessageSourceλ₯Ό μμνλ©΄ λ©λλ€.
package com.example.springboot.i18n;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.AbstractMessageSource;
import org.springframework.stereotype.Component;
import java.text.MessageFormat;
import java.util.*;
@Slf4j
@Component
public class CustomMessageSource extends AbstractMessageSource {
private final Map<String, Map<String, MessageFormat>> formats = new HashMap<>();
private Map<String, Map<String, String>> messages = new HashMap<>();
@Autowired
public CustomMessageSource() {
try {
load();
} catch (Exception e) {
e.printStackTrace();
}
}
public void load() {
ResourceBundle resourceBundle = ResourceBundle.getBundle("messages", Locale.ROOT, new XmlResourceBundleLoader());
XmlResourceBundle xmlResourceBundle = (XmlResourceBundle) resourceBundle;
this.messages = xmlResourceBundle.getMessages();
}
// λ©μμ§ μμ€μμ μ§μνλ μΈμ΄ λͺ©λ‘
public List<String> getLocales() {
return new ArrayList<>(messages.keySet());
}
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
synchronized (formats) {
// μΈμ΄ ν¬λ§·μ΄ μμ κ²½μ° λ©μμ§ ν¬λ§·μ μλ‘ μμ±
if (!formats.containsKey(locale.toString())) {
formats.put(locale.toString(), new HashMap<>());
}
Map<String, MessageFormat> map = formats.get(locale.toString());
// μΈμ΄ ν¬λ§·μ λ©μμ§ μ½λκ° μμΌλ©΄ λ©μμ§ μ 보λ₯Ό ν΅ν΄ ν¬λ§·μ μ μ₯
if (!map.containsKey(code)) {
if (!messages.containsKey(locale.toString())) {
locale = Locale.getDefault();
}
Map<String, String> msgs = messages.get(locale.toString());
map.put(code, new MessageFormat(msgs.getOrDefault(code, code), locale));
}
return map.get(code);
}
}
public String getMessage(String code) {
try {
return getMessage(code, new Object[0], LocaleContextHolder.getLocale());
} catch (NoSuchMessageException e) {
return code;
}
}
public String getMessage(String code, Object[] args) {
try {
return getMessage(code, args, LocaleContextHolder.getLocale());
} catch (NoSuchMessageException e) {
return code;
}
}
public String getMessage(String code, Object[] args, String defaultMessage) {
try {
return getMessage(code, args, defaultMessage, LocaleContextHolder.getLocale());
} catch (NoSuchMessageException e) {
return code;
}
}
public String getMessage(String code, Locale locale) {
try {
return getMessage(code, new Object[0], locale);
} catch (NoSuchMessageException e) {
return code;
}
}
}
λ©μμ§ μμ€ κ°±μ
λ‘컬 νκ²½μμ μ ν리μΌμ΄μ μ κ°λ°νλ λμμλ λ©μμ§ μμ€ μ λ³΄κ° κ³μ λ³κ²½λμ΄μΌνλ μꡬμ¬νμ΄ μμ΅λλ€. μ€νλ§ λΆνΈλ₯Ό μ¬μ©νκ³ μλ€λ©΄ spring-boot-devtoolμ μ¬μ©ν΄μ μ ν리μΌμ΄μ μ΄ λ€μ μ€νν μ μκ² λ³κ²½ν μ μμ΅λλ€. κ·Έλ¬λ, λ©μμ§ μ λ³΄κ° λ³κ²½λλ κ²μ΄ μ ν리μΌμ΄μ μ체μ νΉλ³ν μν₯μ λ―ΈμΉμ§λ μμ΅λλ€. μ ν리μΌμ΄μ μ λ€μ μ€ννμ§ μκ³ λ³κ²½λ λ©μμ§ μ 보λ₯Ό λ°μν μ μλλ‘ νλ κ²μ΄ μ’μ΅λλ€.
μ°λ¦¬κ° ꡬνν΄μΌν λμμ ReloadableResourceBundleMessageSourceκ° λ³κ²½λ νλ‘νΌν° νμΌμ λ€μ λ‘λνλ κ²κ³Ό λΉμ·ν©λλ€.
@Repository
public class CustomMessageSource extends AbstractMessageSource {
@Autowired
public CustomMessageSource(Environment environment) {
instance = this;
try {
load();
} catch (Exception e) {
e.printStackTrace();
}
if (!ArrayUtils.contains(environment.getActiveProfiles(), "production")) {
reload();
}
}
public void reload() {
new Thread(() -> {
try {
File file = new ClassPathResource("messages.xml").getFile();
long lastModified = -1;
while (true) {
try {
if (lastModified < file.lastModified()) {
load();
log.info("Reload MessageSource - {}", System.currentTimeMillis());
lastModified = file.lastModified();
}
} catch (Exception e) {
log.error(e.getMessage());
}
Thread.sleep(5000);
}
} catch (Exception e) {
log.error(e.getMessage());
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
}
}).start();
}
}
μ μ²λΌ νμΌ μμ μΌμ λΉκ΅νμ§ μμλ μλ° 7μ WatchServiceλ₯Ό νμ©ν΄λ νμΌμ κ°μν μ μμ΅λλ€.