스프링 캐시는 기본적으로 일반 애플리케이션의 캐시 요구 사항을 충족할 수 있지만 현실은 항상 복잡합니다. 사용자 볼륨이 증가하거나 성능이 따라잡을 수 없는 경우 항상 메모리를 확장해야 할 수 있습니다. 캐시는 고가용성을 지원하지 않고 데이터를 유지하는 기능도 없기 때문에 이때 캐싱 솔루션을 사용자 정의해야 합니다.
이 기사에서는 Spring 캐시와 Redis를 사용하여 통합하여 원하는 캐시를 얻습니다.
먼저 Redis를 구성해 보겠습니다.
첫 번째 단계는 Redis를 설치하는 것입니다. Baidu의 경우 주로 Redis를 구성합니다.
다음 디렉터리에 배치할 수 있는 redis 구성 파일을 추가하세요.
redis.host=192.168.0.43redis.port=6379redis.pass=2015redis.maxIdle=50redis.maxActive=50redis.maxWait=50redis.testOnBorrow=trueredis.timeout=1000
스프링 구성 파일에서도 redis를 구성해야 합니다
<context:property-placeholder location="classpath:conf/redis.properties" /> <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxIdle" value="${redis.maxIdle}" /> <property name="maxTotal" value="${redis.maxActive}" /> <property name="maxWaitMillis" value="${redis.maxWait}" /> <property name="testOnBorrow" value="${redis.testOnBorrow}" /> </bean> <bean id="connectionFactory"class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="poolConfig" ref="poolConfig" /> <property name="port" value="${redis.port}" /> <property name="hostName" value="${redis.host}" /> <property name="password" value="${redis.pass}" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="connectionFactory" /> </bean>
좋아, redis 구성이 완료되었습니다.
이제 Spring의 캐시를 구성해 보겠습니다.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cache="http://www.springframework.org/schema/cache"xmlns:p="http://www.springframework.org/schema/p"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"><cache:annotation-driven /> <!-- 缓存管理器 --> <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager"> <property name="caches"> <set> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name" value="default" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name" value="commonService.queryCityListByParentCode" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name"value="commonService.queryIndustryListByParentCode" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name" value="commonService.queryIndustryInfoById" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name" value="commonService.queryIndustryNameByIds" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name" value="commonService.queryCityNameByIds" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name" value="commonService.isSpecialSchool" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name" value="commonService.getProvinceByCity" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name" value="permissionService.queryMenuList" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name" value="permissionService.queryOperationOfMenu" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name" value="roleService.queryAllRole" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name" value="permissionService.queryPermissionTree" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name"value="permissionService.queryPermissaionMenuByRoleCode" /> <property name="timeout" value="${redis.timeout}" /> </bean> <bean class="com.config.SystemRedisCache"> <property name="redisTemplate" ref="redisTemplate" /> <property name="name"value="permissionService.queryAllPermissionByRoleCode" /> <property name="timeout" value="${redis.timeout}" /> </bean> </set> </property> <!-- <property name="fallbackToNoOpCache" value="false"/> --> </bean> </beans>
사실 위 구성 파일은 이미 spring의 xml 파일에서 redis와 spring 주석 캐시 간의 관계를 구성했습니다.
해당 SystemRedisCache 클래스는 캐시 인터페이스를 구현하는 사용자 정의 캐시 구현 클래스입니다.
import org.springframework.cache.Cache;import org.springframework.cache.support.SimpleValueWrapper;import org.springframework.dao.DataAccessException;import org.springframework.data.redis.connection.RedisConnection;import org.springframework.data.redis.core.RedisCallback;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.util.StringUtils;/** * 〈一句话功能简述〉<br> * 〈功能详细描述〉 * * @author Administrator * @see [相关类/方法](可选) * @since [产品/模块版本] (可选) */public class SystemRedisCache implements Cache {/** * Redis */private RedisTemplate<String, Object> redisTemplate;/** * 缓存名称 */private String name;/** * 超时时间 */private long timeout;/* * (non-Javadoc) * @see org.springframework.cache.Cache#getName() */@Overridepublic String getName() {return this.name; }/* * (non-Javadoc) * @see org.springframework.cache.Cache#getNativeCache() */@Overridepublic Object getNativeCache() {// TODO Auto-generated method stubreturn this.redisTemplate; }/* * (non-Javadoc) * @see org.springframework.cache.Cache#get(java.lang.Object) */@Overridepublic ValueWrapper get(Object key) {if (StringUtils.isEmpty(key)) {return null; } else {final String finalKey;if (key instanceof String) { finalKey = (String) key; } else { finalKey = key.toString(); } Object object = null; object = redisTemplate.execute(new RedisCallback<Object>() {public Object doInRedis(RedisConnection connection) throws DataAccessException {byte[] key = finalKey.getBytes();byte[] value = connection.get(key);if (value == null) {return null; }return SerializableObjectUtil.unserialize(value); } });return (object != null ? new SimpleValueWrapper(object) : null); } }/* * (non-Javadoc) * @see org.springframework.cache.Cache#get(java.lang.Object, java.lang.Class) */@SuppressWarnings("unchecked") @Overridepublic <T> T get(Object key, Class<T> type) {if (StringUtils.isEmpty(key) || null == type) {return null; } else {final String finalKey;final Class<T> finalType = type;if (key instanceof String) { finalKey = (String) key; } else { finalKey = key.toString(); }final Object object = redisTemplate.execute(new RedisCallback<Object>() {public Object doInRedis(RedisConnection connection) throws DataAccessException {byte[] key = finalKey.getBytes();byte[] value = connection.get(key);if (value == null) {return null; }return SerializableObjectUtil.unserialize(value); } });if (finalType != null && finalType.isInstance(object) && null != object) {return (T) object; } else {return null; } } }/* * (non-Javadoc) * @see org.springframework.cache.Cache#put(java.lang.Object, java.lang.Object) */@Overridepublic void put(final Object key, final Object value) {if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value)) {return; } else {final String finalKey;if (key instanceof String) { finalKey = (String) key; } else { finalKey = key.toString(); }if (!StringUtils.isEmpty(finalKey)) {final Object finalValue = value; redisTemplate.execute(new RedisCallback<Boolean>() { @Overridepublic Boolean doInRedis(RedisConnection connection) { connection.set(finalKey.getBytes(), SerializableObjectUtil.serialize(finalValue));// 设置超时间 connection.expire(finalKey.getBytes(), timeout);return true; } }); } } }/* * 根据Key 删除缓存 */@Overridepublic void evict(Object key) {if (null != key) {final String finalKey;if (key instanceof String) { finalKey = (String) key; } else { finalKey = key.toString(); }if (!StringUtils.isEmpty(finalKey)) { redisTemplate.execute(new RedisCallback<Long>() {public Long doInRedis(RedisConnection connection) throws DataAccessException {return connection.del(finalKey.getBytes()); } }); } } }/* * 清楚系统缓存 */@Overridepublic void clear() {// TODO Auto-generated method stub// redisTemplate.execute(new RedisCallback<String>() {// public String doInRedis(RedisConnection connection) throws DataAccessException {// connection.flushDb();// return "ok";// }// }); }public RedisTemplate<String, Object> getRedisTemplate() {return redisTemplate; }public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {this.redisTemplate = redisTemplate; }public void setName(String name) {this.name = name; }public long getTimeout() {return timeout; }public void setTimeout(long timeout) {this.timeout = timeout; } }
주요 메소드는 get 및 put 메소드이며 내부 로직은 우리 자신의 필요에 따라 구현됩니다.
이제 문제가 발생했습니다. Spring이 자체 주석 캐시를 구성하는 구성 파일에 여러 캐시가 구성되어 있는 것을 발견했습니다. 그러면 Spring은 해당 캐시 관리자를 어떻게 찾나요?
코드로 직접 제공합니다:
/** * * 公共接口 * * @author Administrator * @see [相关类/方法](可选) * @since [产品/模块版本] (可选) */@Service("commonService")public class CommonServiceImpl implements CommonService {/** * 日志记录器 */private static final Logger LOGGER = LoggerFactory.getLogger(CommonServiceImpl.class); @Autowiredprivate DalClient dalClient;/* * @Autowired RedisTemplate<?, ?> redisTemplate; *//** * 根据名称获取自增序列squence的当前值 * * @param SequenceName 自增序列名称 * @return 自增序列当前值 * @see [相关类/方法](可选) * @since [产品/模块版本](可选) */@Overridepublic String getSequenceByName(String SequenceName) {if (StringUtils.isEmpty(SequenceName)) { LOGGER.error("自增序列名称为空,无法返回正常的自增序列值");return null; } else { Map<String, String> paramMap = new HashMap<String, String>(); paramMap.put("sequenceName", SequenceName);// 查询sequence当前值Map<String, Object> resultMap = dalClient.queryForMap("common.GET_SEQUENCE_BY_NAME", paramMap);if (null != resultMap && !resultMap.isEmpty()) {return String.valueOf(resultMap.get("sequenceValue")); } else {return null; } } }/** * 根据上一级的城市编码 查询 所有下属城市 列表 * * @param parentCityCode * @return * @see [相关类/方法](可选) * @since [产品/模块版本](可选) */@Override @Cacheable(value = "commonService.queryCityListByParentCode", key = "new String('commonService.queryCityListByParentCode')+#parentCityCode.toString()", condition = "null != #parentCityCode")public List<CityBean> queryCityListByParentCode(final Integer parentCityCode) { Map<String, Object> paramMap = new HashMap<String, Object>();if (null != parentCityCode) {// 根据所选省份 \ 城市 查询所属城市列表paramMap.put("parentCityCode", parentCityCode);final List<CityBean> cityListResult = dalClient.queryForList("T_CITY.SELECT_BY_PARENTCODE", paramMap, CityBean.class);return cityListResult; } else {final List<CityBean> provinceListResult = dalClient.queryForList("T_CITY.SELECT_ALL_FIRST_STEP_CITY", paramMap, CityBean.class);return provinceListResult; } }/** * 根据上一级的行业编码 查询 所有下属所有行业 * * @param parentCityCode * @return * @see [相关类/方法](可选) * @since [产品/模块版本](可选) */@Override @Cacheable(value = "commonService.queryIndustryListByParentCode", key = "new String('commonService.queryIndustryListByParentCode')+#parentIndustryCode.toString", condition = "null != #parentIndustryCode")public List<IndustryBean> queryIndustryListByParentCode(final Integer parentIndustryCode) { Map<String, Object> paramMap = new HashMap<String, Object>();if (null != parentIndustryCode) { paramMap.put("parentIndustryCode", parentIndustryCode);final List<IndustryBean> industryListResult = dalClient.queryForList("T_INDUSTRY.SELECT_BY_PARENTCODE", paramMap, IndustryBean.class);return industryListResult; } else {final List<IndustryBean> industryListResult = dalClient.queryForList("T_INDUSTRY.SELECT_ALL_FIRST_STEP_INDUSTRY", paramMap, IndustryBean.class);return industryListResult; } }/** * 根据行业编码查询 行业信息 * * @param industryCode * @return * @see [相关类/方法](可选) * @since [产品/模块版本](可选) */@Override @Cacheable(value = "commonService.queryIndustryInfoById", key = "new String('commonService.queryIndustryInfoById')+#industryCode", condition = "(null != #industryCode) and (#industryCode.length() > 0)")public IndustryBean queryIndustryInfoById(final String industryCode) {if (StringUtils.isEmpty(industryCode)) {return null; } else { Map<String, Object> paramMap = new HashMap<String, Object>(); paramMap.put("industryCode", industryCode);final IndustryBean industryInfoResult = dalClient.queryForObject("T_INDUSTRY.SELECT_BY_ID", paramMap, IndustryBean.class);return industryInfoResult; } }/** * 递归删除 元素 因为可能存在重复的 * * @param list 列表 * @param item 要删除的元素 * @see [相关类/方法](可选) * @since [产品/模块版本](可选) */private void deleteListElement(ArrayList<String> list, String item) {if (null != list && !list.isEmpty() && StringUtils.isNotBlank(item)) {if (list.contains(item)) { list.remove(item);if (list.contains(item)) { deleteListElement(list, item); } } } }/** * 根据行业id查询 行业名称 * * @param industryIds 行业Id可能有多个 以分号分隔 * @return 行业名称列表 * @see [相关类/方法](可选) * @since [产品/模块版本](可选) */@Override @Cacheable(value = "commonService.queryIndustryNameByIds", key = "new String('commonService.queryIndustryNameByIds')+#industryIds", condition = "null != #industryIds and #industryIds.length() > 0")public List<String> queryIndustryNameByIds(final String industryIds) {if (StringUtils.isBlank(industryIds)) {return null; } else { String[] industryIdArr = industryIds.split(";");if (null != industryIdArr && industryIdArr.length > 0) { ArrayList<String> paramList = new ArrayList<String>(); paramList.addAll(Arrays.asList(industryIdArr));if (null != paramList && !paramList.isEmpty()) { Map<String, Object> paramMap = new HashMap<String, Object>(); paramMap.put("industryIdList", paramList);// 查询行业列表List<IndustryBean> queryResultList = dalClient.queryForList("T_INDUSTRY.SELECT_BY_ID_LIST", paramMap, IndustryBean.class);// 封装查询结果List<String> industryNameList = new ArrayList<String>();if (null != queryResultList && !queryResultList.isEmpty()) {// 遍历查询列表 将已经存在的编码去掉 剩下的 就是 根據编码查询不出行业的 直接将行业的名字返回 String tempId;for (IndustryBean industryInfo : queryResultList) {if (null != industryInfo) {if (null == industryInfo.getIndustryCode()) {continue; } else { tempId = industryInfo.getIndustryCode().toString();if (paramList.contains(tempId)) { deleteListElement(paramList, tempId); }if (StringUtils.isNotBlank(industryInfo.getIndustryName())) { industryNameList.add(industryInfo.getIndustryName()); } } } } }// 将根据编码查询不出来 的 行业编码 直接返回 industryNameList.addAll(paramList);return industryNameList; } }return null; } }/** * 根据城市id查询 城市名称 * * @param industryIds 行业Id可能有多个 以分号分隔 * @return 行业名称列表 * @see [相关类/方法](可选) * @since [产品/模块版本](可选) */@Override @Cacheable(value = "commonService.queryCityNameByIds", key = "new String('commonService.queryCityNameByIds')+#cityIds", condition = "null != #cityIds and #cityIds.length() > 0")public List<String> queryCityNameByIds(String cityIds) {if (StringUtils.isBlank(cityIds)) {return null; } else { String replacyedCityIds = cityIds.replace(";", ","); String[] industryIdArr = replacyedCityIds.split(",");if (null != industryIdArr && industryIdArr.length > 0) { ArrayList<String> paramList = new ArrayList<String>(); paramList.addAll(Arrays.asList(industryIdArr));if (null != paramList && !paramList.isEmpty()) { Map<String, Object> paramMap = new HashMap<String, Object>(); paramMap.put("cityIdList", paramList);// 查询行业列表List<CityBean> queryResultList = dalClient.queryForList("T_CITY.SELECT_BY_ID_LIST", paramMap, CityBean.class); List<String> industryNameList = new ArrayList<String>();if (null != queryResultList && !queryResultList.isEmpty()) {// 遍历查询列表 将已经存在的编码去掉 剩下的 就是 根據编码查询不出行业的 直接将行业的名字返回// 封装查询结果 String tempId;for (CityBean industryInfo : queryResultList) {if (null != industryInfo) {if (null == industryInfo.getCityCode()) {continue; } else { tempId = industryInfo.getCityCode().toString();if (paramList.contains(tempId)) { deleteListElement(paramList, tempId); }if (StringUtils.isNotBlank(industryInfo.getCityName())) { industryNameList.add(industryInfo.getCityName()); } } } } }// 将根据编码查询不出来 的 行业编码 直接返回 industryNameList.addAll(paramList);return industryNameList; } }return null; } }/** * 查询第一级所有职位 * * @return */@Overridepublic List<JobTypeVo> queryFirstJobList() {/* * List<JobTypeVo> redisIndustryListResult = redisTemplate.execute(new RedisCallback<List<JobTypeVo>>() { * @Override public List<JobTypeVo> doInRedis(RedisConnection connection) { byte[] industryListList = * connection.get((RedisConstants.JOB_FIRST_LIST).getBytes()); if (null != industryListList && * industryListList.length > 0) { return (List<JobTypeVo>) SerializableObjectUtil.unserialize(industryListList); * } else { return null; } } }); if (null != redisIndustryListResult && !redisIndustryListResult.isEmpty()) { * return redisIndustryListResult; } else { */final List<JobTypeVo> queryIndustryListResult = dalClient.queryForList("T_JOB_TYPE.SELECT_FIRST_JOB_CODE",null, JobTypeVo.class);/* * if (null != queryIndustryListResult && !queryIndustryListResult.isEmpty()) { redisTemplate.execute(new * RedisCallback<Boolean>() { * @Override public Boolean doInRedis(RedisConnection connection) { * connection.set((RedisConstants.JOB_FIRST_LIST).getBytes(), * SerializableObjectUtil.serialize(queryIndustryListResult)); return true; } }); } */return queryIndustryListResult;/* } */}/** * 查询 对应级别的职位信息 * * @param typeValue * @param jobCode * @return */@Overridepublic List<JobTypeBean> queryJobTypeList(final int typeValue, final int jobCode) {/* * List<JobTypeBean> redisIndustryListResult = redisTemplate.execute(new RedisCallback<List<JobTypeBean>>() { * @Override public List<JobTypeBean> doInRedis(RedisConnection connection) { byte[] industryListList = * connection.get((RedisConstants.JOB_FIRST_LIST + typeValue + jobCode) .getBytes()); if (null != * industryListList && industryListList.length > 0) { return (List<JobTypeBean>) * SerializableObjectUtil.unserialize(industryListList); } else { return null; } } }); if (null != * redisIndustryListResult && !redisIndustryListResult.isEmpty()) { return redisIndustryListResult; } else { */Map<String, Object> paramMap = new HashMap<String, Object>(); paramMap.put("typeValue", typeValue); paramMap.put("jobFirstCode", jobCode);final List<JobTypeBean> queryResult = dalClient.queryForList("T_JOB_TYPE.SELECT_BY_JOB_CODE", paramMap, JobTypeBean.class);/* * if (null != queryResult && !queryResult.isEmpty()) { redisTemplate.execute(new RedisCallback<Boolean>() { * @Override public Boolean doInRedis(RedisConnection connection) { * connection.set((RedisConstants.JOB_FIRST_LIST + typeValue + jobCode).getBytes(), * SerializableObjectUtil.serialize(queryResult)); return true; } }); } */return queryResult;/* } */}/** * 判断学校是否 特殊学校 * * @param schoolName 学校名称 * @param schoolType 学校分类(1:211 暂无其他) * @return true:是, false:否 * @see [相关类/方法](可选) * @since [产品/模块版本](可选) */@Override @Cacheable(value = "commonService.isSpecialSchool", key = "new String('commonService.isSpecialSchool')+#schoolName + #schoolType", condition = "null != #schoolName and null !=#schoolType and #schoolName.length() > 0")public boolean isSpecialSchool(String schoolName, int schoolType) {if (StringUtils.isEmpty(schoolName)) {return false; } else { Map<String, Object> paramMap = new HashMap<String, Object>(); paramMap.put("schoolName", schoolName); paramMap.put("schoolType", schoolType); Map<String, Object> resultMap = dalClient.queryForMap("T_MY_SPECIAL_SCHOOL.SELECT_BY_NAME_TYPE", paramMap);if (null != resultMap && !resultMap.isEmpty() && resultMap.containsKey("NUM")) {return (long) resultMap.get("NUM") > 0; } else {return false; } } }/** * 根据城市名称获取 城市所在 省份名称 * * @param cityNames * @return * @see [相关类/方法](可选) * @since [产品/模块版本](可选) */@Override @Cacheable(value = "commonService.getProvinceByCity", key = "new String('commonService.getProvinceByCity')+#cityNames", condition = "null != #cityNames and #cityNames.length() > 0")public String getProvinceByCity(final String cityNames) {if (StringUtils.isBlank(cityNames)) {return null; } else { String[] cityArr = cityNames.split("、"); Map<String, Object> paramMap = new HashMap<String, Object>(); Map<String, Object> resultMap; String provinceName; List<String> provinceLait = new ArrayList<String>();for (String cityName : cityArr) {if (StringUtils.isNotBlank(cityName)) { paramMap.put("cityName", cityName); resultMap = dalClient.queryForMap("T_CITY.SELECT_PROVINCE_NAMEBY_CITY_NAME", paramMap);if (null != resultMap && !resultMap.isEmpty() && resultMap.containsKey("provinceName")) { provinceName = String.valueOf(resultMap.get("provinceName"));if (!provinceLait.contains(provinceName)) { provinceLait.add(provinceName); } } } } StringBuffer sb = new StringBuffer(100);if (!provinceLait.isEmpty()) {for (int i = 0; i < provinceLait.size(); i++) {if (i < provinceLait.size() - 1) { sb.append(provinceLait.get(i)).append(","); } else { sb.append(provinceLait.get(i)); } } }return sb.toString(); } }
queryCityListByParentCode 메소드를 예로 들어보세요:
이 메소드에는 @Cacheable 주석이 있는데, 이는 스프링 3.1 이후에 추가된 주석 캐시 태그입니다. 해당 구성 파일을 찾은 후 이 메소드는 이를 찾기 위해 사용자 정의 캐시 구현 클래스를 사용합니다. 스프링 주석의 의미가 명확하지 않은 경우 먼저 스프링 캐시 주석의 의미를 이해할 수 있습니다.
위 내용은 Spring 캐시와 Redis 캐시 통합에 대한 자세한 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!