Spring之国际化信息MessageSource源码阅读

来源:互联网 发布:淘宝附近的人取消了吗 编辑:程序博客网 时间:2024/06/06 21:05

MessageSource架构图

这里写图片描述
1. MessageSource:抽象化的消息接口
2. HierarchicalMessageSource:分层的消息源接口,可获取父消息源
3. MessageSourceSupport:帮助消息源解析的抽象类,通过指定“消息格式化组件MessageFormat”格式化消息。
4. DelegatingMessageSource: 消息源解析委派类(用户未指定时,SpringContext默认使用当前类),功能比较简单:将字符串和参数数组格式化为一个消息字符串
5. AbstractMessageSource:支持‘配置文件’的方式国际化资源 的 抽象类,内部提供一个与区域设置无关的公共消息配置文件,消息代码为关键字。
6. StaticMessageSource:主要用于程序测试,它允许通过编程的方式提供国际化信息
7. ResourceBundleMessageSource:该实现类允许用户通过beanName指定一个资源名(包括类路径的全限定资源名),或通过beanNames指定一组资源名。不同的区域获取加载资源文件,达到国际化信息的目的。
8. ReloadableResourceBundleMessageSource:同ResourceBundleMessageSource区别(spring3.2):
1)加载资源类型及方式:
ResourceBundleMessageSource 依托JDK自带ResourceBundle加载资源,支持绝对路径和工程路径,支持文件为.class文件和.properties。
ReloadResourceBundleMessageSource依托spring的ResourceLoader加载Resource资源,功能更加强大,同时支持.properties和.xml文件。
2)缓存时间:
ResourceBundleMessageSource主要利用ResourceBundle.Control 实现简单的自动重载。
ReloadResourceBundleMessageSource每次加载资源都会记录每个资源的加载时间点,在缓存资源过期后会再次比较文件的修改时间,如果不变则不需要重新加载,同时刷新本次加载时间点。
3)编码方式:
ResourceBundleMessageSource可以统一指定默认的文件编码方式
ReloadResourceBundleMessageSource不仅可以指定统一的默认编码方式,也同时支持为每个文件单独制定编码方式

spring中初始化MessageSource组件

//springContext国际化资源信息初始化。这里的messageSource主要是将这个Bean定义的信息资源加载为容器级的国际化信息资源。public abstract class AbstractApplicationContext extends DefaultResourceLoader        implements ConfigurableApplicationContext, DisposableBean {    public static final String MESSAGE_SOURCE_BEAN_NAME = "messageSource";    /** 实际委托的消息源解析对象 */    private MessageSource messageSource;    //======省略一段代码========/    public void refresh() throws BeansException, IllegalStateException {        //======省略一段代码========/        // 初始化此上下文的消息源。        initMessageSource();        // Initialize event multicaster for this context.        initApplicationEventMulticaster();        //======省略一段代码========/    }    //======省略一段代码========/    /**     * 初始化此上下文的消息源。     * 如果没有在此上下文中定义,请使用父级。     */    protected void initMessageSource() {        //获取工厂        ConfigurableListableBeanFactory beanFactory = getBeanFactory();        //先找工厂中messageSource是否有对应的实例(注册过)        if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {            this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);            //messageSource是否有父消息源            if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {                HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource;                if (hms.getParentMessageSource() == null) {                    // 父MessageSource未注册,则设置messageSource的parentMessageSource为“父上下文的‘MessageSource’ ”。                    hms.setParentMessageSource(getInternalParentMessageSource());                }            }            if (logger.isDebugEnabled()) {                logger.debug("Using MessageSource [" + this.messageSource + "]");            }        }        else {//没有对应的实例,则自己初始化一个            // 实例化一个spring默认实现消息源对象            DelegatingMessageSource dms = new DelegatingMessageSource();            //设置父消息源            dms.setParentMessageSource(getInternalParentMessageSource());            this.messageSource = dms;            //‘单例模式’注册到工厂中            beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);            if (logger.isDebugEnabled()) {                logger.debug("Unable to locate MessageSource with name '" + MESSAGE_SOURCE_BEAN_NAME +                        "': using default [" + this.messageSource + "]");            }        }    }    //获取父上下文的消息源    protected MessageSource getInternalParentMessageSource() {        return (getParent() instanceof AbstractApplicationContext) ?            ((AbstractApplicationContext) getParent()).messageSource : getParent();    }}

各个类源码阅读

MessageSource接口

public interface MessageSource {    /**     * 尝试解决消息。 如果没有找到消息,返回默认消息。     */    String getMessage(String code, Object[] args, String defaultMessage, Locale locale);    /**     * 尝试解决消息。 如果无法找到消息,则视为错误。     */    String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;    /**     * 尝试使用传入的{@code MessageSourceResolvable}参数中包含的所有属性来解析消息。     * <p>NOTE: 我们必须在此方法上抛出{@code NoSuchMessageException},因为在调用此方法时,我们无法确定可解析的{@code defaultMessage}属性是否为空。     */    String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;}

MessageSourceResolvable解析消息要素的包装接口和类

