เข้าถึงไฟล์คุณสมบัติโดยทางโปรแกรมด้วย Spring?


139

เราใช้โค้ดด้านล่างเพื่อฉีด Spring beans ด้วยคุณสมบัติจากไฟล์คุณสมบัติ

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations" value="classpath:/my.properties"/>
</bean>

<bean id="blah" class="abc">
    <property name="path" value="${the.path}"/>
</bean>

มีวิธีที่เราสามารถเข้าถึงคุณสมบัติโดยทางโปรแกรมหรือไม่? ฉันพยายามทำโค้ดโดยไม่ต้องพึ่งพาการฉีด ดังนั้นฉันต้องการเพียงแค่มีโค้ดดังนี้:

PropertyPlaceholderConfigurer props = new PropertyPlaceholderConfigurer();
props.load("classpath:/my.properties");
props.get("path");

ตัวอย่างที่สมบูรณ์ของการเข้าถึงไฟล์คุณสมบัติในฤดูใบไม้ผลิอยู่ที่ลิงค์ต่อไปนี้: bharatonjava.wordpress.com/2012/08/24/…

คำตอบ:


175

วิธีการเกี่ยวกับPropertiesLoaderUtils ?

Resource resource = new ClassPathResource("/my.properties");
Properties props = PropertiesLoaderUtils.loadProperties(resource);

6
นี่คือคำถามสิ่งนี้แตกต่างจากของฉันอย่างไรและได้รับการโหวตอีกสองครั้งและโพสต์ที่สอง ...
Zoidberg

3
เต้นฉันฉันไม่ได้ลงคะแนน :) ฉันจะไม่ใช้ a PropertyPlaceholderConfigurerแม้ว่ามันจะมากเกินไปสำหรับงานนี้
skaffman

5
ฉันพยายามเข้าใกล้สิ่งที่เขามีให้มากที่สุดฉันถูกลดคะแนนหลายครั้งที่ให้รายละเอียดไม่เพียงพอ อย่างไรก็ตามคำตอบของคุณสมควรได้รับการโหวตเพราะถูกต้องฉันคิดว่าฉันแค่อิจฉาฉันไม่ได้รับ 2 คะแนนเช่นกันฮ่า ๆ
Zoidberg

2
เราควรให้อะไรในเส้นทางถ้าไฟล์ถูกวางไว้ในไดเร็กทอรีภายนอกสมมติว่าโฟลเดอร์ config?
prnjn

55

หากสิ่งที่คุณต้องการทำคือเข้าถึงค่าตัวยึดจากรหัสมี@Valueคำอธิบายประกอบ:

@Value("${settings.some.property}")
String someValue;

ในการเข้าถึงตัวยึดตำแหน่งจาก SPEL ให้ใช้ไวยากรณ์นี้:

#('${settings.some.property}')

หากต้องการแสดงการกำหนดค่าให้กับมุมมองที่ปิด SPEL คุณสามารถใช้เคล็ดลับนี้:

package com.my.app;

import java.util.Collection;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.stereotype.Component;

@Component
public class PropertyPlaceholderExposer implements Map<String, String>, BeanFactoryAware {  
    ConfigurableBeanFactory beanFactory; 

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = (ConfigurableBeanFactory) beanFactory;
    }

    protected String resolveProperty(String name) {
        String rv = beanFactory.resolveEmbeddedValue("${" + name + "}");

        return rv;
    }

    @Override
    public String get(Object key) {
        return resolveProperty(key.toString());
    }

    @Override
    public boolean containsKey(Object key) {
        try {
            resolveProperty(key.toString());
            return true;
        }
        catch(Exception e) {
            return false;
        }
    }

    @Override public boolean isEmpty() { return false; }
    @Override public Set<String> keySet() { throw new UnsupportedOperationException(); }
    @Override public Set<java.util.Map.Entry<String, String>> entrySet() { throw new UnsupportedOperationException(); }
    @Override public Collection<String> values() { throw new UnsupportedOperationException(); }
    @Override public int size() { throw new UnsupportedOperationException(); }
    @Override public boolean containsValue(Object value) { throw new UnsupportedOperationException(); }
    @Override public void clear() { throw new UnsupportedOperationException(); }
    @Override public String put(String key, String value) { throw new UnsupportedOperationException(); }
    @Override public String remove(Object key) { throw new UnsupportedOperationException(); }
    @Override public void putAll(Map<? extends String, ? extends String> t) { throw new UnsupportedOperationException(); }
}

