7、【对线面试官】synchronized

今天我们来聊聊synchronized吧?

  1. synchronized是一种互斥锁,一次只能允许一个线程进入被锁住的代码块
  2. synchronized是Java的一个关键字,它能够将代码块/方法锁起来
  3. 如果synchronized修饰的是实例方法,对应的锁则是对象实例
  4. 如果synchronized修饰的是静态方法,对应的锁则是当前类的Class实例
  5. 如果synchronized修饰的是代码块,对应的锁则是传入synchronized的对象实例

嗯,要不你来讲讲synchronized的原理呗?

  1. 通过反编译可以发现
  2. 当修饰方法时,编译器会生成ACC_SYNCHRONIZED关键字用来标识
  3. 当修饰代码块时,会依赖monitorenter和monitorexit指令
  4. 但前面已经说了,无论synchronized修饰的是方法还是代码块,对应的锁都是一个实例(对象)
  5. 在内存中,对象一般由三部分组成,分别是对象头、对象实际数据和对齐填充
  6. 重点在于对象头,对象头又由几部分组成,但我们重点关注对象头Mark Word的信息就好了
  7. Mark Word会记录对象关于锁的信息
  8. 又因为每个对象都会有一个与之对应的monitor对象,monitor对象中存储着当前持有锁的线程以及等待锁的线程队列
  9. 了解Mark Word和monitor对象是理解synchronized原理的前提

嗯,听说synchronized锁在JDK1.6之后做了很多的优化,这块你了解多少呢?

  1. 其实是这样的,在JDK1.6之前是重量级锁,线程进入同步代码块/方法时
  2. monitor对象就会把当前进入线程的Id进行存储,设置Mark Word的monitor对象地址,并把阻塞的线程存储到monitor的等待线程队列中
  3. 它加锁是依赖底层操作系统的mutex相关指令实现,所以会有用户态和内核态之间的切换,性能损耗十分明显
  4. 而JDK1.6以后引入偏向锁和轻量级锁在JVM层面实现加锁的逻辑,不依赖底层操作系统,就没有切换的消耗
  5. 所以,Mark Word对锁的状态记录一共有4种:无锁、偏向锁、轻量级锁和重量级锁

简单来说说偏向锁、轻量级锁和重量级锁吧

  1. 偏向锁指的就是JVM会认为只有某个线程才会执行同步代码(没有竞争的环境)
  2. 所以在Mark Word会直接记录线程ID,只要线程来执行代码了,会比对线程ID是否相等,相等则当前线程能直接获取得到锁,执行同步代码
  3. 如果不相等,则用CAS来尝试修改当前的线程ID,如果CAS修改成功,那还是能获取得到锁,执行同步代码
  4. 如果CAS失败了,说明有竞争环境,此时会对偏向锁撤销,升级为轻量级锁。
  5. 在轻量级锁状态下,当前线程会在栈帧下创建Lock Record,LockRecord会把 Mark Word的信息拷贝进去,且有个Owner指针指向加锁的对象由由
  6. 线程执行到同步代码时,则用CAS试图将Mark Word的指向到线程栈帧的LockRecord,假设CAS修改成功,则获取得到轻量级锁
  7. 假设修改失败,则自旋(重试),自旋一定次数后,则升级为重量级锁
  8. 简单总结一下
    • synchronized锁原来只有重量级锁,依赖操作系统的mutex指令,需要用户态和内核态切换,性能损耗十分明显
    • 重量级锁用到monitor对象而偏向锁则在Mark Word记录线程ID进行比对、轻量级锁则是拷贝Mark Word到Lock Record,用CAS+自旋的方式获取。
  9. 引入了偏向锁和轻量级锁,就是为了在不同的使用场景使用不同的锁,进而提高效率。
    锁只有升级,没有降级
    • 只有一个线程进入临界区,偏向锁
    • 多个线程交替进入临界区,轻量级锁
    • 多线程同时进入临界区,重量级锁
l

8、【对线面试官】AQS & ReentrantLock

今天我们来聊聊lock锁吧?

你知道什么叫做公平和非公平锁吗

  1. 公平锁指的就是:在竞争环境下,先到临界区的线程比后到的线程一定更快地获取得到锁
  2. 那非公平就很好理解了:先到临界区的线程未必比后到的线程更快地获取得到锁

如果让你实现的话,你怎么实现公平和非公平锁?

  1. 公平锁可以把竞争的线程放在一个先进先出的队列上
  2. 只要持有锁的线程执行完了,唤醒队列的下一个线程去获取锁就好了
  3. 非公平锁的概念上面已经提到了:后到的线程可能比前到临界区的线程获取得到锁
  4. 那实现也很简单,线程先尝试能不能获取得到锁,如果获取得到锁了就执行同步代码了
  5. 如果获取不到锁,那就再把这个线程放到队列呗
  6. 所以公平和非公平的区别就是:线程执行同步代码块时,是否会去尝试获取锁。
  7. 如果会尝试获取锁,那就是非公平的。如果不会尝试获取锁,直接进队列,再等待唤醒,那就是公平的。

为什么要进队列呢?线程一直尝试获取锁不就行了么?

  1. 一直尝试获取锁,专业点就叫做自旋,需要耗费资源的。
  2. 多个线程一直在自旋,而且大多数都是竞争失败的,哪有人会这样实现的
  3. 不会吧,不会吧,你不会就是这样实现的吧

那上次面试所问的synchronized锁是公平的还是非公平的?

  1. 非公平的。
  2. 偏向锁很好理解,如果当前线程ID与markword存储的不相等,则CAS尝试更换线程ID,CAS成功就获取得到锁了
  3. CAS失败则升级为轻量级锁
  4. 轻量级锁实际上也是通过CAS来抢占锁资源(只不过多了拷贝Mark Word到Lock Record的过程)
  5. 抢占成功到锁就归属给该线程了,但自旋失败一定次数后升级重量级锁
  6. 重量级锁通过monitor对象中的队列存储线程,但线程进入队列前,还是会先尝试获取得到锁,如果能获取不到才进入线程等待队列中
  7. 综上所述,synchronized无论处理哪种锁,都是先尝试获取,获取不到才升级||放到队列上的,所以是非公平的