//解析消息要素的包装类:code、Arguments、默认消息public interface MessageSourceResolvable {    /**     * 返回用于解决此消息的代码,按照他们应该尝试的顺序。 因此,最后一个代码将是默认代码。     * @return a String array of codes which are associated with this message     */    String[] getCodes();    /**     * 返回要用于解析此消息的参数数组。     * @return an array of objects to be used as parameters to replace     * placeholders within the message text     * @see java.text.MessageFormat     */    Object[] getArguments();    /**     * 返回要用于解析此消息的默认消息。     * @return the default message, or {@code null} if no default     */    String getDefaultMessage();}//spring默认实现的 解析消息要素的包装类public class DefaultMessageSourceResolvable implements MessageSourceResolvable, Serializable {    private final String[] codes;    private final Object[] arguments;    private final String defaultMessage;    //构造方法    public DefaultMessageSourceResolvable(String code) {        this(new String[] {code}, null, null);    }    //构造方法    public DefaultMessageSourceResolvable(String[] codes) {        this(codes, null, null);    }    //构造方法    public DefaultMessageSourceResolvable(String[] codes, String defaultMessage) {        this(codes, null, defaultMessage);    }    //构造方法    public DefaultMessageSourceResolvable(String[] codes, Object[] arguments) {        this(codes, arguments, null);    }    //构造方法    public DefaultMessageSourceResolvable(String[] codes, Object[] arguments, String defaultMessage) {        this.codes = codes;        this.arguments = arguments;        this.defaultMessage = defaultMessage;    }    //构造方法    public DefaultMessageSourceResolvable(MessageSourceResolvable resolvable) {        this(resolvable.getCodes(), resolvable.getArguments(), resolvable.getDefaultMessage());    }    public String[] getCodes() {        return this.codes;    }    /**     * 返回此可解析的默认代码,即代码数组中的最后一个代码。     */    public String getCode() {        return (this.codes != null && this.codes.length > 0) ? this.codes[this.codes.length - 1] : null;    }    public Object[] getArguments() {        return this.arguments;    }    public String getDefaultMessage() {        return this.defaultMessage;    }    /**     * 为此MessageSourceResolvable构建默认的String表示形式:包括代码,参数和默认消息。     */    protected final String resolvableToString() {        StringBuilder result = new StringBuilder();        result.append("codes [").append(StringUtils.arrayToDelimitedString(this.codes, ","));        result.append("]; arguments [" + StringUtils.arrayToDelimitedString(this.arguments, ","));        result.append("]; default message [").append(this.defaultMessage).append(']');        return result.toString();    }    /**默认实现公开了此MessageSourceResolvable的属性。要在更具体的子类中被覆盖,可能通过{@code resolvableToString()}包含可解析的内容。     * @see #resolvableToString()     */    @Override    public String toString() {        return getClass().getName() + ": " + resolvableToString();    }    @Override    public boolean equals(Object other) {        if (this == other) {            return true;        }        if (!(other instanceof MessageSourceResolvable)) {            return false;        }        MessageSourceResolvable otherResolvable = (MessageSourceResolvable) other;        return ObjectUtils.nullSafeEquals(getCodes(), otherResolvable.getCodes()) &&                ObjectUtils.nullSafeEquals(getArguments(), otherResolvable.getArguments()) &&                ObjectUtils.nullSafeEquals(getDefaultMessage(), otherResolvable.getDefaultMessage());    }    @Override    public int hashCode() {        int hashCode = ObjectUtils.nullSafeHashCode(getCodes());        hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getArguments());        hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getDefaultMessage());        return hashCode;    }}

HierarchicalMessageSource消息源分层接口

public interface HierarchicalMessageSource extends MessageSource {    /**     * 设置将用于尝试解决此对象无法解析的消息的父级。     * @param parent 将用于解析此对象无法解析的邮件的父MessageSource。 可能是{@code null},在这种情况下不需要进一步的解决。     */    void setParentMessageSource(MessageSource parent);    /**     * 返回此MessageSource的父级,否则返回{@code null}。     */    MessageSource getParentMessageSource();}

MessageSourceSupport

//用于支撑消息源解析的抽象类public abstract class MessageSourceSupport {    //默认’消息格式组件‘    private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat("");    /** Logger available to subclasses */    protected final Log logger = LogFactory.getLog(getClass());    //是否始终应用’消息格式组件‘,解析没有参数的消息。    private boolean alwaysUseMessageFormat = false;    //缓存来保存已解决消息msg的’消息格式组件‘。 用于传入的默认消息(如:AbstractMessageSource中的commonMessages中的消息)。     private final Map<String, Map<Locale, MessageFormat>> messageFormatsPerMessage =            new HashMap<String, Map<Locale, MessageFormat>>();    /**     * 设置是否始终应用’消息格式组件‘,解析没有参数的消息。     * <p>例如:,MessageFormat希望单引号被转义为"''"。 如果您的消息文本全部用这样的转义编写,     * 即使没有定义参数占位符,您需要将此标志设置为“true”。      * 否则,只有具有实际参数的消息文本应该用MessageFormat转义来编写。     * @see java.text.MessageFormat     */    public void setAlwaysUseMessageFormat(boolean alwaysUseMessageFormat) {        this.alwaysUseMessageFormat = alwaysUseMessageFormat;    }    /**     * 返回是否始终应用’消息格式组件‘,解析没有参数的消息。     */    protected boolean isAlwaysUseMessageFormat() {        return this.alwaysUseMessageFormat;    }    //渲染给定的默认消息字符串。     protected String renderDefaultMessage(String defaultMessage, Object[] args, Locale locale) {        return formatMessage(defaultMessage, args, locale);    }    //渲染给定的消息字符串。     protected String formatMessage(String msg, Object[] args, Locale locale) {        if (msg == null || (!this.alwaysUseMessageFormat && ObjectUtils.isEmpty(args))) {            return msg;        }        MessageFormat messageFormat = null;        synchronized (this.messageFormatsPerMessage) { //防止并发操作            //先尝试取缓存’消息格式组件‘            Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage.get(msg);            if (messageFormatsPerLocale != null) {                messageFormat = messageFormatsPerLocale.get(locale);            }            else {//缓存为空,设置初始化空map值                messageFormatsPerLocale = new HashMap<Locale, MessageFormat>();                this.messageFormatsPerMessage.put(msg, messageFormatsPerLocale);            }            //’消息格式组件‘为空,则初始化一个messageFormat            if (messageFormat == null) {                try {                    messageFormat = createMessageFormat(msg, locale);                }                catch (IllegalArgumentException ex) {                    // 如果alwaysUseMessageFormat为true,即抛错                    if (this.alwaysUseMessageFormat) {                        throw ex;                    }                    // 如果格式未被强制执行,则默认继续处理原始消息                    messageFormat = INVALID_MESSAGE_FORMAT;                }                //缓存当前消息的’消息格式组件‘                messageFormatsPerLocale.put(locale, messageFormat);            }        }        //消息解析对象为默认消息解析器,直接返回msg        if (messageFormat == INVALID_MESSAGE_FORMAT) {            return msg;        }        //使用messageFormat解析解析消息        synchronized (messageFormat) {            return messageFormat.format(resolveArguments(args, locale));        }    }    //为给定的消息和区域设置创建一个MessageFormat。    protected MessageFormat createMessageFormat(String msg, Locale locale) {        return new MessageFormat((msg != null ? msg : ""), locale);    }    //子类可以覆盖,否则只是按原样返回给定的参数数组    protected Object[] resolveArguments(Object[] args, Locale locale) {        return args;    }}