จากนั้นใช้เอ็กซ์โปเซอร์เพื่อแสดงคุณสมบัติให้กับมุมมอง:

<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver" id="tilesViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.tiles2.TilesView"/>
    <property name="attributesMap">
        <map>
            <entry key="config">
                <bean class="com.my.app.PropertyPlaceholderExposer" />
            </entry>
        </map>
    </property>
</bean>

จากนั้นให้ใช้คุณสมบัติที่เปิดเผยดังนี้:

${config['settings.some.property']}

โซลูชันนี้มีข้อได้เปรียบที่คุณสามารถพึ่งพาการใช้ตัวยึดตำแหน่งมาตรฐานที่แทรกโดยบริบท: แท็กตัวยึดคุณสมบัติ

ตอนนี้เป็นบันทึกสุดท้ายหากคุณต้องการจับคุณสมบัติตัวยึดตำแหน่งทั้งหมดและค่าของคุณสมบัติเหล่านั้นคุณต้องไปป์ผ่าน StringValueResolver เพื่อให้แน่ใจว่าตัวยึดตำแหน่งทำงานภายในค่าคุณสมบัติตามที่คาดไว้ รหัสต่อไปนี้จะทำเช่นนั้น

package com.my.app;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.util.StringValueResolver;

public class AppConfig extends PropertyPlaceholderConfigurer implements Map<String, String> {

    Map<String, String> props = new HashMap<String, String>();

    @Override
    protected void processProperties(ConfigurableListableBeanFactory beanFactory, Properties props)
            throws BeansException {

        this.props.clear();
        for (Entry<Object, Object> e: props.entrySet())
            this.props.put(e.getKey().toString(), e.getValue().toString());

        super.processProperties(beanFactory, props);
    }

    @Override
    protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
            StringValueResolver valueResolver) {

        super.doProcessProperties(beanFactoryToProcess, valueResolver);

        for(Entry<String, String> e: props.entrySet())
            e.setValue(valueResolver.resolveStringValue(e.getValue()));
    }

    // Implement map interface to access stored properties
    @Override public Set<String> keySet() { return props.keySet(); }
    @Override public Set<java.util.Map.Entry<String, String>> entrySet() { return props.entrySet(); }
    @Override public Collection<String> values() { return props.values(); }
    @Override public int size() { return props.size(); }
    @Override public boolean isEmpty() { return props.isEmpty(); }
    @Override public boolean containsValue(Object value) { return props.containsValue(value); }
    @Override public boolean containsKey(Object key) { return props.containsKey(key); }
    @Override public String get(Object key) { return props.get(key); }
    @Override public void clear() { throw new UnsupportedOperationException(); }
    @Override public String put(String key, String value) { throw new UnsupportedOperationException(); }
    @Override public String remove(Object key) { throw new UnsupportedOperationException(); }
    @Override public void putAll(Map<? extends String, ? extends String> t) { throw new UnsupportedOperationException(); }
}

ขอบคุณสำหรับคำตอบที่สมบูรณ์มาก! มีวิธีดำเนินการกับช่องสุดท้ายหรือไม่
วอร์ด

2
@WardC คุณไม่สามารถฉีดเข้าไปในสนามสุดท้ายได้ อย่างไรก็ตามคุณสามารถฉีดเข้าไปในอาร์กิวเมนต์ตัวสร้างและตั้งค่าฟิลด์สุดท้ายภายในตัวสร้าง ดูstackoverflow.com/questions/2306078/…และstackoverflow.com/questions/4203302/…
anttix

