Le cache Spring peut essentiellement répondre aux besoins de mise en cache des applications générales, mais la réalité est toujours compliquée. Lorsque votre volume d'utilisateurs augmente ou que vos performances ne peuvent pas suivre, vous devez toujours augmenter le cache mémoire. pour cela, vous n'êtes pas satisfait car il ne prend pas en charge la haute disponibilité et n'a pas la possibilité de conserver les données. Pour le moment, vous devez personnaliser votre solution de mise en cache. Heureusement, Spring y a également pensé.
Cet article utilise le cache Spring pour s'intégrer à Redis afin d'obtenir le cache souhaité.
Configurons d'abord redis :
La première étape consiste à installer redis. Pour ce Baidu, nous configurons principalement redis.
Ajoutez un fichier de configuration Redis, qui peut être placé dans le répertoire suivant
redis.host=192.168.0.43redis.port=6379redis.pass=2015redis.maxIdle=50redis.maxActive=50redis.maxWait=50redis.testOnBorrow=trueredis.timeout=1000
Vous devez également configurer Redis dans le fichier de configuration Spring
<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>
D'accord, la configuration de redis est terminée.
Configurons maintenant le cache de 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>
En fait, le fichier de configuration ci-dessus a configuré la relation entre Redis et le cache d'annotations de Spring avec le fichier XML de Spring Hit.
La classe SystemRedisCache correspondant à
est une classe d'implémentation de cache personnalisée qui implémente l'interface de cache.
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; } }
Les méthodes principales sont les méthodes get et put, et la logique à l'intérieur est implémentée en fonction de nos propres besoins.
Il y a maintenant un problème. Nous avons constaté que plusieurs caches sont configurés dans le fichier de configuration de Spring pour configurer son propre cache d'annotations. Comment Spring trouve-t-il le cacheManager correspondant ?
Nous vous la présentons directement en code :
/** * * 公共接口 * * @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(); } }
Prenons comme exemple la méthode queryCityListByParentCode :
Il y a l'annotation @Cacheable sur cette méthode. Il s'agit de la balise de cache d'annotation ajoutée après le printemps 3.1. Elle recherchera le fichier XML que nous avons configuré au printemps en fonction de la valeur de l'attribut value = "commonService. .queryCityListByParentCode". Recherchez l'attribut name dans et trouvez le fichier de configuration correspondant. Cette méthode implémentera la logique correspondante via notre classe d'implémentation de cache personnalisée. Si vous n'êtes pas clair sur la signification des annotations Spring, vous pouvez d'abord comprendre la signification de annotations du cache de printemps.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!