DelegatingMessageSource消息源解析委派类

//第一部分:消息源解析委派类(用户未指定时,SpringContext默认使用当前类)public class DelegatingMessageSource extends MessageSourceSupport implements HierarchicalMessageSource {    //父消息解析源    private MessageSource parentMessageSource;    public void setParentMessageSource(MessageSource parent) {        this.parentMessageSource = parent;    }    public MessageSource getParentMessageSource() {        return this.parentMessageSource;    }    //解析消息,父消息解析源不为null时,则采用父消息源解析消息,否则有自身解析    public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {        if (this.parentMessageSource != null) {             return this.parentMessageSource.getMessage(code, args, defaultMessage, locale);        }        else {            return renderDefaultMessage(defaultMessage, args, locale);        }    }    //解析消息,父消息解析源不为null时,则采用父消息源解析消息,否则抛错    public String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException {        if (this.parentMessageSource != null) {            return this.parentMessageSource.getMessage(code, args, locale);        }        else {            throw new NoSuchMessageException(code, locale);        }    }    //解析消息,父消息解析源不为null时,则采用父消息源解析消息,否则有自身解析    public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {        if (this.parentMessageSource != null) {            return this.parentMessageSource.getMessage(resolvable, locale);        }        else {            if (resolvable.getDefaultMessage() != null) {                return renderDefaultMessage(resolvable.getDefaultMessage(), resolvable.getArguments(), locale);            }            String[] codes = resolvable.getCodes();            String code = (codes != null && codes.length > 0 ? codes[0] : null);            throw new NoSuchMessageException(code, locale);        }    }}

AbstractMessageSource抽象类

//第二部分:spring支持‘配置文件’的方式国际化资源 的 抽象类public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource {    //父消息源    private MessageSource parentMessageSource;    //与区域设置无关的公共消息,消息代码为关键字    // <bean id="commonMessages" class="org.springframework.beans.factory.config.PropertiesFactoryBean">    //   <property name="locations">    //       <list>    //           <value>classpath*:application.properties</value>    //       </list>    //   </property>    //</bean>    private Properties commonMessages;    //是否使用消息代码作为默认消息,而不是抛出NoSuchMessageException。 适用于开发和调试。    private boolean useCodeAsDefaultMessage = false;    public void setParentMessageSource(MessageSource parent) {        this.parentMessageSource = parent;    }    public MessageSource getParentMessageSource() {        return this.parentMessageSource;    }    /**     * Specify locale-independent common messages, with the message code as key     * and the full message String (may contain argument placeholders) as value.     * <p>May also link to an externally defined Properties object, e.g. defined     * through a {@link org.springframework.beans.factory.config.PropertiesFactoryBean}.     */    public void setCommonMessages(Properties commonMessages) {        this.commonMessages = commonMessages;    }    /**     * Return a Properties object defining locale-independent common messages, if any.     */    protected Properties getCommonMessages() {        return this.commonMessages;    }    //设置是否使用消息代码作为默认消息,而不是抛出NoSuchMessageException。 适用于开发和调试。    //默认值为“false”。    public void setUseCodeAsDefaultMessage(boolean useCodeAsDefaultMessage) {        this.useCodeAsDefaultMessage = useCodeAsDefaultMessage;    }    protected boolean isUseCodeAsDefaultMessage() {        return this.useCodeAsDefaultMessage;    }    public final String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {        String msg = getMessageInternal(code, args, locale);        if (msg != null) {            return msg;        }        if (defaultMessage == null) {            String fallback = getDefaultMessage(code);            if (fallback != null) {                return fallback;            }        }        return renderDefaultMessage(defaultMessage, args, locale);    }    public final String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException {        String msg = getMessageInternal(code, args, locale);        if (msg != null) {            return msg;        }        String fallback = getDefaultMessage(code);        if (fallback != null) {            return fallback;        }        throw new NoSuchMessageException(code, locale);    }    public final String getMessage(MessageSourceResolvable resolvable, Locale locale)            throws NoSuchMessageException {        String[] codes = resolvable.getCodes();        if (codes == null) {            codes = new String[0];        }        for (String code : codes) {            String msg = getMessageInternal(code, resolvable.getArguments(), locale);            if (msg != null) {                return msg;            }        }        String defaultMessage = resolvable.getDefaultMessage();        if (defaultMessage != null) {            return renderDefaultMessage(defaultMessage, resolvable.getArguments(), locale);        }        if (codes.length > 0) {            String fallback = getDefaultMessage(codes[0]);            if (fallback != null) {                return fallback;            }        }        throw new NoSuchMessageException(codes.length > 0 ? codes[codes.length - 1] : null, locale);    }    //将给定的代码和参数解析为给定的区域设置中的消息,如果没有找到返回{@code null}。     protected String getMessageInternal(String code, Object[] args, Locale locale) {        if (code == null) {            return null;        }        if (locale == null) {            locale = Locale.getDefault();        }        Object[] argsToUse = args;        if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {            //解析不带参数数组的消息            String message = resolveCodeWithoutArguments(code, locale);            if (message != null) {                return message;            }        }        else {            //解析参数数组            argsToUse = resolveArguments(args, locale);            //获取’消息格式化组件‘            MessageFormat messageFormat = resolveCode(code, locale);            if (messageFormat != null) {                synchronized (messageFormat) {                    return messageFormat.format(argsToUse);                }            }        }        // 检查指定消息代码的区域设置独立公共消息。获取配置文件中的消息主体        Properties commonMessages = getCommonMessages();        if (commonMessages != null) {            String commonMessage = commonMessages.getProperty(code);            if (commonMessage != null) {                return formatMessage(commonMessage, args, locale);            }        }        //未找到,则尝试使用父消息源解析消息(如果存在)        return getMessageFromParent(code, argsToUse, locale);    }    //尝试从父MessageSource(如果有)检索给定的消息。    protected String getMessageFromParent(String code, Object[] args, Locale locale) {        MessageSource parent = getParentMessageSource();        if (parent != null) {            if (parent instanceof AbstractMessageSource) {                // Call internal method to avoid getting the default code back                // in case of "useCodeAsDefaultMessage" being activated.                return ((AbstractMessageSource) parent).getMessageInternal(code, args, locale);            }            else {                // Check parent MessageSource, returning null if not found there.                return parent.getMessage(code, args, null, locale);            }        }        // Not found in parent either.        return null;    }    //返回默认消息。    protected String getDefaultMessage(String code) {        if (isUseCodeAsDefaultMessage()) { //返回给定代码作为默认消息。            return code;        }        return null;    }    //通过给定的参数数组搜索,找到任何MessageSourceResolvable对象并解析它们。    @Override    protected Object[] resolveArguments(Object[] args, Locale locale) {        if (args == null) {            return new Object[0];        }        List<Object> resolvedArgs = new ArrayList<Object>(args.length);        for (Object arg : args) {            if (arg instanceof MessageSourceResolvable) {                resolvedArgs.add(getMessage((MessageSourceResolvable) arg, locale));            }            else {                resolvedArgs.add(arg);            }        }        return resolvedArgs.toArray(new Object[resolvedArgs.size()]);    }    //解析不带参数的消息    protected String resolveCodeWithoutArguments(String code, Locale locale) {        MessageFormat messageFormat = resolveCode(code, locale);        if (messageFormat != null) {            synchronized (messageFormat) {                return messageFormat.format(new Object[0]);            }        }        return null;    }    //返回”消息格式化组件“(子类必须实现此方法来解决消息)    protected abstract MessageFormat resolveCode(String code, Locale locale);}