50

CREDIT : การเข้าถึงคุณสมบัติแบบเป็นโปรแกรมใน Spring โดยไม่ต้องอ่านไฟล์คุณสมบัติซ้ำ

ฉันพบการใช้งานที่ดีในการเข้าถึงคุณสมบัติทางโปรแกรมในฤดูใบไม้ผลิโดยไม่ต้องโหลดคุณสมบัติเดียวกันกับที่สปริงโหลดไปแล้ว [นอกจากนี้ไม่จำเป็นต้องฮาร์ดโค้ดตำแหน่งไฟล์คุณสมบัติในแหล่งที่มา]

ด้วยการเปลี่ยนแปลงเหล่านี้โค้ดจะดูสะอาดและบำรุงรักษาได้มากขึ้น

แนวคิดค่อนข้างเรียบง่าย เพียงแค่ขยายตัวยึดคุณสมบัติเริ่มต้นของสปริง (PropertyPlaceholderConfigurer) และจับคุณสมบัติที่โหลดในตัวแปรโลคัล

public class SpringPropertiesUtil extends PropertyPlaceholderConfigurer {

    private static Map<String, String> propertiesMap;
    // Default as in PropertyPlaceholderConfigurer
    private int springSystemPropertiesMode = SYSTEM_PROPERTIES_MODE_FALLBACK;

    @Override
    public void setSystemPropertiesMode(int systemPropertiesMode) {
        super.setSystemPropertiesMode(systemPropertiesMode);
        springSystemPropertiesMode = systemPropertiesMode;
    }

    @Override
    protected void processProperties(ConfigurableListableBeanFactory beanFactory, Properties props) throws BeansException {
        super.processProperties(beanFactory, props);

        propertiesMap = new HashMap<String, String>();
        for (Object key : props.keySet()) {
            String keyStr = key.toString();
            String valueStr = resolvePlaceholder(keyStr, props, springSystemPropertiesMode);
            propertiesMap.put(keyStr, valueStr);
        }
    }

    public static String getProperty(String name) {
        return propertiesMap.get(name).toString();
    }

}

ตัวอย่างการใช้งาน

SpringPropertiesUtil.getProperty("myProperty")

การเปลี่ยนแปลงการกำหนดค่าสปริง

<bean id="placeholderConfigMM" class="SpringPropertiesUtil">
    <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE"/>
    <property name="locations">
    <list>
        <value>classpath:myproperties.properties</value>
    </list>
    </property>
</bean>

หวังว่านี่จะช่วยแก้ปัญหาที่คุณมี


8
นี่ไม่ใช่การใช้งานเต็มรูปแบบและจะทำงานไม่ถูกต้อง PropertyPlaceholderConfigurer ใช้ PropertyPlaceholderHelper เพื่อแทนที่คุณสมบัติตัวยึดทั้งหมดรวมถึงตัวยึดตำแหน่งที่ซ้อนกัน ในการใช้งานของ Kalinga หากคุณมี myFile = $ {myFolder} /myFile.txt ค่าคุณสมบัติตามตัวอักษรที่คุณจะได้รับจากแผนที่โดยใช้คีย์ "myFile" จะเป็น $ {myFolder} /myFile.txt

1
นี่คือวิธีแก้ปัญหาที่ถูกต้อง เพื่อจัดการกับความกังวลของ Brian $ {myFolder} ควรเป็นคุณสมบัติของระบบและไม่อยู่ในไฟล์คุณสมบัติ สิ่งนี้สามารถแก้ไขได้โดยการตั้งค่าคุณสมบัติระบบ tomcat หรือรันคุณสมบัติใน eclipse คุณอาจสามารถมีคุณสมบัติในการสร้าง การแก้ปัญหานี้สมมติว่ามีเพียงเล็กน้อยและควรตอบ แต่ในขณะเดียวกันคำตอบนี้ยังมีอีกมากตามแนวปฏิบัติมาตรฐานในการโหลดคุณสมบัติสปริงและจาวาในที่เดียวแทนที่จะแยกกัน อีกทางเลือกหนึ่งคือการโหลดไฟล์คุณสมบัติทั่วไปด้วย myFile ในไฟล์และใช้ไฟล์นั้นเพื่อรับส่วนที่เหลือ
Rob