嗯,讲得挺仔细的。AQS你了解吗?

  1. 嗯嗯,AQS全称叫做AbstractQueuedSynchronizer
  2. 是可以给我们实现锁的一个 「框架」,内部实现的关键就是维护了一个先进先出的队列以及state状态变量
  3. 先进先出队列存储的载体叫做Node节点,该节点标识着当前的状态值、是独占还是共享模式以及它的前驱和后继节点等等信息
  4. 简单理解就是:AQS定义了模板,具体实现由各个子类完成。
  5. 总体的流程可以总结为:会把需要等待的线程以Node的形式放到这个先进先出的队列上,state变量则表示为当前锁的状态。
  6. 像ReentrantLock、 ReentrantReadWrite Lock、 CountDownLatch、 Semaphore 这些常用的实现类都是基于AQS实现的
  7. AQS支持两种模式:独占(锁只会被一个线程独占)和共享(多个线程可同时执行)

你以ReentrantLock来讲讲加锁和解锁的过程呗

  • 以非公平锁为了,我们在外界调用lock方法的时候,源码是这样实现的
    1. CAS尝试获取锁,获取成功则可以执行同步代码
    2. CAS获取失败,则调用acquire方法acquire方法实际上就是AQS的模板方法
    3. acquire首先会调用子类的tryAcquire 方法(又回到了ReentrantLock中)
    4. tryAcquire方法实际上会判断当前的state是否等于0,等于0说明没有线程持有锁,则又尝试CAS直接获取锁
    5. 如果CAS获取成功,则可以执行同步代码
    6. 如果CAS获取失败,那判断当前线程是否就持有锁,如果是持有的锁,那更新state的值,获取得到锁(这里其实就是处理可重入的逻辑)
    7. CAS失败&&非重入的情况,则回到try Acquire方法执行「入队列」的操作
    8. 将节点入队列之后,会判断「前驱节点」是不是头节点,如果是头结点又会用CAS尝试获取锁
    9. 如果是「前驱节点」是头节点并获取得到锁,则把当前节点设置为头结点,并且将前驱节点置空(实际上就是原有的头节点已经释放锁了)
    10. 没获取得到锁,则判断前驱节点的状态是否为SIGNAL,如果不是,则找到合法的前驱节点,并使用CAS将状态设置为SIGNAL
    11. 最后调用park将当前线程挂起

你说了一大堆,麻烦使用压缩算法压缩下加锁的过程。

压缩后:当线程CAS获取锁失败,将当前线程入队列,把前驱节点状态设置为SIGNAL状态,并将自己挂起。

为什么要设置前驱节点为 SIGNAL状态,有啥用?

  1. 其实就是表示后继节点需要被唤醒,你咋啥都不知道啊?跟你沟通有点烦.我先把解锁的过程说下吧
    • 外界调用unlock方法时,实际上会调用AQS的release方法,而release方法会调用子类tryRelease方法(又回到了ReentrantLock中)
    • tryRelease会把state一直减(锁重入可使state>1),直至到0,说明当前线程已经把锁释放了
    • 随后从队尾往前找节点状态需要<0,并离头节点最近的节点进行唤醒
  2. 唤醒之后,被唤醒的线程则尝试使用CAS获取锁,假设获取锁得到则把头节点给干掉,把自己设置为头节点成
  3. 解锁的逻辑非常简单哈
  4. 压缩一下:把state置0,唤醒头结点下一个合法的节点,被唤醒的节点线程自然就会去获取锁
  5. 回到上一个问题,为什么要设置前驱节点为SIGNAL状态
  6. 其实归终结底就是为了判断节点的状态,去做些处理。
  7. Node中节点的状态有4种,分别是:CA NCELLED(1)、 SIGNAL(-1)、 CONDITI ON(-2)、 PROPAGATE(-3)和0。
  8. 在ReentrantLock解锁的时候,会判断节点的状态是否小于0,小于等于0才说明需要被唤醒
  9. 另外一提的是:公平锁的实现与非公平锁是很像的,只不过在获取锁时不会直接尝试使用CAS来获取锁。
  10. 只有当队列没节点并且state为0时才会去获取锁,不然都会把当前线程放到队列中

流程图

l

9、【对线面试官】线程池

今天来聊聊线程池呗,你对Java线程池了解多少?

或者换个问法:为什么需要线程池?

  1. JVM在HotSpot的线程模型下,Java线程会一对一映射为内核线程
  2. 这意味着,在Java中每次创建以及回收线程都会去内核创建以及回收
  3. 这就有可能导致:创建和销毁线程所花费的时间和资源可能比处理的任务花费的时间和资源要更多
  4. 线程池的出现是为了提高线程的复用性以及固定线程的数量!!!

你在项目中用到了线程池吗?

  1. 嗯,用到的。我先说下背景吧
  2. 我所负责的项目是消息管理平台,提供其中一个功能就是:运营会圈定人群,然后群发消息
  3. 主要流程大致就是:创建模板-》定时-》群发消息-》用户收到消息
  4. 运营圈定的人群实际上在模板上只是一个ID,我这边要通过ID去获取到HDFS文件
  5. 对HDFS文件进行遍历,然后继续往下发
  6. 「接收到定时任务,再对HDFS进行遍历」这里的处理,我用的就是线程池处理

为什么选择用线程池呢?

  1. HDFS遍历其实就是IO的操作,我把这个过程给异步化,为了提高系统的吞吐量,于是我这里用的线程池。
  2. 即便遍历HDFS出现问题,我这边都有完备的监控和告警可以及时发现。

那你是怎么用线程池的呢?用Executors去创建的吗?

  1. 不是的,我这边用的ThreadPoolExecutor去创建线程池
  2. 其实看阿里巴巴开发手册就有提到,不要使用Executors去创建线程。
  3. 最主要的目的就是:使用ThreadPoolExecutor创建的线程你是更能了解线程池运行的规则,避免资源耗尽的风险
  4. ThreadPoolExecutor在构造的时候有几个重要的参数,分别是:
    corePoolSize (核心线程数量) 、maxim umPoolSize(最大线程数量)、keepAli veTime(线程空余时间) 、workQueue(阻塞队列)、handler(任务拒绝策略)
  5. 这几个参数应该很好理解哈,我就说下任务提交的流程,分别对应着几个参数的作用吧。
    • 首先会判断运行线程数是否小于corePoolSize,如果小于,则直接创建新的线程执行任务
    • 如果大于corePoolSize,判断workQueue阻塞队列是否已满,如果还没满,则将任务放到阻塞队列中
    • 如果workQueue阻塞队列已经满了,则判断当前线程数是否大于maximumPoolSize,如果没大于则创建新的线程执行任务
    • 如果大于maximumPoolSize,则执行任务拒绝策略(具体就是你自己实现的handler)
  6. 这里有个点需要注意下,就是workQueu e阻塞队列满了,但当前线程数小于maximumPoolSize,这时候会创建新的线程执行任务
  7. 源码就是这样实现的
  8. 不过一般我们都是将corePoolSize和maximumPoolSize设置相同数量
  9. keepAliveTime指的就是,当前运行的线程数大于核心线程数了,只要空闲时间达到了,就会对线程进行回收

