为何要优化
当我们需要对接新的短信服务商的时候我们需要怎么做?
1. 在sms下添加一个新的第三方平台的工具包
2. 在我们的case中添加一个新的判断,判断platfrom字段并调用新的工具包 并且我们还需要手动的封装每个通道需要的smsConfig这个类
3. 在我们查询的过程中,每次都要去查询通道表里的内容,每次都要查询通道签名表的内容,每次都要查询通道模板表里的内容,性能不高
我们可以将查询的部分封装到我们的工具包中,传入我们集信达的模板code和签名code即可
如何优化
空间换时间:我们能否在项目启动后将一部分信息读取到内存中去,这样不用每次都去查询数据库
损失的空间是内存,时间是代码执行的效率, 空间换时间 指的是将一部分信息先读取到内存中 然后以后每次读取就不是磁盘的IO操作了是对内存的操作
我们需要初始化哪些信息
如何优化
空间换时间:我们能否在项目启动后将一部分信息读取到内存中去,这样不用每次都去查询数据库
损失的空间是内存,时间是代码执行的效率, 空间换时间 指的是将一部分信息先读取到内存中 然后以后每次读取就不是磁盘的IO操作了是对内存的操作
1.通道表里的信息
方案1:我们将config里的信息读取到我们的内存中,创建一个List configs,
然后我们调用的时候通过api服务传递过来的ids,遍历ids判断使用哪个通道 构造对应第三方的工具类
不好,我们在发送的时候还需要创建我们的工具类,利用这个工具类调用具体的实现逻辑,我们应该在构建的时候将我们的工具类实例化出来,并保存在内存中
方案2:我们先读取mysql中的config里的内容,根据congfig里的内容将通道的工具类实例化到我们的内存中
如何实现:通过反射的方式创建对象并封装属性
看Demo
提供两个类 man person
@Data
public class Person {
private String name;
private Integer age;
}
@Data
public class Man extends Person{
private String sex;
private String play;
public String toString(){
return "name:"+super.getName()+",age:"+super.getAge()+",sex:"+this.sex+",play:"+this.play;
}
}
public static void main(String[] args) {
try {
String classPath = "cn.itheima.reflex.pojo.Man";
Class classes = ReflexDemo1.class.getClassLoader().loadClass(classPath);
Object o = classes.newInstance();
Method[] declaredMethods = classes.getDeclaredMethods();
//遍历所有的方法
for (Method indexMethod : declaredMethods) {
if (indexMethod.getName().equals("getName")) {
System.out.println("证明getName方法存在");
}
if (indexMethod.getName().equals("getAge")) {
System.out.println("证明getAge方法存在");
}
if (indexMethod.getName().equals("getSex")) {
System.out.println("证明getSex方法存在");
}
if (indexMethod.getName().equals("getPlay")) {
System.out.println("证明getPlay方法存在");
}
}
Method getName = classes.getMethod("getName", null);
Method getAge = classes.getMethod("getAge", null);
Method getSex = classes.getMethod("getSex", null);
Method getPlay = classes.getMethod("getPlay", null);
if (getName == null) {
System.out.println("getName不存在");
}
if (getAge == null) {
System.out.println("getAge不存在");
}
if (getSex == null) {
System.out.println("getSex不存在");
}
if (getPlay == null) {
System.out.println("getPlay不存在");
}
//封装属性
Method setName = classes.getMethod("setName", String.class);
setName.invoke(o, "张三");
Method setAge = classes.getMethod("setAge", Integer.class);
setAge.invoke(o, 20);
Method setSex = classes.getMethod("setSex", String.class);
setSex.invoke(o, "男");
Method setPlay = classes.getMethod("setPlay", String.class);
setPlay.invoke(o, "打篮球");
System.out.println(o);
} catch (Exception e) {
System.out.println("系统异常");
}
}
当我们不提供get set方法的时候
我们如何封装参数
提供的实体类如下:
public class Man extends Person {
private String sex;
private String play;
public String toString(){
return "name:"+super.name+",age:"+super.age+",sex:"+this.sex+",play:"+this.play;
}
}
public class Person {
public String name;
public Integer age;
}
public static void main(String[] args) {
try {
String classPath = "cn.itheima.reflex.demo2.Man";
Class classes = ReflexDemo2.class.getClassLoader().loadClass(classPath);
Object o = classes.newInstance();
//获取对象父类的属性
//根据反射获得类中声明的属性对象
Field nameField = classes.getSuperclass().getDeclaredField("name");
Field ageField = classes.getSuperclass().getDeclaredField("age");
//封装属性内容
nameField.set(o,"张三");
ageField.set(o,20);
Field sex = classes.getDeclaredField("sex");
Field play = classes.getDeclaredField("play");
//设置可以操作当前属性值
sex.setAccessible(true);
play.setAccessible(true);
sex.set(o,"男");
play.set(o,"打游戏");
System.out.println(o);
} catch (Exception e) {
System.out.println("系统异常");
}
}
注意:
sex.setAccessible(true);
play.setAccessible(true);
setAccessible的意思是允许访问私有方法,如果不设置对于一些设置了访问权限的字段我们没有办法访问
然后我们来看我们的业务逻辑
我们希望在服务启动完后,将config里的信息读取出来,并且查询的时候需要按照level排序,根据config里的信息 自动的将util类给封装到系统的内存中去
服务启动完成后怎么设置
- 声明spring组件@Component
- 实现CommandLineRunner接口,重写run方法
在run方法中我们需要读取config表里的内容,读取出来,根据接入平台的名称通过反射的方式去构造工具类
public void initConnect() {
//TODO 根据通道配置,初始化每个通道的bean对象
//1、查询数据库获得通道列表
List<ConfigEntity> configs = configService.listForConnect();
log.info("查询到可用通道:{}",configs);
List beanList = new ArrayList();
//2、遍历通道列表,通过反射创建每个通道的Bean对象(例如AliyunSmsService、MengWangSmsService等)
configs.forEach(config -> {
try {
//封装Bean对象所需的SmsConfig配置对象
SmsConfig smsConfig = new SmsConfig();
smsConfig.setId(config.getId());
smsConfig.setDomain(config.getDomain().trim());
smsConfig.setName(config.getName().trim());
smsConfig.setPlatform(config.getPlatform().trim());
smsConfig.setAccessKeyId(config.getAccessKeyId().trim());
smsConfig.setAccessKeySecret(config.getAccessKeySecret().trim());
if (StringUtils.isNotBlank(config.getOther())) {
LinkedHashMap linkedHashMap = JSON.parseObject(config.getOther(), LinkedHashMap.class);
smsConfig.setOtherConfig(linkedHashMap);
}
//动态拼接要创建的bean实例的全类名
String className = "com.itheima.sms.sms." + config.getPlatform().trim() + "SmsService";
log.info("准备通过反射动态创建:{}",className);
Class<?> aClass = Class.forName(className);
//获得类的构造方法对象
Constructor<?> constructor = aClass.getConstructor(SmsConfig.class);
//创建bean对象
Object beanService = constructor.newInstance(smsConfig);
//bean对象中的signatureService和templateService属性需要进行赋值
SignatureServiceImpl signatureService = SpringUtils.getBean(SignatureServiceImpl.class);
TemplateServiceImpl templateService = SpringUtils.getBean(TemplateServiceImpl.class);
//根据反射获得类中声明的属性对象
Field signatureServiceField = aClass.getSuperclass().getDeclaredField("signatureService");
Field templateServiceField = aClass.getSuperclass().getDeclaredField("templateService");
//设置可以操作当前属性值
signatureServiceField.setAccessible(true);
templateServiceField.setAccessible(true);
//为bean对象设置属性值
signatureServiceField.set(beanService,signatureService);
templateServiceField.set(beanService,templateService);
beanList.add(beanService);
}catch (Exception e){
e.printStackTrace();
}
});
//3、将每个通道的Bean对象保存到CONNECT_LIST集合中
if(!CONNECT_LIST.isEmpty()){
CONNECT_LIST.clear();
}
CONNECT_LIST.addAll(beanList);
log.info("将初始化的通道加载到集合中:{}",CONNECT_LIST);
}
将我们的工具类加载到我们的内存中,我们调用的时候只需要通过level就能找到对应的通道的工具类了
调用的时候我们如何调用,我们现在的调用方式是 new 出具体的第三方的工具类 然后调用第三方的send方法
这里我们介绍策略模式
学习猫狗案例
策略模式—主要掌握Comparable和Comparator的Demo
策略模式主要解决了什么问题:
解决的是同一种方法多种实现的问题,比如排序,我要按照年龄排序,我又要按照身高排序,我又要按照体重排序
我并不能每次都会建立一个比较器,每次重写对应的方法,那么我的比较功能的代码就会非常的多,
我要有年龄的排序代码,我要有身高的排序代码,我要有体重的排序代码等等
这样做就是方法重写,我们只有一个比较的方法,而具体比较的实现交由每个类自己实现,我们的子类只需要重写对应的方法即可
然后我们的问题又出现了,加入我作为一直猫,我对于第一只猫想要按照体重排序,第二只猫我希望采用身高来进行排序
这个时候我们通过方法重写就完全不能满足要求了,方法重写的时候我们不能重复重写,只能重写一次, 那么我们就不能解决多种功能的需求
而策略模式就是解决这个问题。 当然通过匿名内部类的方式也可以解决这一问题,但是匿名内部类的方式是写死的,并无法为其他类进行调用
所以建议采用策略模式
策略模式是将对应的实现写到具体的类中,并且实现统一的接口,在我们使用对应的方法的时候我们需要把具体的类(具体的比较器类)
告诉我们的方法
从而保证一个类的行为或其算法可以在运行时更改
策略模式的优缺
优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。
缺点: 1、策略类会增多,代码量增大。 2、所有策略类都需要对外暴露。
使用场景:
1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,
那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
2、一个系统需要动态地在几种算法中选择一种。
3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
对于我们的系统来说我们的策略模式使用在什么地方了
首先我们所有第三方的工具类都继承自我们的抽象类abstract class AbstractSmsService
并且里面有一个send方法,该send方法是真正调用到我们的工具类中的方法
我们执行调用的时候 是不是直接调用abstractSmsService.send()方法就行了 我们只要提前将我们的abstractSmsService的实现类确定好调用是不是就利用了多态的方式进行调用
我们来看我们server服务的业务逻辑,参考流程图
配合代码进行讲解
现在我们的代码有没有问题
如果我们在执行过程中manager服务对通道的内容进行了修改我们应该怎么样,由于我们将我们的数据读到了内存中,并且根据config里的内容实例化出了工具类
这些信息是不是没有被同步啊
这样导致我们manager服务修改的数据不能及时的同步到我们的server服务上去
我们需要在manager服务里做一个通知server服务更新内存消息的方法
改方法需要:
1)获取所有有用的redis服务,通过SERVER_ID_HASH里拿到所有的通道id
2)判断时间是否大于5分钟 如果大于5分钟代表 服务已经和redis断开连接了,需要删除掉这个服务
3)如果服务正常,往这个TOPIC_HIGH_SERVER这个key里存放一个初始化通道的消息
这里利用的是redis的发布订阅模式 采用广播的形式 发送消息
public void sendUpdateMessage() {
// TODO 发送消息,通知短信发送服务更新内存中的通道优先级
Map map = redisTemplate.opsForHash().entries("SERVER_ID_HASH");
log.info("所有的短信发送服务实例:" + map);
long currentTimeMillis = System.currentTimeMillis();
for (Object key : map.keySet()) {
Object value = map.get(key);
long parseLong = Long.parseLong(value.toString());
if(currentTimeMillis - parseLong > (1000 * 60 * 5)) {
//删除redis中缓存的可用通道,因为通道优先级发生变化,redis中缓存的可用通道需要重新加载
redisTemplate.delete("listForConnect");
}else{
//说明当前这个实例状态正常
ServerTopic serverTopic = ServerTopic.builder().option(ServerTopic.INIT_CONNECT).value(key.toString()).build();
// ServerTopic simpleEntityTopic = new ServerTopic(ServerTopic.INIT_CONNECT, key.toString());
//发送消息
redisTemplate.convertAndSend("TOPIC_HIGH_SERVER",serverTopic.toString());
// redisTemplate.convertAndSend("TOPIC_HIGH_SERVER",simpleEntityTopic.toString());
return;
}
}
}
在server服务这端需要接收manager服务的消息
第一步 声明spring组件@Component 实现MessageListener接口
重写onMessage方法当有方法调用的时候就会接收到消息
将消息取出转对象ServerTopic
判断对象里的option是什么 如果是init_connect需要我们重新初始化对象
我们需要重新执行Init方法
@Override
public void onMessage(Message message, byte[] pattern) {
//TODO 消息监听,根据消息内容调用smsConnectLoader进行通道初始化或者通道更新
//将消息体进行反序列化,得到json字符串
String jsonMsg = redisTemplate.getDefaultSerializer().deserialize(message.getBody()).toString();
//将json字符串封装成ServerTopic对象
ServerTopic serverTopic = JSON.parseObject(jsonMsg, ServerTopic.class);
switch (serverTopic.getOption()){
case ServerTopic.INIT_CONNECT://初始化通道
smsConnectLoader.initConnect();
break;
// case ServerTopic.USE_NEW_CONNECT://更新通道
// smsConnectLoader.changeNewConnect();
default:
break;
}
}