2
ฉันพยายามใช้วิธีแก้ปัญหานี้กับ 'PropertySourcesPlaceholderConfigurer' ใหม่จาก Spring 3.1+ แต่ฉันพบว่า method processProperties (ConfigurableListableBeanFactory beanFactory, Properties props) เลิกใช้แล้วดังนั้นตอนนี้จึงไม่มีการเข้าถึงอาร์กิวเมนต์ 'props' การดูแหล่งที่มาของ PropertySourcesPlaceholderConfigurer ไม่พบวิธีที่สะอาดในการแสดงคุณสมบัติ มีความคิดที่จะทำหรือไม่? ขอบคุณ!
Jorge Palacio

48

ฉันได้ทำสิ่งนี้และได้ผล

Properties props = PropertiesLoaderUtils.loadAllProperties("my.properties");
PropertyPlaceholderConfigurer props2 = new PropertyPlaceholderConfigurer();
props2.setProperties(props);

ที่ควรจะทำงาน


26

คุณยังสามารถใช้เครื่องมือสปริงหรือโหลดคุณสมบัติผ่าน PropertiesFactoryBean

<util:properties id="myProps" location="classpath:com/foo/myprops.properties"/>

หรือ:

<bean id="myProps" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
  <property name="location" value="classpath:com/foo/myprops.properties"/>
</bean>

จากนั้นคุณสามารถเลือกได้ในใบสมัครของคุณด้วย:

@Resource(name = "myProps")
private Properties myProps;

และใช้คุณสมบัติเหล่านี้เพิ่มเติมในการกำหนดค่าของคุณ:

<context:property-placeholder properties-ref="myProps"/>

สิ่งนี้ยังอยู่ในเอกสาร: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#xsd-config-body-schemas-util-properties


10

สร้างชั้นเรียนเหมือนด้านล่าง

    package com.tmghealth.common.util;

    import java.util.Properties;

    import org.springframework.beans.BeansException;

    import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

    import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;

    import org.springframework.context.annotation.Configuration;

    import org.springframework.context.annotation.PropertySource;

    import org.springframework.stereotype.Component;


    @Component
    @Configuration
    @PropertySource(value = { "classpath:/spring/server-urls.properties" })
    public class PropertiesReader extends PropertyPlaceholderConfigurer {

        @Override
        protected void processProperties(
                ConfigurableListableBeanFactory beanFactory, Properties props)
                throws BeansException {
            super.processProperties(beanFactory, props);

        }

    }

จากนั้นทุกที่ที่คุณต้องการเข้าถึงการใช้คุณสมบัติ

    @Autowired
        private Environment environment;
    and getters and setters then access using 

    environment.getProperty(envName
                    + ".letter.fdi.letterdetails.restServiceUrl");

- เขียน getters และ setters ในคลาส accessor

    public Environment getEnvironment() {
            return environment;
        }`enter code here`

        public void setEnvironment(Environment environment) {
            this.environment = environment;
        }

1
คำตอบที่ดีที่สุดคือควรกำหนดค่าสภาพแวดล้อมอัตโนมัติ
sbochins

4

ดังที่คุณทราบว่าเวอร์ชันใหม่กว่าของ Spring ไม่ได้ใช้ PropertyPlaceholderConfigurer และตอนนี้ใช้โครงสร้างที่น่าหวาดเสียวอื่นที่เรียกว่า PropertySourcesPlaceholderConfigurer หากคุณกำลังพยายามแก้ไขคุณสมบัติจากโค้ดและหวังว่าทีม Spring จะให้วิธีการนี้กับเราเมื่อนานมาแล้วให้โหวตโพสต์นี้! ... เพราะนี่คือวิธีใหม่ของคุณ:

คุณสมบัติ Subclass PropertySourcesPlaceholderConfigurer:

public class SpringPropertyExposer extends PropertySourcesPlaceholderConfigurer {

    private ConfigurableListableBeanFactory factory;

    /**
     * Save off the bean factory so we can use it later to resolve properties
     */
    @Override
    protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
            final ConfigurablePropertyResolver propertyResolver) throws BeansException {
        super.processProperties(beanFactoryToProcess, propertyResolver);

        if (beanFactoryToProcess.hasEmbeddedValueResolver()) {
            logger.debug("Value resolver exists.");
            factory = beanFactoryToProcess;
        }
        else {
            logger.error("No existing embedded value resolver.");
        }
    }

    public String getProperty(String name) {
        Object propertyValue = factory.resolveEmbeddedValue(this.placeholderPrefix + name + this.placeholderSuffix);
        return propertyValue.toString();
    }
}

ในการใช้งานโปรดใช้คลาสย่อยของคุณใน @Configuration ของคุณและบันทึกข้อมูลอ้างอิงไว้เพื่อใช้ในภายหลัง

@Configuration
@ComponentScan
public class PropertiesConfig {

    public static SpringPropertyExposer commonEnvConfig;

    @Bean(name="commonConfig")
    public static PropertySourcesPlaceholderConfigurer commonConfig() throws IOException {
        commonEnvConfig = new SpringPropertyExposer(); //This is a subclass of the return type.
        PropertiesFactoryBean commonConfig = new PropertiesFactoryBean();
        commonConfig.setLocation(new ClassPathResource("META-INF/spring/config.properties"));
        try {
            commonConfig.afterPropertiesSet();
        }
        catch (IOException e) {
            e.printStackTrace();
            throw e;
        }
        commonEnvConfig.setProperties(commonConfig.getObject());
        return commonEnvConfig;
    }
}

การใช้งาน:

Object value = PropertiesConfig.commonEnvConfig.getProperty("key.subkey");


3

คุณสามารถรับคุณสมบัติของคุณผ่านEnvironmentชั้นเรียน ตามเอกสารหมายถึง:

คุณสมบัติมีบทบาทสำคัญในแอปพลิเคชันเกือบทั้งหมดและอาจมาจากแหล่งที่มาที่หลากหลาย: ไฟล์คุณสมบัติคุณสมบัติของระบบ JVM ตัวแปรสภาพแวดล้อมระบบ JNDI พารามิเตอร์บริบท servlet อ็อบเจ็กต์คุณสมบัติเฉพาะกิจแผนที่และอื่น ๆ บทบาทของอ็อบเจ็กต์สภาพแวดล้อมที่มีความสัมพันธ์กับคุณสมบัติคือจัดเตรียมอินเตอร์เฟสเซอร์วิสที่สะดวกแก่ผู้ใช้สำหรับการกำหนดคอนฟิกแหล่งคุณสมบัติและการแก้ไขคุณสมบัติจากคุณสมบัติเหล่านั้น

มี Environment เป็นenvตัวแปรเรียกง่ายๆว่า

env.resolvePlaceholders("${your-property:default-value}")

คุณสามารถรับคุณสมบัติ 'ดิบ' ของคุณผ่าน:

env.getProperty("your-property")

มันจะค้นหาแหล่งคุณสมบัติทั้งหมดที่ฤดูใบไม้ผลิได้ลงทะเบียนไว้

คุณสามารถรับสภาพแวดล้อมผ่าน:

  • ฉีด ApplicationContext โดยการนำไปใช้ApplicationContextAwareแล้วเรียกgetEnvironment()ใช้บริบท
  • EnvironmentAwareการดำเนินการ

ได้มาจากการใช้คลาสเนื่องจากคุณสมบัติได้รับการแก้ไขในช่วงเริ่มต้นของการเริ่มต้นแอปพลิเคชันเนื่องจากอาจจำเป็นสำหรับการสร้าง bean