StaticMessageSource

//实现类一:主要用于程序测试,它允许通过编程的方式提供国际化信息。public class StaticMessageSource extends AbstractMessageSource {    //从'code + locale'键映射到消息字符串    private final Map<String, String> messages = new HashMap<String, String>();    private final Map<String, MessageFormat> cachedMessageFormats = new HashMap<String, MessageFormat>();    @Override    protected String resolveCodeWithoutArguments(String code, Locale locale) {        return this.messages.get(code + "_" + locale.toString());    }    @Override    protected MessageFormat resolveCode(String code, Locale locale) {        String key = code + "_" + locale.toString();        //如果为手动关联的消息串,直接返回null        String msg = this.messages.get(key);        if (msg == null) {            return null;        }        synchronized (this.cachedMessageFormats) {            MessageFormat messageFormat = this.cachedMessageFormats.get(key);            if (messageFormat == null) {                //构建“消息格式组件”                messageFormat = createMessageFormat(msg, locale);                this.cachedMessageFormats.put(key, messageFormat);            }            return messageFormat;        }    }    //将给定的消息与给定的代码相关联。    public void addMessage(String code, Locale locale, String msg) {        Assert.notNull(code, "Code must not be null");        Assert.notNull(locale, "Locale must not be null");        Assert.notNull(msg, "Message must not be null");        this.messages.put(code + "_" + locale.toString(), msg);        if (logger.isDebugEnabled()) {            logger.debug("Added message [" + msg + "] for code [" + code + "] and Locale [" + locale + "]");        }    }    //批量将给定的消息与给定的代码相关联。    public void addMessages(Map<String, String> messages, Locale locale) {        Assert.notNull(messages, "Messages Map must not be null");        for (Map.Entry<String, String> entry : messages.entrySet()) {            addMessage(entry.getKey(), locale, entry.getValue());        }    }    @Override    public String toString() {        return getClass().getName() + ": " + this.messages;    }}

ResourceBundleMessageSource

