spring-data-redis 自定义注解扩展实现时效设置
来源:互联网 发布:模拟倒车软件 编辑:程序博客网 时间:2024/04/28 08:01
前言
spring目前在@Cacheable和@CacheEvict等注解上不支持缓存时效设置,只允许通过配置文件设置全局时效。这样就很不方便设定时间。比如系统参数和业务数据的时效是不一样的,这给程序开发造成很大的困扰。不得已,本人实现了类似Sping的三个注解。以下是具体实现。
工程下载>>>
实现
1、注解类的实现
package com.example.spring.boot.redis.annotation;import java.lang.annotation.*;/** * Author: 王俊超 * Date: 2017-06-10 05:56 * All Rights Reserved !!! */@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic @interface RedisCacheEvict { /** * 缓存名称 * * @return */ String cacheName(); /** * 缓存key * * @return */ String key();}
package com.example.spring.boot.redis.annotation;import java.lang.annotation.*;import java.util.concurrent.TimeUnit;/** * Author: 王俊超 * Date: 2017-06-10 05:56 * All Rights Reserved !!! */@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic @interface RedisCacheGet { /** * 缓存名称 * * @return */ String cacheName(); /** * 缓存key * * @return */ String key(); /** * 缓存过期时间 * * @return */ int expire() default 0; /** * 缓存的时间单位 * * @return */ TimeUnit timeUnit();}
package com.example.spring.boot.redis.annotation;import java.lang.annotation.*;import java.util.concurrent.TimeUnit;/** * Author: 王俊超 * Date: 2017-06-10 05:56 * All Rights Reserved !!! */@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic @interface RedisCachePut { /** * 缓存名称 * * @return */ String cacheName(); /** * 缓存key * * @return */ String key(); /** * 缓存过期时间 * * @return */ int expire() default 0; /** * 缓存的时间单位 * * @return */ TimeUnit timeUnit();}
2、切面代码实现
package com.example.spring.boot.redis.aspect;import com.example.spring.boot.redis.annotation.RedisCacheEvict;import com.example.spring.boot.redis.annotation.RedisCacheGet;import com.example.spring.boot.redis.annotation.RedisCachePut;import com.example.spring.boot.redis.common.RedisClient;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.Signature;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.core.LocalVariableTableParameterNameDiscoverer;import org.springframework.core.ParameterNameDiscoverer;import org.springframework.util.StringUtils;import java.lang.reflect.Method;/** * 缓存处理时间切面 * * Author: 王俊超 * Date: 2017-06-10 06:17 * All Rights Reserved !!! */@Aspectpublic class RedisCacheAspect { private final static char DOT = '.'; private final static char SHARP = '#'; private RedisClient redisClient; public RedisClient getRedisClient() { return redisClient; } public void setRedisClient(RedisClient redisClient) { this.redisClient = redisClient; } /** * 对象入缓存 * @param pjp * @param cachePut * @return * @throws Throwable */ @Around("@annotation(cachePut)") public Object cachePut(ProceedingJoinPoint pjp, RedisCachePut cachePut) throws Throwable { Object keyObject = getCacheKey(pjp, cachePut.key()); Object result = pjp.proceed(); // 不为空才保存数据 if (result != null && keyObject != null){ redisClient.set(cachePut.cacheName(), keyObject, result, cachePut.expire()); } return result; } /** * 优先从缓存中取对象 * * @param pjp * @param cacheGet * @return * @throws Throwable */ @Around("@annotation(cacheGet)") public Object cacheGet(ProceedingJoinPoint pjp, RedisCacheGet cacheGet) throws Throwable { Object keyObject = getCacheKey(pjp, cacheGet.key()); Object result = redisClient.get(cacheGet.cacheName(), keyObject); // 如果从缓存中没有取到数据,就从调用方法获取数据 if (result == null) { result = pjp.proceed(); // 方法的返回值不是void类型,就要将结果入缓存 Class clz = getAdvicedMethod(pjp).getReturnType(); if (clz != Void.class) { redisClient.set(cacheGet.cacheName(), keyObject, result, cacheGet.expire()); } } return result; } /** * 删除缓存 * * @param pjp * @param cacheEvict * @return * @throws Throwable */ @Around("@annotation(cacheEvict)") public Object cacheEvict(ProceedingJoinPoint pjp, RedisCacheEvict cacheEvict) throws Throwable { Object keyObject = getCacheKey(pjp, cacheEvict.key()); redisClient.del(cacheEvict.cacheName(), keyObject); return pjp.proceed(); } /** * 获取缓存的key对象 * * @param pjp * @param key * @return * @throws Exception */ private Object getCacheKey(ProceedingJoinPoint pjp, String key) throws Exception { // 以#开头 if (key.length() > 0 && key.charAt(0) == SHARP) { // 去掉# key = key.substring(1); // 将key分割成属性和参数名,第一个“.”之前是参数名,之后是属性名称 int dotIdx = key.indexOf(DOT); String argName = key; if (dotIdx > 0) { argName = key.substring(0, dotIdx); key = key.substring(dotIdx + 1); // 剩下的属性 } // 取参数值 Object argVal = getArg(pjp, argName); // 获取参数的属性值 Object objectKey = argVal; if (dotIdx > 0) { objectKey = getObjectKey(argVal, key); } return objectKey; } else { // 不是以#开头的就以其值作为参数key return key; } } /** * 获取参数对象 * * @param pjp 连接点 * @param parameterName 参数名称 * @return */ private Object getArg(ProceedingJoinPoint pjp, String parameterName) throws NoSuchMethodException { Method method = getAdvicedMethod(pjp); ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); String[] parameterNames = parameterNameDiscoverer.getParameterNames(method); if (parameterNames != null) { int idx = 0; for (String name : parameterNames) { if (name.equals(parameterName)) { return pjp.getArgs()[idx]; } idx++; } } throw new IllegalArgumentException("no such parameter name: [" + parameterName + "] in method: " + method); } /** * 获取拦截的方法 * * @param pjp 连接点 * @return * @throws NoSuchMethodException */ private Method getAdvicedMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException { Signature sig = pjp.getSignature(); MethodSignature msig = null; if (!(sig instanceof MethodSignature)) { throw new IllegalArgumentException("annotation can only use to method."); } msig = (MethodSignature) sig; Object target = pjp.getTarget(); return target.getClass().getMethod(msig.getName(), msig.getParameterTypes()); } /** * 获取从object上获取key所对应的属性对象 * 例如:key: country.province.city.town * 就相当于调用:object.getCountry().getProvince().getCity.getTown() * * @param object * @param key * @return * @throws Exception */ private Object getObjectKey(Object object, String key) throws Exception { // 如果key已经是空了就直接返回 if (StringUtils.isEmpty(key)) { return object; } int dotIdx = key.indexOf(DOT); // 形如key=aa.bb**的情况 if (dotIdx > 0) { // 取第一个属性值 String propertyName = key.substring(0, dotIdx); // 取剩下的key key = key.substring(dotIdx + 1); Object property = getProperty(object, getterMethod(propertyName)); return getObjectKey(property, key); } else { // key=aa return getProperty(object, getterMethod(key)); } } /** * 获取name的getter方法名称 * * @param name * @return */ private String getterMethod(String name) { return "get" + Character.toUpperCase(name.charAt(0)) + name.substring(1); } /** * 调用obj对象上的getterMethodName * * @param obj * @param getterMethodName * @return * @throws Exception */ private Object getProperty(Object obj, String getterMethodName) throws Exception { return obj.getClass().getMethod(getterMethodName).invoke(obj); }}
3、工具类实现
package com.example.spring.boot.redis.common;import com.esotericsoftware.kryo.Kryo;import com.esotericsoftware.kryo.io.Input;import com.esotericsoftware.kryo.io.Output;import org.springframework.data.redis.serializer.RedisSerializer;import org.springframework.data.redis.serializer.SerializationException;/** * 序列化和反序列化工具 * * Author: 王俊超 * Date: 2017-05-31 21:43 * All Rights Reserved !!! */public class KryoRedisSerializer<T> implements RedisSerializer<T> { private Kryo kryo = new Kryo(); @Override public byte[] serialize(T t) throws SerializationException { byte[] buffer = new byte[2048]; Output output = new Output(buffer); kryo.writeClassAndObject(output, t); byte[] result = output.toBytes(); output.close(); return result; } @Override public T deserialize(byte[] bytes) throws SerializationException { Input input = new Input(bytes); @SuppressWarnings("unchecked") T t = (T) kryo.readClassAndObject(input); input.close(); return t; }}
package com.example.spring.boot.redis.common;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.data.redis.serializer.RedisSerializer;import java.util.Set;/** * Redis客户端实现工具 * * Author: 王俊超 * Date: 2017-06-04 19:57 * All Rights Reserved !!! */public class RedisClientImpl implements RedisClient { private RedisTemplate<Object, Object> redisTemplate; private RedisSerializer<Object> keySerializer; private RedisSerializer<Object> valSerializer; public RedisTemplate<Object, Object> getRedisTemplate() { return redisTemplate; } public void setRedisTemplate(RedisTemplate<Object, Object> redisTemplate) { this.redisTemplate = redisTemplate; } public RedisSerializer<Object> getKeySerializer() { return keySerializer; } public void setKeySerializer(RedisSerializer<Object> keySerializer) { this.keySerializer = keySerializer; } public RedisSerializer<Object> getValSerializer() { return valSerializer; } public void setValSerializer(RedisSerializer<Object> valSerializer) { this.valSerializer = valSerializer; } ///////////////////////////////////// /** * 获取最终的key * * @param cacheName * @param key * @return */ private byte[] getRealKey(Object cacheName, Object key) { byte[] b1 = keySerializer.serialize(cacheName); byte[] b2 = keySerializer.serialize(key); byte[] result = new byte[b1.length + b2.length]; System.arraycopy(b1, 0, result, 0, b1.length); System.arraycopy(b2, 0, result, b1.length, b2.length); return result; } /** * 获取真实key * @param cacheName * @param key * @return */ private byte[] getRealKey(byte[] cacheName, Object key) { byte[] b2 = keySerializer.serialize(key); byte[] result = new byte[cacheName.length + b2.length]; System.arraycopy(cacheName, 0, result, 0, cacheName.length); System.arraycopy(b2, 0, result, cacheName.length, b2.length); return result; } @Override public long del(Object cacheName, Object... keys) { return redisTemplate.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { byte[] b1 = keySerializer.serialize(cacheName); long result = 0; for (Object o : keys) { result += connection.del(getRealKey(b1, o)); } return result; } }); } @Override public void set(byte[] key, byte[] value, long liveTime) { redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { connection.set(key, value); if (liveTime > 0) { connection.expire(key, liveTime); } return true; } }); } @Override public void set(Object cacheName, Object key, Object value, long liveTime) { this.set(getRealKey(cacheName, key), valSerializer.serialize(value), liveTime); } @Override public void set(Object cacheName, Object key, Object value) { this.set(cacheName, key, value, 0L); } @Override public void set(byte[] key, byte[] value) { this.set(key, value, 0L); } /** * 获取redis value (String) * * @param key * @return */ @Override public <T> T get(byte[] key) { return redisTemplate.execute(new RedisCallback<T>() { @Override public T doInRedis(RedisConnection connection) throws DataAccessException { byte[] result = connection.get(key); Object obj = null; if (result != null) { obj = valSerializer.deserialize(result); } return (T) obj; } }); } @Override public <T> T get(Object cacheName, Object key) { return this.get(getRealKey(cacheName, key)); } @Override public Set keys(byte[] pattern) { return redisTemplate.execute(new RedisCallback<Set>() { @Override public Set doInRedis(RedisConnection connection) throws DataAccessException { return connection.keys(pattern); } }); } @Override public Set keys(String pattern) { return this.keys(keySerializer.serialize(pattern)); } @Override public boolean exists(byte[] key) { return redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { return connection.exists(key); } }); } @Override public boolean exists(Object cacheName, Object key) { return this.exists(getRealKey(cacheName, key)); } @Override public boolean flushDb() { return redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { connection.flushDb(); return true; } }); } @Override public long dbSize() { return redisTemplate.execute(new RedisCallback<Long>() { public Long doInRedis(RedisConnection connection) throws DataAccessException { return connection.dbSize(); } }); } @Override public String ping() { return redisTemplate.execute(new RedisCallback<String>() { public String doInRedis(RedisConnection connection) throws DataAccessException { return connection.ping(); } }); }}
4、应用配置
package com.example.spring.boot.redis;import com.example.spring.boot.redis.aspect.RedisCacheAspect;import com.example.spring.boot.redis.common.KryoRedisSerializer;import com.example.spring.boot.redis.common.RedisClient;import com.example.spring.boot.redis.common.RedisClientImpl;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.data.redis.connection.RedisClusterConfiguration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import java.net.UnknownHostException;import java.util.Arrays;import java.util.List;/** * Author: 王俊超 * Date: 2017-05-07 10:02 * All Rights Reserved !!! */@Configurationpublic class AppConfig { /** * Redis连接工厂 * * @return */ @Primary @Bean("redisConnectionFactory") public RedisConnectionFactory redisConnectionFactory() { // Redis集群地址 List<String> clusterNodes = Arrays.asList("192.168.241.150:7110", "192.168.241.150:7111", "192.168.241.150:7112", "192.168.241.150:7113", "192.168.241.150:7114", "192.168.241.150:7115", "192.168.241.150:7116", "192.168.241.150:7117", "192.168.241.150:7118", "192.168.241.150:7119" ); // 获取Redis集群配置信息 RedisClusterConfiguration rcf = new RedisClusterConfiguration(clusterNodes); return new JedisConnectionFactory(rcf); } /** * 创建redis模板 * * @param factory * @return * @throws UnknownHostException */ @Primary @Bean("redisTemplate") public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 可根据需要设置// // redis value使用的序列化器// template.setValueSerializer(new KryoRedisSerializer<>());// template.setHashKeySerializer(new KryoRedisSerializer<>());// // redis key使用的序列化器// template.setKeySerializer(new KryoRedisSerializer<>());// template.setHashValueSerializer(new KryoRedisSerializer<>()); template.afterPropertiesSet(); return template; } /** * 返回redis客户端 * * @param redisTemplate * @return */ @Bean public RedisClient redisClient(RedisTemplate<Object, Object> redisTemplate) { RedisClientImpl redisClient = new RedisClientImpl(); redisClient.setRedisTemplate(redisTemplate); KryoRedisSerializer<Object> serializer = new KryoRedisSerializer<>(); redisClient.setKeySerializer(serializer); redisClient.setValSerializer(serializer); return redisClient; } /** * redis缓存的切面 * @param redisClient * @return */ @Bean public RedisCacheAspect redisCacheAspect(RedisClient redisClient) { RedisCacheAspect aspect = new RedisCacheAspect(); aspect.setRedisClient(redisClient); return aspect; }}
5、应用的启动
package com.example.spring.boot.redis;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cache.annotation.EnableCaching;import org.springframework.context.ConfigurableApplicationContext;import org.springframework.transaction.annotation.EnableTransactionManagement;/** * Author: 王俊超 * Date: 2017-05-07 10:04 * All Rights Reserved !!! */@SpringBootApplication@EnableTransactionManagement@EnableCaching@EnableAutoConfiguration//@EnableAspectJAutoProxypublic class AppRunner { public static void main(String[] args) { ConfigurableApplicationContext ctx = SpringApplication.run(AppRunner.class, args); }}
6、配置文件
server.context-path=/redis/cache# 开启AOPspring.aop.auto=true
测试
1、测试数据
package com.example.spring.boot.redis.entity;/** * 县 * Author: 王俊超 * Date: 2017-06-12 20:06 * All Rights Reserved !!! */public class City { private long id; private String name; private Province province; public City() { } public City(long id, String name, Province province) { this.id = id; this.name = name; this.province = province; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Province getProvince() { return province; } public void setProvince(Province province) { this.province = province; }}
package com.example.spring.boot.redis.entity;/** * 国家 * Author: 王俊超 * Date: 2017-06-12 20:06 * All Rights Reserved !!! */public class Country { private long id; private String name; public Country() { } public Country(long id, String name) { this.id = id; this.name = name; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; }}
package com.example.spring.boot.redis.entity;/** * 省 * Author: 王俊超 * Date: 2017-06-12 20:06 * All Rights Reserved !!! */public class Province { private long id; private String name; private Country country; public Province() { } public Province(long id, String name, Country country) { this.id = id; this.name = name; this.country = country; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Country getCountry() { return country; } public void setCountry(Country country) { this.country = country; }}
package com.example.spring.boot.redis.entity;/** * 镇 * Author: 王俊超 * Date: 2017-06-12 20:06 * All Rights Reserved !!! */public class Town { private long id; private String name; private City city; public Town() { } public Town(long id, String name, City city) { this.id = id; this.name = name; this.city = city; } public long getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public City getCity() { return city; } public void setCity(City city) { this.city = city; }}
package com.example.spring.boot.redis;import com.example.spring.boot.redis.annotation.RedisCacheEvict;import com.example.spring.boot.redis.annotation.RedisCacheGet;import com.example.spring.boot.redis.annotation.RedisCachePut;import com.example.spring.boot.redis.entity.City;import com.example.spring.boot.redis.entity.Country;import com.example.spring.boot.redis.entity.Province;import com.example.spring.boot.redis.entity.Town;import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;/** * Author: 王俊超 * Date: 2017-06-10 06:26 * All Rights Reserved !!! */@Componentpublic class TestData { public final static Country COUNTRY = new Country(111111, "CHINA"); public final static Province PROVINCE = new Province(222222, "GuangZhou", COUNTRY); public final static City CITY = new City(333333, "ShenZhen", PROVINCE); public final static Town TOWN = new Town(444444, "Where", CITY); public final static String LOCATION = "location"; @RedisCachePut(cacheName = LOCATION, key = "#town.city.province.country.id", expire = 60, timeUnit = TimeUnit.SECONDS) public Country createCountry(Town town) { return town.getCity().getProvince().getCountry(); } @RedisCacheGet(cacheName = LOCATION, key = "#id", expire = 60, timeUnit = TimeUnit.SECONDS) public Country getCountry(long id) { return COUNTRY; } @RedisCacheEvict(cacheName = LOCATION, key = "#id") public void deleteCountry(long id) { // 清除缓存 }}
2、测试用例
import com.example.spring.boot.redis.AppRunner;import com.example.spring.boot.redis.TestData;import com.example.spring.boot.redis.common.KryoRedisSerializer;import com.example.spring.boot.redis.common.RedisClient;import com.example.spring.boot.redis.entity.Country;import org.junit.Assert;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;/** * Author: 王俊超 * Date: 2017-06-12 21:10 * All Rights Reserved !!! */@RunWith(SpringRunner.class)@SpringBootTest( classes = AppRunner.class)public class TestRunner { @Autowired RedisClient redisClient; @Autowired TestData testData; /** * 测试序列化,反序列化 */ @Test public void testSerialize() { Country c1 = testData.createCountry(TestData.TOWN); KryoRedisSerializer<Object> serializer = new KryoRedisSerializer<>(); byte[] b1 = serializer.serialize(c1); Country c2 = (Country) serializer.deserialize(b1); Assert.assertNotNull(c2); Assert.assertEquals(c1.getId(), c2.getId()); Assert.assertEquals(c1.getName(), c2.getName()); } @Test public void testCachePut() { // 先清理缓存 redisClient.del(TestData.LOCATION, TestData.COUNTRY.getId()); Country c = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId()); Assert.assertNull(c); // 创建一个国家 Country c1 = testData.createCountry(TestData.TOWN); // 直接从缓存中取数据,说明数据已经入缓存 Country c2 = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId()); Assert.assertNotNull(c2); Assert.assertEquals(c1.getId(), c2.getId()); Assert.assertEquals(c1.getName(), c2.getName()); } @Test public void testCacheGet() { // 先清理缓存 redisClient.del(TestData.LOCATION, TestData.COUNTRY.getId()); Country c = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId()); Assert.assertNull(c); // 取数据 Country c1 = testData.getCountry(TestData.COUNTRY.getId()); // 从缓存中取 Country c2 = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId()); Assert.assertNotNull(c2); Assert.assertEquals(c1.getId(), c2.getId()); Assert.assertEquals(c1.getName(), c2.getName()); } @Test public void testCacheEvict() { // 创建一个国家 Country c = testData.createCountry(TestData.TOWN); redisClient.del(TestData.LOCATION, TestData.COUNTRY.getId()); c = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId()); Assert.assertNull(c); }}
阅读全文
1 0
- spring-data-redis 自定义注解扩展实现时效设置
- Spring Cache+Redis实现自定义注解缓存
- Spring Data+Redis缓存实现
- redis实现 spring-redis-data初学习
- redis实现 spring-redis-data初学习
- redis实现 spring-redis-data初学习
- redis实现 spring-redis-data,存取对象
- redis实现 spring-redis-data初学习
- redis实现 spring-redis-data初学习
- redis实现 spring-redis-data初学习
- Spring Data自定义接口实现
- spring自定义注解AOP实现
- spring-data-redis 设置过期时间
- java 自定义注解 spring aop 实现注解
- Java自定义注解代码实现与扩展
- Spring-Data-Redis-Repository中以自定义class作为id的实现
- Redis+MyBatis自定义注解实现缓存
- 使用Spring Data +Redis实现缓存
- 网络盒子连接电视过程及原理,及相关知识
- UVa 10720
- 2017福建程序设计竞赛总结
- 数字证书原理,公钥私钥加密原理
- python 2.7输出中文字符串的编码问题
- spring-data-redis 自定义注解扩展实现时效设置
- 2.3.3--Big Number
- 一、树莓派系统安装
- 在Centos7上搭建局域网的yum源仓库
- 微服务解析
- R语言使用密度聚类笔法处理数据
- Android中的内存优化
- VB遇到的问题【运行EXE时出现“VB未预期的错误”】
- 比特币、质能方程、主观价值论与量子力学