那我再问一个问题,你创建线程池肯定会指定线程数的嘛,你这块是怎么考量的。

  1. 线程池指定线程数这块,首先要考量自己的业务是什么样的
  2. 是cpu密集型的还是io密集型的,假设运行应用的机器CPU核心数是N
  3. 那cpu密集型的可以先给到N+1,io密集型的可以给到2N去试试
  4. 上面这个只是一个常见的经验做法,具体究竟开多少线程,需要压测才能比较准确地定下来
  5. 线程不是说越大越好,在之前的面试我也提到过,多线程是为了充分利用CPU的资源
  6. 如果设置的线程过多,线程大量有上下文切换,这一部分也会带来系统的开销,这就得不偿失了

ThreadPoolExecutor你看过源码吗?

  1. 看过的,其实上面说的ThreadPoolExecutor几个参数,在源码的顶部注释都有
  2. 在执行的时候,重点就在于它维护了一个ctl参数,这个ctl参数的用高3位表示线程池的状态,低29位来表示线程的数量
  3. 里边用到了大量的位运算符操作,具体细节我就忘了,但是流程还是上面所讲的
l

面试官:“谈谈Spring中都用到了那些设计模式?”。

JDK 中用到了那些设计模式?Spring 中用到了那些设计模式?这两个问题,在面试中比较常见。我在网上搜索了一下关于 Spring 中设计模式的讲解几乎都是千篇一律,而且大部分都年代久远。所以,花了几天时间自己总结了一下,由于我的个人能力有限,文中如有任何错误各位都可以指出。另外,文章篇幅有限,对于设计模式以及一些源码的解读我只是一笔带过,这篇文章的主要目的是回顾一下 Spring 中的常见的设计模式。

Design Patterns(设计模式) 表示面向对象软件开发中最好的计算机编程实践。 Spring 框架中广泛使用了不同类型的设计模式,下面我们来看看到底有哪些设计模式?

控制反转(IoC)和依赖注入(DI)

IoC(Inversion of Control,控制翻转) 是Spring 中一个非常非常重要的概念,它不是什么技术,而是一种解耦的设计思想。它的主要目的是借助于“第三方”(Spring 中的 IOC 容器) 实现具有依赖关系的对象之间的解耦(IOC容易管理对象,你只管使用即可),从而降低代码之间的耦合度。IOC 是一个原则,而不是一个模式,以下模式(但不限于)实现了IoC原则。

图片

ioc-patterns

Spring IOC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 IOC 容器负责创建对象,将对象连接在一起,配置这些对象,并从创建中处理这些对象的整个生命周期,直到它们被完全销毁。

在实际项目中一个 Service 类如果有几百甚至上千个类作为它的底层,我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IOC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。关于Spring IOC 的理解,推荐看这一下知乎的一个回答:https://www.zhihu.com/question/23277575/answer/169698662 ,非常不错。

控制翻转怎么理解呢? 举个例子:”对象a 依赖了对象 b,当对象 a 需要使用 对象 b的时候必须自己去创建。但是当系统引入了 IOC 容器后, 对象a 和对象 b 之前就失去了直接的联系。这个时候,当对象 a 需要使用 对象 b的时候, 我们可以指定 IOC 容器去创建一个对象b注入到对象 a 中”。 对象 a 获得依赖对象 b 的过程,由主动行为变为了被动行为,控制权翻转,这就是控制反转名字的由来。

DI(Dependecy Inject,依赖注入)是实现控制反转的一种设计模式,依赖注入就是将实例变量传入到一个对象中去。

工厂设计模式

Spring使用工厂模式可以通过 BeanFactoryApplicationContext 创建 bean 对象。

两者对比:

  • BeanFactory :延迟注入(使用到某个 bean 的时候才会注入),相比于BeanFactory来说会占用更少的内存,程序启动速度更快。
  • ApplicationContext :容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。

ApplicationContext的三个实现类:

  1. ClassPathXmlApplication:把上下文文件当成类路径资源。
  2. FileSystemXmlApplication:从文件系统中的 XML 文件载入上下文定义信息。
  3. XmlWebApplicationContext:从Web系统中的XML文件载入上下文定义信息。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

public class App {
public static void main(String[] args) {
ApplicationContext context = new FileSystemXmlApplicationContext(
"C:/work/IOC Containers/springframework.applicationcontext/src/main/resources/bean-factory-config.xml");

HelloApplicationContext obj = (HelloApplicationContext) context.getBean("helloApplicationContext");
obj.getMsg();
}
}

单例设计模式

在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。

使用单例模式的好处:

  • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
  • 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。

Spring 中 bean 的默认作用域就是 singleton(单例)的。 除了 singleton 作用域,Spring 中 bean 还有下面几种作用域:

  • prototype : 每次请求都会创建一个新的 bean 实例。
  • request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
  • session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。
  • global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话

Spring 实现单例的方式:

  • xml:``
  • 注解:@Scope(value = "singleton")

Spring 通过 ConcurrentHashMap 实现单例注册表的特殊方式实现单例模式。Spring 实现单例的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
synchronized (this.singletonObjects) {
// 检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//...省略了很多代码
try {
singletonObject = singletonFactory.getObject();
}
//...省略了很多代码
// 如果实例对象在不存在,我们注册到单例注册表中。
addSingleton(beanName, singletonObject);
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
//将对象添加到单例注册表
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

}
}
}

代理设计模式

代理模式在 AOP 中的应用

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:

图片

SpringAOPProcess

当然你也可以使用 AspectJ ,Spring AOP 已经集成了AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。

使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。

Spring AOP 和 AspectJ AOP 有什么区别?

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。

模板方法

模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。

图片

模板方法UML图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public abstract class Template {
//这是我们的模板方法
public final void TemplateMethod(){
PrimitiveOperation1();
PrimitiveOperation2();
PrimitiveOperation3();
}

protected void PrimitiveOperation1(){
//当前类实现
}

//被子类实现的方法
protected abstract void PrimitiveOperation2();
protected abstract void PrimitiveOperation3();

}
public class TemplateImpl extends Template {

@Override
public void PrimitiveOperation2() {
//当前类实现
}

@Override
public void PrimitiveOperation3() {
//当前类实现
}
}

Spring 中 jdbcTemplatehibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。

观察者模式

观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。