//实现类二:该实现类允许用户通过beanName指定一个资源名(包括类路径的全限定资源名),或通过beanNames指定一组资源名。//使用方式://<bean id="myResource"   lass="org.springframework.context.support. ReloadableResourceBundleMessageSource">  // <property name="basenames">  //     <list>  //       <value>com/baobaotao/i18n/fmt_resource</value>  //     </list>  //  </property>  //</bean>  public class ResourceBundleMessageSource extends AbstractMessageSource implements BeanClassLoaderAware {    //资源文件列表(包括类路径的全限定资源名)    private String[] basenames = new String[0];    //用于解析资源束文件的默认字符集。    private String defaultEncoding;    //是否使用系统默认的编码    private boolean fallbackToSystemLocale = true;    //缓存时间    private long cacheMillis = -1;    //加载绑定资源文件的类加载器    private ClassLoader bundleClassLoader;    private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();    //缓存code和区域所对应 资源包ResourceBundles。    private final Map<String, Map<Locale, ResourceBundle>> cachedResourceBundles =            new HashMap<String, Map<Locale, ResourceBundle>>();    //缓存ResourceBundle和code及local所对应的MessageFormat    private final Map<ResourceBundle, Map<String, Map<Locale, MessageFormat>>> cachedBundleMessageFormats =            new HashMap<ResourceBundle, Map<String, Map<Locale, MessageFormat>>>();    //设置资源文件    public void setBasename(String basename) {        setBasenames(basename);    }    //批量设置资源文件    public void setBasenames(String... basenames) {        if (basenames != null) {            this.basenames = new String[basenames.length];            for (int i = 0; i < basenames.length; i++) {                String basename = basenames[i];                Assert.hasText(basename, "Basename must not be empty");                this.basenames[i] = basename.trim();            }        }        else {            this.basenames = new String[0];        }    }    //设置用于解析绑定的资源文件的默认字符集。    public void setDefaultEncoding(String defaultEncoding) {        this.defaultEncoding = defaultEncoding;    }    //设置是否返回到系统区域设置,如果没有找到特定语言环境的文件。     // 默认为“true”; 如果这是关闭的,唯一的后备将是默认文件(例如,basename“messages”的“messages.properties”)。    public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) {        this.fallbackToSystemLocale = fallbackToSystemLocale;    }    //设置缓存加载的绑定的资源文件的秒数。    public void setCacheSeconds(int cacheSeconds) {        this.cacheMillis = (cacheSeconds * 1000);    }    //设置记载绑定资源文件的类加载器    public void setBundleClassLoader(ClassLoader classLoader) {        this.bundleClassLoader = classLoader;    }    protected ClassLoader getBundleClassLoader() {        return (this.bundleClassLoader != null ? this.bundleClassLoader : this.beanClassLoader);    }    //设置bean类加载器    public void setBeanClassLoader(ClassLoader classLoader) {        this.beanClassLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());    }    /**     * 将给定的消息代码解析为已注册资源包中的key,按原样返回捆绑包中的值(不使用MessageFormat解析)。     */    @Override    protected String resolveCodeWithoutArguments(String code, Locale locale) {        String result = null;        for (int i = 0; result == null && i < this.basenames.length; i++) {            ResourceBundle bundle = getResourceBundle(this.basenames[i], locale);            if (bundle != null) {                result = getStringOrNull(bundle, code);            }        }        return result;    }    /**     * 将给定的消息代码解析为注册资源包中的key,每个消息代码使用缓存的MessageFormat实例。     */    @Override    protected MessageFormat resolveCode(String code, Locale locale) {        MessageFormat messageFormat = null;        for (int i = 0; messageFormat == null && i < this.basenames.length; i++) {            ResourceBundle bundle = getResourceBundle(this.basenames[i], locale);            if (bundle != null) {                messageFormat = getMessageFormat(bundle, code, locale);            }        }        return messageFormat;    }    //为给定的basename和代码返回一个ResourceBundle,从缓存中提取已生成的MessageFormats。    protected ResourceBundle getResourceBundle(String basename, Locale locale) {        if (this.cacheMillis >= 0) {             // 新建ResourceBundle.getBundle调用,以使ResourceBundle执行其本地缓存,而不必花费更广泛的查找步骤。            return doGetBundle(basename, locale);        }        else {            // 永久缓存:在重复的getBundle调用中优先使用语言环境缓存。            synchronized (this.cachedResourceBundles) {                //先获取缓存                Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);                if (localeMap != null) {                    ResourceBundle bundle = localeMap.get(locale);                    if (bundle != null) {                        return bundle;                    }                }                //没找到缓存                try {                    ResourceBundle bundle = doGetBundle(basename, locale);                    if (localeMap == null) {                        localeMap = new HashMap<Locale, ResourceBundle>();                        this.cachedResourceBundles.put(basename, localeMap);                    }                    //放入缓存                    localeMap.put(locale, bundle);                    return bundle;                }                catch (MissingResourceException ex) {                    if (logger.isWarnEnabled()) {                        logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());                    }                    // Assume bundle not found                    // -> do NOT throw the exception to allow for checking parent message source.                    return null;                }            }        }    }    //获取给定basename和locale设置的资源包。    protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {        if ((this.defaultEncoding != null && !"ISO-8859-1".equals(this.defaultEncoding)) ||                !this.fallbackToSystemLocale || this.cacheMillis >= 0) {            //jdk版本不能小于16            if (JdkVersion.getMajorJavaVersion() < JdkVersion.JAVA_16) {                throw new IllegalStateException("Cannot use 'defaultEncoding', 'fallbackToSystemLocale' and " +                        "'cacheSeconds' on the standard ResourceBundleMessageSource when running on Java 5. " +                        "Consider using ReloadableResourceBundleMessageSource instead.");            }            return new ControlBasedResourceBundleFactory().getBundle(basename, locale);        }        else {            // Good old standard call...            return ResourceBundle.getBundle(basename, locale, getBundleClassLoader());        }    }    //为给定的包和代码返回一个MessageFormat,从缓存中提取已生成的MessageFormats。    protected MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale)            throws MissingResourceException {        synchronized (this.cachedBundleMessageFormats) {            //先走缓存            Map<String, Map<Locale, MessageFormat>> codeMap = this.cachedBundleMessageFormats.get(bundle);            Map<Locale, MessageFormat> localeMap = null;            if (codeMap != null) {                localeMap = codeMap.get(code);                if (localeMap != null) {                    MessageFormat result = localeMap.get(locale);                    if (result != null) {                        return result;                    }                }            }            //缓存为空的话,执行下面步骤            //获取资源包中指定key所对应的值            String msg = getStringOrNull(bundle, code);            if (msg != null) {                //放入缓存                if (codeMap == null) {                    codeMap = new HashMap<String, Map<Locale, MessageFormat>>();                    this.cachedBundleMessageFormats.put(bundle, codeMap);                }                if (localeMap == null) {                    localeMap = new HashMap<Locale, MessageFormat>();                    codeMap.put(code, localeMap);                }                MessageFormat result = createMessageFormat(msg, locale);                localeMap.put(locale, result);                return result;            }            return null;        }    }    //获取资源包中指定key所对应的值    private String getStringOrNull(ResourceBundle bundle, String key) {        try {            return bundle.getString(key);        }        catch (MissingResourceException ex) {            // Assume key not found            // -> do NOT throw the exception to allow for checking parent message source.            return null;        }    }    /**     * Show the configuration of this MessageSource.     */    @Override    public String toString() {        return getClass().getName() + ": basenames=[" +                StringUtils.arrayToCommaDelimitedString(this.basenames) + "]";    }    /**     * Factory indirection for runtime isolation of the optional dependencv on     * Java 6's Control class.     * @see ResourceBundle#getBundle(String, java.util.Locale, ClassLoader, java.util.ResourceBundle.Control)     * @see MessageSourceControl     */    private class ControlBasedResourceBundleFactory {        public ResourceBundle getBundle(String basename, Locale locale) {            return ResourceBundle.getBundle(basename, locale, getBundleClassLoader(), new MessageSourceControl());        }    }    /**     * Custom implementation of Java 6's {@code ResourceBundle.Control},     * adding support for custom file encodings, deactivating the fallback to the     * system locale and activating ResourceBundle's native cache, if desired.     */    private class MessageSourceControl extends ResourceBundle.Control {        @Override        public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)                throws IllegalAccessException, InstantiationException, IOException {            if (format.equals("java.properties")) {                //按不同区域获取所对应的不同资源文件名称                String bundleName = toBundleName(baseName, locale);                final String resourceName = toResourceName(bundleName, "properties");                final ClassLoader classLoader = loader;                final boolean reloadFlag = reload;                InputStream stream;                try {                    stream = AccessController.doPrivileged(                            new PrivilegedExceptionAction<InputStream>() {                                public InputStream run() throws IOException {                                    InputStream is = null;                                    if (reloadFlag) {                                        URL url = classLoader.getResource(resourceName);                                        if (url != null) {                                            URLConnection connection = url.openConnection();                                            if (connection != null) {                                                connection.setUseCaches(false);                                                is = connection.getInputStream();                                            }                                        }                                    }                                    else {                                        is = classLoader.getResourceAsStream(resourceName);                                    }                                    return is;                                }                            });                }                catch (PrivilegedActionException ex) {                    throw (IOException) ex.getException();                }                if (stream != null) {                    try {                        return (defaultEncoding != null ?                                new PropertyResourceBundle(new InputStreamReader(stream, defaultEncoding)) :                                new PropertyResourceBundle(stream));                    }                    finally {                        stream.close();                    }                }                else {                    return null;                }            }            else {                return super.newBundle(baseName, locale, format, loader, reload);            }        }        @Override        public Locale getFallbackLocale(String baseName, Locale locale) {            return (fallbackToSystemLocale ? super.getFallbackLocale(baseName, locale) : null);        }        @Override        public long getTimeToLive(String baseName, Locale locale) {            return (cacheMillis >= 0 ? cacheMillis : super.getTimeToLive(baseName, locale));        }        @Override        public boolean needsReload(String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime) {            if (super.needsReload(baseName, locale, format, loader, bundle, loadTime)) {                cachedBundleMessageFormats.remove(bundle);                return true;            }            else {                return false;            }        }    }}