อ่านเพิ่มเติมเกี่ยวกับเอกสาร: เอกสารเกี่ยวกับสภาพแวดล้อมฤดูใบไม้ผลิ


2

นี่คือตัวอย่างอื่น

XmlBeanFactory factory = new XmlBeanFactory(new FileSystemResource("beans.xml"));
PropertyPlaceholderConfigurer cfg = new PropertyPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));
cfg.postProcessBeanFactory(factory);

2

สิ่งนี้จะแก้ไขคุณสมบัติที่ซ้อนกัน

public class Environment extends PropertyPlaceholderConfigurer {

/**
 * Map that hold all the properties.
 */
private Map<String, String> propertiesMap; 

/**
 * Iterate through all the Property keys and build a Map, resolve all the nested values before building the map.
 */
@Override
protected void processProperties(ConfigurableListableBeanFactory beanFactory, Properties props) throws BeansException {
    super.processProperties(beanFactory, props);

    propertiesMap = new HashMap<String, String>();
    for (Object key : props.keySet()) {
        String keyStr = key.toString();
        String valueStr = beanFactory.resolveEmbeddedValue(placeholderPrefix + keyStr.trim() + DEFAULT_PLACEHOLDER_SUFFIX);
        propertiesMap.put(keyStr, valueStr);
    }
} 

/**
 * This method gets the String value for a given String key for the property files.
 * 
 * @param name - Key for which the value needs to be retrieved.
 * @return Value
 */
public String getProperty(String name) {
    return propertiesMap.get(name).toString();
}

1

โพสต์นี้ยังอธิบายคุณสมบัติการเข้าถึงวิธีการเข้าถึง: http://maciej-miklas.blogspot.de/2013/07/spring-31-programmatic-access-to.html

คุณสามารถเข้าถึงคุณสมบัติที่โหลดโดยตัวยึดคุณสมบัติสปริงเหนือถั่วสปริงดังกล่าว:

@Named
public class PropertiesAccessor {

    private final AbstractBeanFactory beanFactory;

    private final Map<String,String> cache = new ConcurrentHashMap<>();

    @Inject
    protected PropertiesAccessor(AbstractBeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    public  String getProperty(String key) {
        if(cache.containsKey(key)){
            return cache.get(key);
        }

        String foundProp = null;
        try {
            foundProp = beanFactory.resolveEmbeddedValue("${" + key.trim() + "}");
            cache.put(key,foundProp);
        } catch (IllegalArgumentException ex) {
           // ok - property was not found
        }

        return foundProp;
    }
}

0
create .properties file in classpath of your project and add path configuration in xml`<context:property-placeholder location="classpath*:/*.properties" />`

ใน servlet-context.xml หลังจากนั้นคุณสามารถใช้ไฟล์ของคุณได้ทุกที่


0

โปรดใช้รหัสด้านล่างในไฟล์การกำหนดค่าสปริงของคุณเพื่อโหลดไฟล์จากเส้นทางคลาสของแอปพลิเคชันของคุณ

 <context:property-placeholder
    ignore-unresolvable="true" ignore-resource-not-found="false" location="classpath:property-file-name" />

0

นี่เป็นวิธีที่ดีที่สุดที่ฉันได้รับ:

package your.package;

import java.io.IOException;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;

public class ApplicationProperties {

    private Properties properties;

    public ApplicationProperties() {
        // application.properties located at src/main/resource
        Resource resource = new ClassPathResource("/application.properties");
        try {
            this.properties = PropertiesLoaderUtils.loadProperties(resource);
        } catch (IOException ex) {
            Logger.getLogger(ApplicationProperties.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public String getProperty(String propertyName) {
        return this.properties.getProperty(propertyName);
    }
}

สร้างอินสแตนซ์คลาสและเรียกเมธอด obj.getProperty ("my.property.name");
Daniel Almeida
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.