Spring 事件驱动模型中的三种角色

事件角色

ApplicationEvent (org.springframework.context包下)充当事件的角色,这是一个抽象类,它继承了java.util.EventObject并实现了 java.io.Serializable接口。

Spring 中默认存在以下事件,他们都是对 ApplicationContextEvent 的实现(继承自ApplicationContextEvent):

  • ContextStartedEventApplicationContext 启动后触发的事件;
  • ContextStoppedEventApplicationContext 停止后触发的事件;
  • ContextRefreshedEventApplicationContext 初始化或刷新完成后触发的事件;
  • ContextClosedEventApplicationContext 关闭后触发的事件。

图片

ApplicationEvent-Subclass

事件监听者角色

ApplicationListener 充当了事件监听者角色,它是一个接口,里面只定义了一个 onApplicationEvent()方法来处理ApplicationEventApplicationListener接口类源码如下,可以看出接口定义看出接口中的事件只要实现了 ApplicationEvent就可以了。所以,在 Spring中我们只要实现 ApplicationListener 接口实现 onApplicationEvent() 方法即可完成监听事件

1
2
3
4
5
6
package org.springframework.context;
import java.util.EventListener;
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
void onApplicationEvent(E var1);
}

事件发布者角色

ApplicationEventPublisher 充当了事件的发布者,它也是一个接口。

1
2
3
4
5
6
7
8
@FunctionalInterface
public interface ApplicationEventPublisher {
default void publishEvent(ApplicationEvent event) {
this.publishEvent((Object)event);
}

void publishEvent(Object var1);
}

ApplicationEventPublisher 接口的publishEvent()这个方法在AbstractApplicationContext类中被实现,阅读这个方法的实现,你会发现实际上事件真正是通过ApplicationEventMulticaster来广播出去的。具体内容过多,就不在这里分析了,后面可能会单独写一篇文章提到。

Spring 的事件流程总结

  1. 定义一个事件: 实现一个继承自 ApplicationEvent,并且写相应的构造函数;
  2. 定义一个事件监听者:实现 ApplicationListener 接口,重写 onApplicationEvent() 方法;
  3. 使用事件发布者发布消息: 可以通过 ApplicationEventPublisherpublishEvent() 方法发布消息。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 定义一个事件,继承自ApplicationEvent并且写相应的构造函数