ReloadableResourceBundleMessageSource

//实现类三:该实现类允许用户通过beanName指定一个资源名(包括类路径的全限定资源名),或通过beanNames指定一组资源名。//使用方式://<bean id="myResource"   lass="org.springframework.context.support. ReloadableResourceBundleMessageSource">  // <property name="basenames">  //     <list>  //       <value>com/baobaotao/i18n/fmt_resource</value>  //     </list>  //  </property>  //  <!--① 刷新资源文件的周期,以秒为单位-->  //  <property name="cacheSeconds" value="5"/>   //</bean>  public class ReloadableResourceBundleMessageSource extends AbstractMessageSource        implements ResourceLoaderAware {    private static final String PROPERTIES_SUFFIX = ".properties";    private static final String XML_SUFFIX = ".xml";    //资源文件包名    private String[] basenames = new String[0];    //默认编码方式    private String defaultEncoding;    //不同文件设置不同的编码方式    private Properties fileEncodings;    //使用系统区域设置    private boolean fallbackToSystemLocale = true;    //缓存时间    private long cacheMillis = -1;    private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();    private ResourceLoader resourceLoader = new DefaultResourceLoader();    /** 文件名-区域 所对应的配置文件名 */    private final Map<String, Map<Locale, List<String>>> cachedFilenames =            new HashMap<String, Map<Locale, List<String>>>();    /** 缓存来保存每个文件名已经加载的属性 */    private final Map<String, PropertiesHolder> cachedProperties = new HashMap<String, PropertiesHolder>();    /** 缓存以保存每个区域的合并加载属性*/    private final Map<Locale, PropertiesHolder> cachedMergedProperties = new HashMap<Locale, PropertiesHolder>();    public void setBasename(String basename) {        setBasenames(basename);    }    public void setBasenames(String... basenames) {        if (basenames != null) {            this.basenames = new String[basenames.length];            for (int i = 0; i < basenames.length; i++) {                String basename = basenames[i];                Assert.hasText(basename, "Basename must not be empty");                this.basenames[i] = basename.trim();            }        }        else {            this.basenames = new String[0];        }    }    public void setDefaultEncoding(String defaultEncoding) {        this.defaultEncoding = defaultEncoding;    }    public void setFileEncodings(Properties fileEncodings) {        this.fileEncodings = fileEncodings;    }    public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) {        this.fallbackToSystemLocale = fallbackToSystemLocale;    }    //过期时间设置    public void setCacheSeconds(int cacheSeconds) {        this.cacheMillis = (cacheSeconds * 1000);    }    //设置PropertiesPersister用于解析属性文件。    public void setPropertiesPersister(PropertiesPersister propertiesPersister) {        this.propertiesPersister =                (propertiesPersister != null ? propertiesPersister : new DefaultPropertiesPersister());    }    public void setResourceLoader(ResourceLoader resourceLoader) {        this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader());    }    /**     * 将给定的消息代码解析为检索到的包文件中的key,按原样返回包中找到的值(不使用MessageFormat解析)。     */    @Override    protected String resolveCodeWithoutArguments(String code, Locale locale) {        if (this.cacheMillis < 0) {            PropertiesHolder propHolder = getMergedProperties(locale);            String result = propHolder.getProperty(code);            if (result != null) {                return result;            }        }        else {            for (String basename : this.basenames) {                List<String> filenames = calculateAllFilenames(basename, locale);                for (String filename : filenames) {                    PropertiesHolder propHolder = getProperties(filename);                    String result = propHolder.getProperty(code);                    if (result != null) {                        return result;                    }                }            }        }        return null;    }    /**     * 将给定的消息代码解析为检索到的包文件中的key,每个消息代码使用缓存的MessageFormat实例。     */    @Override    protected MessageFormat resolveCode(String code, Locale locale) {        if (this.cacheMillis < 0) {             PropertiesHolder propHolder = getMergedProperties(locale);            MessageFormat result = propHolder.getMessageFormat(code, locale);            if (result != null) {                return result;            }        }        else {            //遍历资源包,生成key所对应的messageFormat            for (String basename : this.basenames) {                List<String> filenames = calculateAllFilenames(basename, locale);                for (String filename : filenames) {                    PropertiesHolder propHolder = getProperties(filename);                    MessageFormat result = propHolder.getMessageFormat(code, locale);                    if (result != null) {                        return result;                    }                }            }        }        return null;    }    /**     * 获取locale所对应的持有properties对象     */    protected PropertiesHolder getMergedProperties(Locale locale) {        synchronized (this.cachedMergedProperties) {            //先走缓存            PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale);            if (mergedHolder != null) {                return mergedHolder;            }            //创建一个新的properties对象            Properties mergedProps = new Properties();            //创建一个的持有properties的对象            mergedHolder = new PropertiesHolder(mergedProps, -1);            for (int i = this.basenames.length - 1; i >= 0; i--) {//遍历资源包名列表                //获取该区域该资源包名所对应的资源文件列表                List filenames = calculateAllFilenames(this.basenames[i], locale);                for (int j = filenames.size() - 1; j >= 0; j--) {                    String filename = (String) filenames.get(j);                    PropertiesHolder propHolder = getProperties(filename);                    if (propHolder.getProperties() != null) {                        mergedProps.putAll(propHolder.getProperties());                    }                }            }            this.cachedMergedProperties.put(locale, mergedHolder);            return mergedHolder;        }    }    /**     * 计算给定的捆绑包基础名称和区域设置的所有文件名。 将计算给定区域设置的文件名,系统区域设置(如果适用)和默认文件。     * @param basename the basename of the bundle     * @param locale the locale     * @return the List of filenames to check     * @see #setFallbackToSystemLocale     * @see #calculateFilenamesForLocale     */    protected List<String> calculateAllFilenames(String basename, Locale locale) {        synchronized (this.cachedFilenames) {            Map<Locale, List<String>> localeMap = this.cachedFilenames.get(basename);            if (localeMap != null) {                List<String> filenames = localeMap.get(locale);                if (filenames != null) {                    return filenames;                }            }            List<String> filenames = new ArrayList<String>(7);            filenames.addAll(calculateFilenamesForLocale(basename, locale));            if (this.fallbackToSystemLocale && !locale.equals(Locale.getDefault())) {                List<String> fallbackFilenames = calculateFilenamesForLocale(basename, Locale.getDefault());                for (String fallbackFilename : fallbackFilenames) {                    if (!filenames.contains(fallbackFilename)) {                        // Entry for fallback locale that isn't already in filenames list.                        filenames.add(fallbackFilename);                    }                }            }            filenames.add(basename);            if (localeMap != null) {                localeMap.put(locale, filenames);            }            else {                localeMap = new HashMap<Locale, List<String>>();                localeMap.put(locale, filenames);                this.cachedFilenames.put(basename, localeMap);            }            return filenames;        }    }    /**     * Calculate the filenames for the given bundle basename and Locale,     * appending language code, country code, and variant code.     * E.g.: basename "messages", Locale "de_AT_oo" -> "messages_de_AT_OO",     * "messages_de_AT", "messages_de".     * <p>Follows the rules defined by {@link java.util.Locale#toString()}.     * @param basename the basename of the bundle     * @param locale the locale     * @return the List of filenames to check     */    protected List<String> calculateFilenamesForLocale(String basename, Locale locale) {        List<String> result = new ArrayList<String>(3);        String language = locale.getLanguage();        String country = locale.getCountry();        String variant = locale.getVariant();        StringBuilder temp = new StringBuilder(basename);        temp.append('_');        if (language.length() > 0) {            temp.append(language);            result.add(0, temp.toString());        }        temp.append('_');        if (country.length() > 0) {            temp.append(country);            result.add(0, temp.toString());        }        if (variant.length() > 0 && (language.length() > 0 || country.length() > 0)) {            temp.append('_').append(variant);            result.add(0, temp.toString());        }        return result;    }    //从缓存或新加载中获取给定文件名的PropertiesHolder。    protected PropertiesHolder getProperties(String filename) {        synchronized (this.cachedProperties) {            PropertiesHolder propHolder = this.cachedProperties.get(filename);            //判断是否需要刷新            if (propHolder != null &&                    (propHolder.getRefreshTimestamp() < 0 ||                     propHolder.getRefreshTimestamp() > System.currentTimeMillis() - this.cacheMillis)) {                // up to date                return propHolder;            }            return refreshProperties(filename, propHolder);        }    }    //刷新给定包文件名的PropertiesHolder。     protected PropertiesHolder refreshProperties(String filename, PropertiesHolder propHolder) {        //记录刷新时间        long refreshTimestamp = (this.cacheMillis < 0 ? -1 : System.currentTimeMillis());        //加载资源文件所对应的resource对象        Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX);        if (!resource.exists()) { //如果resource不存在,则加载对应的xml文件            resource = this.resourceLoader.getResource(filename + XML_SUFFIX);        }        if (resource.exists()) {            long fileTimestamp = -1;            if (this.cacheMillis >= 0) {                // 如果设置了缓存时间,文件的最后修改的时间戳将被读取。                try {                    //最近一次操作时间                    fileTimestamp = resource.lastModified();                    if (propHolder != null && propHolder.getFileTimestamp() == fileTimestamp) {                        if (logger.isDebugEnabled()) {                            logger.debug("Re-caching properties for filename [" + filename + "] - file hasn't been modified");                        }                        //设置刷新时间                        propHolder.setRefreshTimestamp(refreshTimestamp);                        return propHolder;                    }                }                catch (IOException ex) {                    // Probably a class path resource: cache it forever.                    if (logger.isDebugEnabled()) {                        logger.debug(                                resource + " could not be resolved in the file system - assuming that is hasn't changed", ex);                    }                    fileTimestamp = -1;                }            }            try {                //从给定的resource中加载properties                Properties props = loadProperties(resource, filename);                propHolder = new PropertiesHolder(props, fileTimestamp);            }            catch (IOException ex) {                if (logger.isWarnEnabled()) {                    logger.warn("Could not parse properties file [" + resource.getFilename() + "]", ex);                }                // 加载报错,创建一个空的PropertiesHolder对象                propHolder = new PropertiesHolder();            }        }        else {            // Resource 不存在            if (logger.isDebugEnabled()) {                logger.debug("No properties file found for [" + filename + "] - neither plain properties nor XML");            }            // 创建一个空的PropertiesHolder对象            propHolder = new PropertiesHolder();        }        //设置刷新时间        propHolder.setRefreshTimestamp(refreshTimestamp);        //放入缓存        this.cachedProperties.put(filename, propHolder);        return propHolder;    }    /**     * 解析给定的resource资源中返回对应properties对象     * @param resource the resource to load from     * @param filename the original bundle filename (basename + Locale)     * @return the populated Properties instance     * @throws IOException if properties loading failed     */    protected Properties loadProperties(Resource resource, String filename) throws IOException {        InputStream is = resource.getInputStream();        Properties props = new Properties();        try {            //xml资源文件            if (resource.getFilename().endsWith(XML_SUFFIX)) {                if (logger.isDebugEnabled()) {                    logger.debug("Loading properties [" + resource.getFilename() + "]");                }                //解析xml文件                this.propertiesPersister.loadFromXml(props, is);            }            else {                //优先取filename所对应编码设置,没有则取默认设置                String encoding = null;                if (this.fileEncodings != null) {                    encoding = this.fileEncodings.getProperty(filename);                }                if (encoding == null) {                    encoding = this.defaultEncoding;                }                if (encoding != null) {                    if (logger.isDebugEnabled()) {                        logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'");                    }                    this.propertiesPersister.load(props, new InputStreamReader(is, encoding));                }                else {                    if (logger.isDebugEnabled()) {                        logger.debug("Loading properties [" + resource.getFilename() + "]");                    }                    this.propertiesPersister.load(props, is);                }            }            return props;        }        finally {            is.close();        }    }    /**     * 清除所有资源包对应的properties缓存。     */    public void clearCache() {        logger.debug("Clearing entire resource bundle cache");        synchronized (this.cachedProperties) {            this.cachedProperties.clear();        }        synchronized (this.cachedMergedProperties) {            this.cachedMergedProperties.clear();        }    }    /**     * 清除此MessageSource及其所有父资源的缓存。     * @see #clearCache     */    public void clearCacheIncludingAncestors() {        clearCache();        if (getParentMessageSource() instanceof ReloadableResourceBundleMessageSource) {            ((ReloadableResourceBundleMessageSource) getParentMessageSource()).clearCacheIncludingAncestors();        }    }    @Override    public String toString() {        return getClass().getName() + ": basenames=[" + StringUtils.arrayToCommaDelimitedString(this.basenames) + "]";    }    /**     * PropertiesHolder for caching.     * 存储源文件的最后修改的时间戳以进行有效的更改检测,以及上次刷新尝试的时间戳(每次缓存条目重新验证时更新)。     */    protected class PropertiesHolder {        //所持有properties对象        private Properties properties;        //源文件的最后修改的时间戳        private long fileTimestamp = -1;        //刷新时间        private long refreshTimestamp = -1;        /** 缓存:每个code已经生成的各区域所对应MessageFormats */        private final Map<String, Map<Locale, MessageFormat>> cachedMessageFormats =                new HashMap<String, Map<Locale, MessageFormat>>();        public PropertiesHolder(Properties properties, long fileTimestamp) {            this.properties = properties;            this.fileTimestamp = fileTimestamp;        }        public PropertiesHolder() {        }        public Properties getProperties() {            return properties;        }        public long getFileTimestamp() {            return fileTimestamp;        }        public void setRefreshTimestamp(long refreshTimestamp) {            this.refreshTimestamp = refreshTimestamp;        }        public long getRefreshTimestamp() {            return refreshTimestamp;        }        public String getProperty(String code) {            if (this.properties == null) {                return null;            }            return this.properties.getProperty(code);        }        public MessageFormat getMessageFormat(String code, Locale locale) {            if (this.properties == null) {                return null;            }            synchronized (this.cachedMessageFormats) {                //先尝试走缓存                Map<Locale, MessageFormat> localeMap = this.cachedMessageFormats.get(code);                if (localeMap != null) {                    MessageFormat result = localeMap.get(locale);                    if (result != null) {                        return result;                    }                }                //缓存没有,则properties中code所对应的值                String msg = this.properties.getProperty(code);                if (msg != null) {                    if (localeMap == null) {                        localeMap = new HashMap<Locale, MessageFormat>();                        this.cachedMessageFormats.put(code, localeMap);                    }                    //构建 MessageFormat 放入缓存                    MessageFormat result = createMessageFormat(msg, locale);                    localeMap.put(locale, result);                    return result;                }                return null;            }        }    }}

MessageFormat 消息格式化组件

这个类的功能比较强大,主要就是将消息串、参数数组格式化成字符串。例如:“{0}去上学”和参数“小明”,格式化成:“小明去上学”!

//’消息格式化组件‘public class MessageFormat extends Format {    private static final long serialVersionUID = 6479157306784022952L;    private int maxOffset = -1;    //构造方法    public MessageFormat(String pattern) {        this.locale = Locale.getDefault(Locale.Category.FORMAT);        applyPattern(pattern);    }    //构造方法    public MessageFormat(String pattern, Locale locale) {        this.locale = locale;        applyPattern(pattern);    }    //消息区域设置    public void setLocale(Locale locale) {        this.locale = locale;    }    //获取消息区域    public Locale getLocale() {        return locale;    }    //格式化    public final String format (Object obj) {        return format(obj, new StringBuffer(), new FieldPosition(0)).toString();    }    public final StringBuffer format(Object arguments, StringBuffer result,                                     FieldPosition pos)    {        return subformat((Object[]) arguments, result, pos, null);    }    //======省略很多代码^_^========/}
阅读全文
0 0
原创粉丝点击