public class DemoEvent extends ApplicationEvent{
private static final long serialVersionUID = 1L;

private String message;

public DemoEvent(Object source,String message){
super(source);
this.message = message;
}

public String getMessage() {
return message;
}


// 定义一个事件监听者,实现ApplicationListener接口,重写 onApplicationEvent() 方法;
@Component
public class DemoListener implements ApplicationListener<DemoEvent>{

//使用onApplicationEvent接收消息
@Override
public void onApplicationEvent(DemoEvent event) {
String msg = event.getMessage();
System.out.println("接收到的信息是:"+msg);
}

}
// 发布事件,可以通过ApplicationEventPublisher 的 publishEvent() 方法发布消息。
@Component
public class DemoPublisher {

@Autowired
ApplicationContext applicationContext;

public void publish(String message){
//发布事件
applicationContext.publishEvent(new DemoEvent(this, message));
}
}

当调用 DemoPublisherpublish() 方法的时候,比如 demoPublisher.publish("你好") ,控制台就会打印出:接收到的信息是:你好

适配器模式

适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。

spring AOP中的适配器模式

我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter 。Advice 常用的类型有:BeforeAdvice(目标方法调用前,前置通知)、AfterAdvice(目标方法调用后,后置通知)、AfterReturningAdvice(目标方法执行结束后,return之前)等等。每个类型Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptorAfterReturningAdviceAdapterAfterReturningAdviceInterceptor。Spring预定义的通知要通过对应的适配器,适配成 MethodInterceptor接口(方法拦截器)类型的对象(如:MethodBeforeAdviceInterceptor 负责适配 MethodBeforeAdvice)。

spring MVC中的适配器模式

在Spring MVC中,DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由HandlerAdapter 适配器处理。HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。

为什么要在 Spring MVC 中使用适配器模式? Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样:

1
2
3
4
5
6
7
if(mappedHandler.getHandler() instanceof MultiActionController){  
((MultiActionController)mappedHandler.getHandler()).xxx
}else if(mappedHandler.getHandler() instanceof XXX){
...
}else if(...){
...
}

假如我们再增加一个 Controller类型就要在上面代码中再加入一行 判断语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。

装饰者模式

装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个Decorator套在原有代码外面。其实在 JDK 中就有很多地方用到了装饰者模式,比如 InputStream家族,InputStream 类下有 FileInputStream (读取文件)、BufferedInputStream (增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream 代码的情况下扩展了它的功能。

图片

装饰者模式示意图

Spring 中配置 DataSource 的时候,DataSource 可能是不同的数据库和数据源。我们能否根据客户的需求在少修改原有类的代码下动态切换不同的数据源?这个时候就要用到装饰者模式(这一点我自己还没太理解具体原理)。Spring 中用到的包装器模式在类名上含有 Wrapper或者 Decorator。这些类基本上都是动态地给一个对象添加一些额外的职责

总结

Spring 框架中用到了哪些设计模式:

  • 工厂设计模式 : Spring使用工厂模式通过 BeanFactoryApplicationContext 创建 bean 对象。
  • 代理设计模式 : Spring AOP 功能的实现。
  • 单例设计模式 : Spring 中的 Bean 默认都是单例的。
  • 模板方法模式 : Spring 中 jdbcTemplatehibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
  • 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
  • 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
  • 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller
  • ……

参考

l

个人博客系统设计(支持hexo和halo同步)

  1. 本文主要介绍自己的博客系统是如何设计的,并使用Halo博客同步器 将hexo(git pages: https://linshenkx.github.io )文章自动同步到halo( http://linshenkx.cn )。
    实现一次编写、两套博客系统并存、多个网址访问的效果。

一 总览

达到效果

个人博客网址 介绍 对应git仓库/管理界面
https://linshenkx.gitee.io hexo next gitee pages https://gitee.com/linshenkx/linshenkx
https://linshenkx.github.io hexo next github pages https://github.com/linshenkx/linshenkx.github.io
https://linshen.netlify.app netlify加速,文章同步自blog源码仓库 https://app.netlify.com/teams/linshenkx
https://linshenkx.cn halo个人网站,文章同步自blog源码仓库 https://linshenkx.cn/admin/index.html#/dashboard

blog博客源码仓库(核心,私有):https://github.com/linshenkx/blog

博客发布流程

  1. 编写博客
    在blog工程下写博客,工程为标准hexo,博客为markdown文件放在source/_posts目录下,使用多层级分类存放
  2. 发布到git pages
    完成博客的增删改后,在工程目录下执行hexo clean && hexo d -g部署到git pages。
    这里我配置了同时发布到github和gitee,需要注意的是,gitee的git pages需要手动去触发更新才能生效。
  3. 提交并推送工程
    提交并推送blog工程的修改。
    netlify将自动获取blog工程,并执行hexo部署脚本(效果和git pages一样,只是用netlify访问据说会快一点)
    自己开发的Halo博客同步器也会检测到blog工程更新,根据更新情况将变化同步到halo博客系统中。

二 设计思路

1 起因

本来我一直是在使用csdn的,但是网页端写作确实不方便,而且还可能受网络情况限制。
所以我后面一般都是用印象笔记做记录,在印象笔记写好再看心情整理到csdn上去。
但是悄不注意的,在21年初csdn改版,同时也改变了排名和引流规则。
之前一个星期2500到3000的访问量现在只剩1500到2000了。

嗯,不可忍。换。

2 调研

市面上的博客系统可根据对Git Pages的支持(即是否支持生成静态网站)分为两大类:

一是以hexo为代表的静态网站生成器:如hexo、hugo、jekyll,较成熟,有较多第三方主题和插件,可与git pages搭配使用,也可自行部署。

二是以halo为代表的五花八门的个人博客系统,功能更加强大,自由度更高,通常带后台管理,但不支持git pages,需自行部署。

3 分析

个人博客的话使用git pages比较稳定,网址固定,可以永久使用,而且可以通过搭配不同的git服务商来保证访问速度。
但是git pages的缺点也很明显,是静态网站,虽然可以搭配第三方插件增强,但说到底还是个静态网站。

而如果自己买服务器,买域名,用第三方个人博客系统,就可以玩得比较花里胡哨了,但谁知道会用多久呢。
服务器、域名都要自己负责,三五年之后还能不能访问就比较难说了。
但是年轻人嘛,总还是花里胡哨点才香。

那我就全都要。

git pages作为专业性较强的个人网站可以永久访问,
然后再弄个服务器放个博客系统自己玩。

4 选型

静态网站生成器选的是hexo,传统一点,支持的插件和主题比较多。
hugo虽然也不错,但似乎国内用的不多,支持可能还不够完善。

然后hexo的主题用的最经典的next,比较成熟,功能也很完善
虽然整体比较严肃压抑,但可以自己加个live2d增添点活力,
作为一个展示专业性的博客网站这样也就够了

自定义博客系统的话我选的是halo,最主要原因是它是java写的,利于二次开发(事实上后面用着也确实有问题,还提交了一个issue)
而且功能比较强大,生态比较完善,虽然第三方主题少且基本都没更新,但是…实在是找不出其他一个能打的了
另外halo支持导入markdown,且功能基本都通过rest接口放开,适合开发者使用

三 设计实现

1 hexo

hexo本身只是静态网站生成器,你可以把hexo项目本身发布成为git pages项目,
像github、gitee这些会识别出这是一个hexo项目,然后进行编译,得到静态资源供外部访问。
这也是最简单的用法。

但是不推荐。

因为git pages项目一般都要求是public的(且名称固定,一个git账号只有一个git pages仓库),
hexo项目包含你的博客markdown源文件和其他的个人信息。
我们只是想把必要的生成后的静态网页放出去而已,至于项目的配置信息和markdown源文件应该藏起来。

所以需要使用 hexo-deployer-git 插件进行git pages的部署。
即放到git公开的文件只有生成后的网页文件而已,git只是把你生成后的index.html进行直接展示,不会再去编译了
(需要在source目录下添加.nojekyll文件表明为静态网页,无须编译)

而项目本身为了更好地进行管理和记录,还是要发布到git上面的,作为一个普通的私有仓库,名称可以任意(如 blog)

这样,每次要增删改完文章只需要执行hexo clean && hexo d -g即可发布到git仓库上
注意,不同git服务商git pages规则不一样。
比方说我gitee和github的用户名都是linshenkx
但是gitee要求的仓库名是linshenkx,而github的仓库名就必须是linshenkx.github.io了
而github的git pages仓库在接收到推送后就自动(编译)部署
gitee则需要到仓库web界面手动触发更新

截至到这一步是大多数人的做法,即git上两个仓库并存,一(或多)个git pages公有仓库做展示,一个blog仓库存放博客源码
注意:如果git pages仓库允许私有,则可以使用一个仓库多个分支来实现相同效果。
但还是推荐使用两个仓库,因为这样更通用,设计上也更合理。

工程总体结构如下,为普通hexo工程:
img
博客源码目录结构如下,为多层级结构:
img

2 halo

halo的使用看官方文档一般就够了,这里需要补充的是其代理配置。
因为halo的在线下载更新主题功能通常需要连接到github,我习惯通过代理访问
这里提供一下配置方法
即在容器启动时添加JVM参数即可

1
2
docker run -it -d --name halo --network host -e JVM_OPTS="-Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=7890 -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=7890" -v /opt/halo/workspace:/root/.halo --restart=always halohub/halo

3 markdown图片

markdown图片的存放一直是个麻烦的问题。
最害怕遇到就是图链的失效,而且往往自己还不能发现。
理想状态下就是markdown一张图片支持配置多个图床链接,第一个图床链接超时就使用下一个。
这种服务端的处理思想很明显不适合放到客户端。
退而求其次,配置一个链接,访问这个链接会触发对多个图床的访问,然后那个快用那个。
这个效果技术上不难实现,也有个商业产品(聚合图床)是这样的,缺点是收费。
然后我又在github、gitee上找了各个图床软件,都不怎么样(这个时间成本都够给聚合图床开几年会员了)
最终还是妥协,用云存储吧,选了阿里
七牛、腾讯也都试了,其实都差不多,看个人爱好,没有太特别的理由

如果你用typora写markdown的话很方便,它支持picgo插件

但我习惯在idea里面编写,idea也有一些markdown-image插件,基本都不好用
所以我还是安装了picgo,开了快捷键,复制图片直接快捷键粘贴体验也还是比较舒服的
picgo的特点是插件多,不过插件质量一般,有很多bug

花了两天时间纠结、测试,最后的方案是:idea编辑+阿里云存储+picgo上传

4 同步

这才是重点

1 同步的方向

即在哪里写文章,同步到哪里

我还是习惯用idea写markdown文档而不是在网页上。
所以确定是流向为 hexo->halo

2 技术支撑

halo支持导入markdown文件,所以主要问题为hexo的markdown博客源码文件的获取
hexo文章存储路径为 source/_posts ,有多层级文件夹,可以简单地理解成文件IO操作获取文章内容。
但关键是存储在git上,这里可以用JGit进行操作。
同时,JGit支持获取两次commit之间的文件变化情况。
即可以捕获到文章的增删改操作,而不用每次都全量地同步。

3 成果

又处理了一些细节问题,最终还是自己做了个haloSyncServer同步程序,
封装成docker,放服务器上跑,实现同步。
待整理后开源。

2021年11月更新
开源地址为:https://github.com/linshenkx/haloSyncServer
效果
img

l

法拍房

以前玩过法拍房。

答案是:买卖不破租赁,没辙儿。

而且存在租赁的情况法院必然会在拍卖前告知,可能你冲动之下不看仔细就瞎jb拍了。人家租赁合同估计做得也没啥原则性的漏洞(正常情况下房主计划断供之前就准备好一切了,还能等到拍卖完了才想起来临时抱佛脚?),否则你也不会跑来知乎问问题。

imgimg知乎懂哥太多了,民法典上写得清清楚楚的东西还要来杠

给想玩法拍房的人一个建议。

以前我玩法拍房,都是先找银行的熟人把银行的不良贷款列一份名单给我。

img

看上哪套房子,就联系银行把该房的债权买下来,(银行-〉有资质的第三方机构-〉个人,具体步骤结尾有补充)(即房主欠银行的钱变成房主欠我的钱)。银行本来就不是拍卖机构,拍卖周期又可能比较长,银行一般急着回笼资金节约流程巴不得有人把债权买走,还会打折卖。之后自己成了债权人,就可以在拍卖信息上把联系电话留成自己的。

有买家想了解这套房,就会打我的电话,这时我会告诉买家这套房有一些问题,比如就存在题主说的“有超长租赁合同”之类的。买家一听这房子藏雷,就不打算买了。

最后想买的买家全都被我劝退,没有任何人和我竞争,这时我再让合伙人出手竞拍(债权人不能买,所以需要一个合伙人),于是这套房子就被我以起拍价买下来了。

你们要买法拍房的可以参考这个方法。


没想到这里还有杠精,是因为我把这路子免费分享出来,知道的人多了会挡你财路?

补充细节:

1,债权不能直接从银行到个人,需要经手有资质的第三方机构,比如拍卖行。具体步骤:挑好房子谈好折扣——打款到拍卖行——和银行签债转协议——银行债转公示——公证处债权公证——法院出债转文书——诉讼主体变更——接手债权。

2,法拍房如果出现意外情况,处理较繁琐。我以前只出资投债权赚差价,不沾房子本身。不认识熟人的散户建议联系专门的拍卖中介机构,不会多收你几个钱。中介也是按上面说的这些流程走的。


最后声明:

知乎杠精太多了。

我回答这个问题仅仅只是因为关注动态里蹦出来了这个问题,而我正好以前在这个领域玩过一段时间,所以顺手答一下。我本人是原神和vtb领域的答主,对金融方面的流量不感兴趣,更何况还是知乎这种公认变现价值低的劣质流量。

另外本人自前年底去年初住建部全国巡视指导发政策后就不再玩房了,不打广告不为自己引流(当然你可以去b站关注一下嘉然),也不接受网友任何投资与资金往来。答主绝不可能以任何形式问你要钱、拉你进任何形式的群聊、让你注册任何形式的账号等等。

imgimg

l

问答系统总结

1.开发工具GIt,idea

  • 我是把代码放到github上的,以便于代码版本管理,不能拷贝来拷贝去
  • java语言/规范最基础,要研究jvm,垃圾回收算法思路,根据什么样的规则,怎么回收申请的一些对象;参数可以配置,怎么配置,对应的后台运行又是怎么样的

    2.Spring Boot,Velocity

  • spring框架:spring是怎么做的,view、controller、service是怎么连在一起的:spring是控制反转、依赖注入,解决了数据数据初始化一些问题,能够吧代码写得很简洁,然后脱颖而出,核心是怎么做的,数据初始化是怎么做的。面向切面的编程可以用在什么地方,spring的一般框架是mvc
    :层是controller、中间层是service、底层是dao。为什么这么分?他比其他的区别是什么?对一些request包装了
    找spring一两点深入研究:我不仅仅是用了spring,我还研究了他某一个组件,他是怎么实现的?讲讲(闪光点)

  • velocity(模板语言)
    我用了velocity,我发现了velocity里的很多东西和我学的java、c++都是一样的,他这个框架他把一些公用的都提取出来,前面的spring boot是个java框架,为什么要用velocity,因为我要前后端分离,view和后面的数据要分离,data是怎么传过来的,怎么解析的,他支持什么东西?(不仅仅用了这个东西,还有思想,解耦)

    3.mybatis

  • 怎么把数据库的一些前后的读取给做了,怎么把xml里的多重条件,怎么做文法的解析,然后把这些条件给处理掉。

    4.登录/注册

  • 网站安全(salt):密码为什么加了一个salt就变得安全?

  • 通过拦截器来实现的:拦截器的思想、框架,留好接口;拦截器实现登录注册:我在cookie里放了一个token,token怎么处理用户登录注册的:在用户登录注册的时候,会下发一个token,把token与用户信息关联起来,关联起来之后我为了优化token信息,把token放到数据库里(redis),设计一个分布式的统一登录系统,现在的互联网产品都是统一登录的,比如,登录qq之后,登录网页就不需要登录了,qq登录过的token直接注入到网页上去。这是个ssension共享问题:、

  • 保证数据安全:验证(邮件激活)

    5.前缀树

  • 构造一个前缀树,通过一个有限状态机来实现一传文本是不是包含敏感词,繁杂度是多少 很重要:优点有哪些?为什么不用kmp,文本查找算法:
    可以很快的加一些词汇过来;有扩展性,以及性能更提高

    6.redis

  • 数据结构:跳列表,哈希,优先队列,list:我了解redis底层是怎么实现的,为什么他的效率很高,他的字符串是怎么保存的,做这个工程的时候我用在的异步队列上、排序上、异步框架

    7.异步框架

  • 思路:我这个网站附带的每一步操作可能附带的操作都非常多,为了更快的吧结果返回给用户,所以采用异步框架,自己写的,数据结构:使用redis的队列,因为redis能够保证线程同步;除了用队列,我还想过用有优先队列,这样我的异步框架能够把紧急的任务线处理掉。我这个异步框架:有消息的发射,消息的处理,事件的模型定义以及具体执行的eventhandle,我定义了一些公共接口把这些实现了。

    8.邮件(smtp协议)

  • 做了一个简单邮件,怎么连接上服务器,我当时做这个的时候,ssl问题
    ,ssl理解,服务器需要ssl链接,为了安全服务器是怎么做的;java sdk 1.7 1.8的问题,1.8是需要换一个jar包的

  • 豆瓣电影排序:好的问题能挑选出来,互动越多,时间越新,评分越高。

    9.timeline(时间轴)

  • 肯定会问:为什么用推拉模式,用推实时性高能让好友快速得到消息,用拉能节省僵尸号、不是活跃用户的存储空间。怎么区分?最后把timeline组合起来‘timeline模板系统,每个新鲜事展向不一样,和velocity结合起来,后台存储的都是核心数据,每个数据对应的是一个模板,我把模板结合起来,我就能快速的把时间轴展示出来。

    10.爬虫

    11.solr搜索

  • 搜索去重:对比相似度,敏感哈希算法,哈希算法:两个字符串稍微有一点点不一样,结构就是不一样的。可能头尾是不一样的,内容一样:采用敏感哈希算法把相似度求出来,区别:敏感哈希算法两个文档相似度很高,他生成的哈希值的比例是很相似的。

    12.单元测试/部署

  • 部署:运维,llinux nigix反向代理,与正向对比。负载均衡:为什么要负载均衡。

l

问答系统总结2

1.开发工具GIt,idea

  • 我是把代码放到github上的,以便于代码版本管理,不能拷贝来拷贝去
  • java语言/规范最基础,要研究jvm,垃圾回收算法思路,根据什么样的规则,怎么回收申请的一些对象;参数可以配置,怎么配置,对应的后台运行又是怎么样的

    2.Spring Boot,Velocity

  • spring框架:spring是怎么做的,view、controller、service是怎么连在一起的:spring是控制反转、依赖注入,解决了数据数据初始化一些问题,能够吧代码写得很简洁,然后脱颖而出,核心是怎么做的,数据初始化是怎么做的。面向切面的编程可以用在什么地方,spring的一般框架是mvc
    :层是controller、中间层是service、底层是dao。为什么这么分?他比其他的区别是什么?对一些request包装了
    找spring一两点深入研究:我不仅仅是用了spring,我还研究了他某一个组件,他是怎么实现的?讲讲(闪光点)
  • velocity(模板语言)
    我用了velocity,我发现了velocity里的很多东西和我学的java、c++都是一样的,他这个框架他把一些公用的都提取出来,前面的spring boot是个java框架,为什么要用velocity,因为我要前后端分离,view和后面的数据要分离,data是怎么传过来的,怎么解析的,他支持什么东西?(不仅仅用了这个东西,还有思想,解耦)

    3.mybatis

  • 怎么把数据库的一些前后的读取给做了,怎么把xml里的多重条件,怎么做文法的解析,然后把这些条件给处理掉。

    4.登录/注册

  • 网站安全(salt):密码为什么加了一个salt就变得安全?
  • 通过拦截器来实现的:拦截器的思想、框架,留好接口;拦截器实现登录注册:我在cookie里放了一个token,token怎么处理用户登录注册的:在用户登录注册的时候,会下发一个token,把token与用户信息关联起来,关联起来之后我为了优化token信息,把token放到数据库里(redis),设计一个分布式的统一登录系统,现在的互联网产品都是统一登录的,比如,登录qq之后,登录网页就不需要登录了,qq登录过的token直接注入到网页上去。这是个ssension共享问题:、
  • 保证数据安全:验证(邮件激活)

    5.前缀树

  • 构造一个前缀树,通过一个有限状态机来实现一传文本是不是包含敏感词,繁杂度是多少 很重要:优点有哪些?为什么不用kmp,文本查找算法:
    可以很快的加一些词汇过来;有扩展性,以及性能更提高

    6.redis

  • 数据结构:跳列表,哈希,优先队列,list:我了解redis底层是怎么实现的,为什么他的效率很高,他的字符串是怎么保存的,做这个工程的时候我用在的异步队列上、排序上、异步框架

    7.异步框架

*思路:我这个网站附带的每一步操作可能附带的操作都非常多,为了更快的吧结果返回给用户,所以采用异步框架,自己写的,数据结构:使用redis的队列,因为redis能够保证线程同步;除了用队列,我还想过用有优先队列,这样我的异步框架能够把紧急的任务线处理掉。我这个异步框架:有消息的发射,消息的处理,事件的模型定义以及具体执行的eventhandle,我定义了一些公共接口把这些实现了。

8.邮件(smtp协议)

  • 做了一个简单邮件,怎么连接上服务器,我当时做这个的时候,ssl问题
    ,ssl理解,服务器需要ssl链接,为了安全服务器是怎么做的;java sdk 1.7 1.8的问题,1.8是需要换一个jar包的
  • 豆瓣电影排序:好的问题能挑选出来,互动越多,时间越新,评分越高。

    9.timeline(时间轴)

  • 肯定会问:为什么用推拉模式,用推实时性高能让好友快速得到消息,用拉能节省僵尸号、不是活跃用户的存储空间。怎么区分?最后把timeline组合起来‘timeline模板系统,每个新鲜事展向不一样,和velocity结合起来,后台存储的都是核心数据,每个数据对应的是一个模板,我把模板结合起来,我就能快速的把时间轴展示出来。

    10.爬虫

    11.solr搜索

  • 搜索去重:对比相似度,敏感哈希算法,哈希算法:两个字符串稍微有一点点不一样,结构就是不一样的。可能头尾是不一样的,内容一样:采用敏感哈希算法把相似度求出来,区别:敏感哈希算法两个文档相似度很高,他生成的哈希值的比例是很相似的。

    12.单元测试/部署

  • 部署:运维,llinux nigix反向代理,与正向对比。负载均衡:为什么要负载均衡。
l

2020年工作总结

1.工作内容

  • 开发、上线了60多家机构模板

  • 完成64万多历史未走识别模板报告导入athena处理的数据清洗

  • 完成word、ppt、xps等格式报告转pdf功能的调研和开发

  • 日常的机构模板异常、改动的修复

  • 根据相应需求对识别服务做了一些改动和优化

    2. 收获

  • 对模板可能出现的问题,积累了一定的经验,可以快速的定位解决

  • 对面向对象编程有了更深的理解,分层次展现、分级别访问、封装对象之间各种关系

  • 工作的条理性更加清晰,能够分清主次和轻重缓急。在开发时间仓促的情况下,能够详细计划。

  • 能够更快地理解每个需求背后的逻辑和需要做的一些事情

    3. 好的、坏的

好的

  • 通过学习、调研,能够独立解决一些疑难问题
  • 沟通、做事的逻辑有所提升,能够独立完成一部分工作,承担自己的责任。
  • 代码更加规范了

坏的

  • 业务中所用到的技术,理解得还不够全面

  • 业务开发中遇到的问题,经验欠缺,处理起来还不够熟练

    4. 计划

  • 一个项目中,涉及的技术很多,有的不懂的需要加强学习

  • 争取明年4月份,识别率能够达到80%吧

l

git切换分支保存修改的代码的方法

git之 分支切换时相互影响的问题

git下分支的应用是很方便的一个功能,但是有一个问题,如果我在分支A有工作尚未完成,想要跳到分支B,如果不注意,可能搞乱你的工作。

首先说,如果你的分支A工作区和缓存区是干净的(即你在A分支commit之后再没做任何更改),你随便往别的分支跳都不会有影响的。但是如果你在A分支下有未完成的工作,即你用git status看显示有没有add或者commit的内容,你往B分支checkout的时候,会把你在A分支下的工作带过去,如图:
这里写图片描述
在命令$git checkout B之后显示的A A1 和A A2意思即为提醒使用者,有未提交的工作也一起跳转到分支B上啦(前面的大写A意味着A1,A2文件是新建且已经git add的文件;如果是大写M则意味着A1,A2文件为内容有更改的原有文件;D则意味着是删除了A1,A2文件)。这个时候你如果在B分支上进行其他工作而不编辑A1,A2文件,目前来说是没问题的。然而,一旦你在B分支上完成了某项工作,运行了commit命令,A1,A2文件之前的更改也会在A分支上进行提交,而git的规矩是,在那个分支上进行的提交,就算哪个分支上的工作。

也就是说,一旦你把A分支上尚未完成的工作带到了B分支上并在B分支上顺利提交,那么你本来希望是在A分支上进行的工作,则会被提交到本地库中B分支上,该部分工作在A分支下用git log命令查看不到但是在B分支下则可以查看到。这在实际的工作中会导致你的两个分支乱掉或者出现提交冲突。不是不能补救,但是会很麻烦,所以要尽量避免。

那么怎么避免呢?事实上,在比较旧的版本的git下,你在分支A下有未完成的工作的情形下,是无法跳转到分支B下的,这就很好的避免了后续的尴尬情况,不过目前版本的git是允许你带着未完成工作进行跳转的,所以你可以通过以下手段来避免这种情况下搞乱你的工作:

1.跳转分支之前git status一下查看是不是有没有add和commit的工作,如果有,可以的话,就都提交掉。(事实上尚未add的工作带到了新分支下如果不继续对该文件进行处理,带过去也是没有影响的,大不了跳回来再带回来嘛,反正木有add过的内容在新分支下commit也不会把这部分工作提交。)
2.如果确实有尚未add和commit的工作,但是并未完成不方便进行提交,可以利用git stash进行现场保留,然后跳转。(git stash的用法也是一块比较重要的内容,这里暂不详细介绍了,可以直接百度其用法~)
3.如果1.2你都没有做,很不小心地带着未commit的工作跳转到了另一分支下,跳转之后的提示可以让你意识到你把先前分支的工作带过来了,不做任何修改直接再跳回去就好(就又带回去了),然后进行1或2步中所说。

方法

最近在一个原有的项目上做一次非常大的改版,底层的数据库做了很大的变化,跟现在的版本无法兼容。现在的工作除了开发最新的版本之外还要对原来的版本做例行的维护,修修补补。于是有了在两个分支之间游走切换的问题,最新改版的代码在分支new上,旧版本的代码在分支old上,我在new上开发了一半,忽然有人给了我一个改进的需求,于是我要切换回old去修改代码。在这个场景下,我摸索了三种方法:

及时commit代码

在new分支上把已经开发完成的部分代码commit掉,不push,然后切换到old分支修改代码,做完了commit,所有分支互不影响,这是一个理想的方法。

使用git stash

有时候写了一半的JAVA代码,都还不能编译通过的,就被叫去改另一个分支的bug了。

在new分支上的时候在命令行输入:

1
git stash

或者

1
git stash save “修改的信息"

这样以后你的代码就回到自己上一个commit了,直接git stash的话git stash的栈会直接给你一个hash值作为版本的说明,如果用git stash save “修改的信息”,git stash的栈会把你填写的“修改的信息”作为版本的说明。

接下来你回到old分支修改代码完成,你又再回到new分支,输入:

1
git stash pop

或者

1
2
git stash list
git stash apply stash@{0}

就可以回到保存的版本了。git stash pop的作用是将git stash栈中最后一个版本取出来,git stash apply stash@{0}的作用是可以指定栈中的一个版本,通过git stash list可以看到所有的版本信息:

1
2
stash@{0}: On order-master-bugfix: 22222
stash@{1}: On order-master-bugfix: 22222

然后你可以选择一个你需要的版本执行:

1
git stash apply stash@{0}

这时候你搁置的代码就回来了。

这是一个非常常用的场景,我正在一个分支上修改功能,然后遇到一个bug需要解决,我得切换到其他分支来修改这个bug,但是目前的功能还在开发阶段,还不成熟,还不想执行add和commit,执行这两个后就会在历史中有记录,并不想这样做,于是就有了git stash功能,把我当前的修改暂时保存起来,然后回来的时候再取出来继续开发功能.

git stash是针对整个git工程来进行保存的,也就是说区分不了branch.比如我在a分支git stash save “sss”暂存了一个修改,那么我切换到b分支,我使用git stash pop 就能把在a分支保存的”sss”这个修改同步到了b分支上.所以当我们需要在不同的分支上取出不同的分支上保存的修改,那么就用到了git stash list,这个命令可以把在所有分支上暂存的信息显示出来,然后我们通过 git stash apply stash@{0} 来选择恢复哪个暂存,stash@{0}这个会在list中列出来.

而我们使用Android studio就太方便了.

在当前工程的任何一个文件中,点击右键,选择git–> 选择repository —> 里面会列出stash changes和unstash changes命令,一个是保存修改的命令,一个是恢复修改的命令.

stash changes会让我们给要保存的内容输入一个message,这个和git stash save “”是一样的

img

而 unstash changes会列表我们之前保存过的list

img

可以很方便的恢复我们之前的保存的内容.

l