Java初中级面试-框架篇
Spring
优缺点
优点
- 解耦;(IOC容器、DI操作)
- 简化开发;(学会的前提下)
- 支持AOP;(面向切面编程)
- 轻量级,非侵入式,支持整合各种框架(不排斥其它框架);
- 内部集成了多种服务,便于使用;(事务管理,jpa,各个模板Template类,ws远程调用)
- Spring支持用户自定义某些组件或执行流程,方便更替,拓展性良好;
缺点
- 有技术使用门槛;
- 由于使用了框架,整合其它技术可能存在兼容性问题;
- 由于框架程序加载,服务启动效率不如原生;
- 某些bug的出现,是源自对框架底层的不了解,出现问题难以排查解决;(回归第一点)
常用注解
@Controller
- MVC-控制器;
- 在SpringMVC中,控制器Controller负责处理由DispatcherServlet分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model ,然后再把该Model返回给对应的View进行展示;
- 用于处理DispatcherServlet分发的请求;
@RestController
- @RestController注解是从Spring 4.0引入的,用于简化RESTful 接口的创建;
- @RestController = @Controller + @ResponseBody;
- 响应客户端JSON数据;
@RequestParam
- 将请求参数绑定到你控制器的方法参数上;
- 用于接收url或body的k=v的这种传参方式的参数;
@PathVariable
- 可以绑定URL路径占位符参数到方法参数中;
- /user/{id} ----> @PathVariable(“id”) Integer id;
@RequestBody
- 接收前端请求的请求体中的JSON或XML数据,将其转换为指定的对象来接收;JSON用得较多
- 比如前端请求的content-type为application/json或者是application/xml等。一般情况下来说常用其来处理application/json类型。
@ResponseBody
- 直接向客户端响应JSON字符串;
- 会将Controller方法返回的对象转JSON字符串,响应客户端;
@RequestMapping
- 是用来映射请求的,可以用它来指定映射关系;
- 最终Spring MVC会通过反射及元数据相关的技术,建立HTTP请求跟具体处理器及方法的关系,用于请求的具体分发操作;
- 这个注解会将 HTTP 请求映射到 MVC 和 REST 控制器的处理方法上;
@Service
- 业务处理类,Spring会创建业务处理对象交给IOC;
- @Service对应的是业务层Bean;
@Repository
- @Repository对应数据访问层Bean;
- 用于对Dao实现类注解;
@Bean
- 将方法返回结果对象作为Bean保存到IOC中,如果没有指定Bean的名称,则默认以方法名称作为Bean的id(name)保存;
- 主要用在@Configuration注解的Java配置类里;
@Component
- 定义组件级别的Bean对象,交给IOC管理;
- 泛指各种组件,就是说当我们的类不属于各种归类的时候(不属于@Controller、@Services等的时候),我们就可以使用@Component来标注这个类。
- 把普通类实例化到spring容器中;
@Configuration
- 定义配置类;
- 可替换xml配置文件;
- 被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器;
@Autowired
- 用在属性上,从IOC容器中,基于属性类型找到Bean,注入当前属性;
- @Autowired是用在JavaBean中的注解,通过byType形式,用来给指定的字段或方法注入所需的外部资源;
- 它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qWPrwPl0-1644844846673)(.\素材\image-20200916115220146.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7qdPSMsB-1644844846676)(.\素材\image-20200916115306712.png)]
@Resource
(Spring支持的JSR-250规范定义的注解,非Spring框架注解)
- @Resource和@Autowired注解都是用来实现依赖注入的。只是@AutoWried按byType自动注入,而@Resource默认按byName自动注入;
- @Resource(name=“beanName”)
@Autowired 与@Resource的区别:
1、 @Autowired与@Resource都可以用来装配bean. 都可以写在字段上,或写在setter方法上。
2、 @Autowired默认按类型装配(这个注解是属业spring的),默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用,如下:
@Autowired
@Qualifier("baseDao")
private BaseDao baseDao;
3、@Resource(这个注解属于J2EE的),默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行安装名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。
@Resource(name="baseDao")
private BaseDao baseDao;
推荐使用:@Resource注解在字段上,这样就不用写setter方法了,并且这个注解是属于J2EE的,减少了与spring的耦合。这样代码看起就比较优雅。
@Qualifier
- 可以 为 方法注入 指定名称的Bean 作为 方法参数;
- 假如一个接口写有两个实现类,那么如果用@Autowired引用的话会报错。
可以用@Autowired+@Qualifire(“userServiceImpl01”) == @Resource(name=“userServiceImpl01”) ; - @Primary作用于实现类上。当一个接口有多个实现类时 并且用@Autowired类型匹配 @Primary作用于那一个实现类上就表示默认调用那个实现类;如下:
结果: UserServiceImpl01
@Scope
- 定义bean存在的方式(作用域);
- 在Spring IoC 容器中具有以下几种作用域,基本作用域(singleton,prototype),web作用域(request,session,globalsession),自定义作用域;
- singleton:单例模式;@Scope(value = “singleton”) ----- 默认方式
- prototype:原型模式,每次通过容器getBean方法获取protetype定义bean,都将产生一个新的bean实例;
- 几乎90%以上的业务使用singleton单实例就可以,所以spring默认的类型也是singleton,singleton虽然保证了全局是一个实例,对性能有所提高,但是某些场景下需要考虑并发安全问题;(如某个单例bean对象中有个成员属性,多线程并发修改、设置其值)
Score-proxyMode见:https://www.cnblogs.com/maplesnow/p/11628034.html
@Transactional
- 声明式事务,开启事务;
- 声明式事务基于AOP,将具体业务逻辑与事务处理解耦。
@Order
- 注解@Order或者接口Ordered的作用是定义Spring IOC容器中Bean的执行顺序的优先级,而不是定义Bean的加载顺序,Bean的加载顺序不受@Order或Ordered接口的影响;
- 默认是最低优先级,值越小优先级越高;
实现CommandLineRunner接口的类会在Spring IOC容器加载完毕后执行,适合预加载类及其它资源;也可以使用ApplicationRunner,使用方法及效果是一样的;
结果:
----YellowPersion----
----BlackPersion----
额外补充:
InitializingBean的运行比CommandLineRunner运行的早;
@PostConstruct会在实现 InitializingBean 接口的afterPropertiesSet()方法之前执行;
Spring初始化bean的操作 在 ApplicationRunner和CommandLineRunner接口调用之前;
@Async
原理见:
https://mp.weixin.qq.com/s/Pqo8Mr2D5yhjBSPYERU8Lw
具体使用:
https://www.jianshu.com/p/3d31727fe3c5
Spring容器启动初始化bean时,判断类中是否使用了@Async注解,创建切入点和切入点处理器,根据切入点创建代理,在调用@Async注解标注的方法时,会调用代理,执行切入点处理器invoke方法,将方法的执行提交给线程池,实现异步执行。
需要注意的一个错误用法是,如果A类的a方法(没有标注@Async)调用它自己的b方法(标注@Async)是不会异步执行的,因为从a方法进入调用的都是它本身,不会进入代理。 跟方法嵌套事务失效是一个原因。
- Spring3开始提供了@Async注解,该注解可以被标注在方法上,以便异步地调用该方法;
- 底层实现,动态代理、线程池、Callable、Future(FutureTask);
- 可以用于优化服务端性能;
案例代码
环境:基于SpringBoot 2.1.4
启动类:@EnableAsync
开启允许异步操作
// 配置类 自定义 异步执行的 线程池
@Configuration
public class AsycnConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("taskExecutor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
// 测试 service
@Service
@Slf4j
public class TestService {
// 同步方法1
public String testReturnA_1(){
log.info("serviceA: " + Thread.currentThread().getName() + " tid: " + Thread.currentThread().getId());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "testA";
}
// 同步方法2
public String testReturnB_1(){
log.info("serviceB: " + Thread.currentThread().getName() + " tid: " + Thread.currentThread().getId());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "testB";
}
// 异步方法1
@Async("taskExecutor")
public Future<String> testReturnA(){
log.info("serviceA: " + Thread.currentThread().getName() + " tid: " + Thread.currentThread().getId());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new AsyncResult<>("testA");
}
// 异步方法2
@Async("taskExecutor")
public Future<String> testReturnB(){
log.info("serviceB: " + Thread.currentThread().getName() + " tid: " + Thread.currentThread().getId());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new AsyncResult<>("testB");
}
}
// 皮包类 用于 伪装 无实际逻辑意义 -- 接收 异步 方法 结果
public class AsyncResult<V> implements Future<V> {
private final V value;
public AsyncResult(V value) {
this.value = value;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return true;
}
@Override
public V get() throws InterruptedException, ExecutionException {
return this.value;
}
@Override
public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
return this.value;
}
}
// 暴露测试接口
@Slf4j
@RequestMapping
@RestController
public class TestController {
@Resource
TestService testService;
@GetMapping("/test1")
public String test() {
log.info("controller 1: " + Thread.currentThread().getName() + " tid: " + Thread.currentThread().getId());
String a = testService.testReturnA_1();
String b = testService.testReturnB_1();
try {
log.info("controller1 asyn finishes");
return a + "-" + b;
} catch (Exception e) {
log.error(e.getMessage(), e);
return "error";
}
}
@GetMapping("/test2")
public String test1() {
log.info("controller 2: " + Thread.currentThread().getName() + " tid: " + Thread.currentThread().getId());
Future<String> futureA = testService.testReturnA();
Future<String> futureB = testService.testReturnB();
System.out.println(futureA.getClass().getName());
try {
String a = futureA.get();
String b = futureB.get();
log.info("controller2 asyn finishes");
return a + "-" + b;
} catch (Exception e) {
log.error(e.getMessage(), e);
return "error";
}
}
结论
从结果来看,采用异步方式,可以缩短请求的时间。可以用这种方式来对服务端某些接口做性能优化;
IOC
spring ioc指的是控制反转,IOC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。交由Spring容器统一进行管理,从而实现松耦合;
“控制反转”,不是什么技术,而是一种设计思想。
设计目的
解耦, 为了解决对象之间的耦合度过高的问题;
不用IOC的系统
对象之间耦合度过高的系统,必然会出现牵一发而动全身的情形;
用了IOC的系统
对象间的关系由IOC来维护,A、B、C、D四个对象再也没有相关耦合,拆解方便,组装、替换也方便;
详见: https://www.cnblogs.com/superjt/p/4311577.html
底层实现用到的技术
基于XML实现
- 加载xml配置文件,读取配置文件;— XML技术
- 反射机制:对配置文件中的类名使用反射机制可以实现类加载初始化等工作,利用反射实例化bean,加载到IOC容器Map,也可以调用类的方法进行属性注入,也可以根据field对象执行器set来实现属性注入,java.lang.reflect提供了反射相关的工具;
- 实例化完成后,基于bean初始化配置,可以利用反射进一步执行bean的初始化方法;
基于注解方式实现
- 获取基础扫描包; – 从配置文件或 启动类 获取 扫描包
- 扫描获取所有Class; – 文件读取技术
- 解析Class,获取Class及Method的注解; – 反射 及 注解 元数据 相关技术
- 基于反射实例化Bean,封装到对应容器Map; – 反射 (此过程如果类中有AOP相关注解体现,还需要利用动态代理技术获取相关代理对象)
- 基于反射及注解相关配置实现属性依赖注入; – 反射
- 基于反射及注解配置,执行Bean初始化方法; – 反射
MVC
MVC流程
主要概念
前端控制器:DispatcherServlet
处理器映射器:HandlerMapping
处理器执行链:HandlerExecutionChain
处理器适配器:HandlerAdapter
视图解析器:ViewResolver
流程
- 客户端请求到达前端控制器-DispatcherServlet;
- 前端控制器DispatcherServlet请求处理器映射器HandlerMapping;
- 处理器映射器HandlerMapping根据HTTP相关URI查找相应的处理器(Handler),返回处理器执行链HandlerExecutionChain给前端控制器DispatcherServlet;
- 前端控制器DispatcherServlet请求处理器适配器HandlerAdapter;
- 处理器适配器HandlerAdapter执行处理器Handler,生成ModelAndView,返回ModelAndView给前端控制器DispatcherServlet;
- 前端控制器DispatcherServlet请求视图解析器ViewResolver;
- 视图解析器ViewResolver返回视图对象给前端控制器DispatcherServlet;
- 最后渲染视图,通过View返回给请求者;
实现原理
- 围绕Servlet实现;
- SpringMVC由Tomcat以web.xml里一个Servlet一个Listener的配置触发启动,然后以这两个建立IOC容器体系,最终进行组件的初始化工作,启动完成。
- 基于IOC,结合Servlet基于HTTP协议利用反射执行具体处理方法是其实现的核心技术;
总结
Spring MVC启动了两个IOC容器用于构建Spring应用,通过DispatcherServlet拦截Http请求,通过handlerMapping和handlerAdapter获取handler处理得到ModelAndView后,再通过View对象将Model包装在Http请求中,通过RequestDispatcher完成请求的进一步转发给真正的视图进行处理并返回http响应。
AOP
概要
Spring对面向切面编程提供了强有力的支持,通过它让我们将业务逻辑从应用服务(如事务管理)中分离出来,实现了高内聚开发(所谓高内聚是指一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则。),应用对象只关注业务逻辑,不再负责其它系统问题(如日志、事务等)。Spring支持用户自定义切面,实现面向切面编程。
实现原理
在Spring IOC跟DI的基础下,底层实则是用的动态代理(JDK动态代理、CGLib动态代理)技术来实现的AOP面向切面编程。
Spring哪些地方使用了哪些设计模式
- 工厂模式,这个很明显bai,在各种BeanFactory以及duApplicationContext创建中都用到了;
- 代理模式,在AOP实现中用到了JDK、CGLib的动态代理;
- 策略模式,第一个地方,加载资源文件的方式,使用了不同的方法,比如:ClassPathResourece,FileSystemResource,ServletContextResource,UrlResource但他们都有共同的借口Resource;第二个地方就是在Aop的实现中,采用了两种不同的方式,JDK动态代理和CGLIB代理;
- 单例模式,这个比如在创建bean的时候;
- 观察者模式,定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
spring中Observer模式常用的地方是listener的实现。如ApplicationListener; - 模板方法模式, 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤,spring中的JdbcTemplate;
Spring Bean 生命周期
Spring Bean的生命周期分为
四个阶段
和多个扩展点
。扩展点又可以分为影响多个Bean
和影响单个Bean
。整理如下:
四个阶段
- 实例化 Instantiation
- 属性赋值 Populate
- 初始化 Initialization
- 销毁 Destruction
多个扩展点
- 影响多个Bean
- BeanPostProcessor
- InstantiationAwareBeanPostProcessor
- 影响单个Bean
- Aware
- Aware Group1
- BeanNameAware
- BeanClassLoaderAware
- BeanFactoryAware
- Aware Group2
- EnvironmentAware
- EmbeddedValueResolverAware
- ApplicationContextAware(ResourceLoaderAware\ApplicationEventPublisherAware\MessageSourceAware)
- 生命周期
- InitializingBean
- DisposableBean
SpringBoot
相比于Spring的优势
- 内置web容器,可以以jar包独立部署启动;
- Spring Boot提供Spring框架的最大自动化配置,大量使用自动配置,使得开发者对Spring的配置尽量减少;
- Spring Boot更多的是采用 Java Config 的方式,对Spring进行配置,减少过多的xml配置;
- Spring Boot提供了基于http、ssh、telnet对运行时的项目进行监控;我们可以引入 spring-boot-start-actuator 依赖,直接使用 REST 方式来获取进程的运行期性能参数,从而达到监控的目的,比较方便。提供运行时的应用监控。
- 自动管理依赖关系,集成很多启动器,大大降低依赖复杂度;
- 快速构建Spring项目,使用 Spring 项目引导页面可以在几秒构建一个SpringBoot项目,降低开发人员构建Spring项目的难度;
缺点
- 有使用门槛,如果不熟悉底层实现原理,出现某些问题,会更加难以定位及解决;
- 引入某些starter,由于自动配置的原因,可能出现你意想不到、不想看到的错误;
自动配置原理
- SpringBoot 项目启动类,@SpringBootApplication,SpringBoot启动的时候加载主配置类,开启了自动配置功能@EnableAutoConfiguration;
- @EnableAutoConfiguration 作用:利用EnableAutoConfigurationImportSelector给容器中导入一些组件,可以查看selectImports()方法的内容;
- 获取候选的配置;
SpringFactoriesLoader.loadFactoryNames()
扫描所有jar包类路径下 META‐INF/spring.factories
把扫描到的这些文件的内容包装成properties对象
从properties中获取到EnableAutoConfiguration.class类(类名)对应的值,然后把他们添加在容器中
- 每一个这样的 xxxAutoConfiguration类都是容器中的一个组件,都加入到容器中;用他们来做自动配置;
- 每一个自动配置类进行自动配置功能;根据当前不同的条件判断(判断能否加载 某个 依赖 的 starter 中的类),决定这个配置类是否生效?决定当前配置类中的Bean是否加载;
- 一但这个配置类生效;这个配置类就会给容器中添加各种组件;这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
- 所有在配置文件中能配置的属性都是在xxxxProperties类中封装者;配置文件能配置什么就可以参照某个功能对应的这个属性类;
自定义starter流程
-
创建starter模块和autoconfig模块;
-
在starter模块中引入autoconfig模块;
-
构建autoconfig模块,如下;
-
编写***Properties配置实体类,用来接收当前组件的某些配置参数;
-
编写****AutoConfiguration自动配置类;
// 例如 @Configuration//表示这是一个配置类 @ConditionalOnWebApplication//表示web应用才生效 @EnableConfigurationProperties(HelloProperties.class)//使@ConfigruationProperties注解的类生效,只有配加上这个注解,HelloProperties才会生效 public class HelloServiceAutoConfiguration { @Autowired HelloProperties helloProperties; @Bean public HelloService helloService(){ HelloService helloService = new HelloService(); helloService.setHelloProperties(helloProperties); return helloService; } }
-
在resources文件夹下创建META-INF文件夹,在META-INF文件夹下创建spring.factories文件,下面是spring.factories文件的内容;
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.atguigu.starter.HelloServiceAutoConfiguration #这里配置的是自动配置类的全路径
-
在具体SpringBoot项目中引入如下starter依赖坐标,启动即可实现自定义的stater自动配置;
生产环境如何部署SpringBoot项目
部署为Unix/Linux Service
-
项目中pom依赖;
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <executable>true</executable> </configuration></plugin>
-
设置为Linux系统服务;
#将你的应用打成jar包,部署到服务器,假设部署路径为/var/app,包名为app.jar,通过如下方式将应该设置为一个系统服务sudo ln -s /var/app/app.jar /etc/init.d/app
-
赋予可执行权限;
chmod u+x app.jar
-
以系统服务的方式管理具体服务;
#接下来,就可以使用我们熟悉的service foo start|stop|restart来对应用进行启停等管理了sudo service app start|stop#命令将得到形如Started|Stopped [PID]的结果反馈#默认PID文件路径:/var/run/appname/appname.pid#默认日志文件路径:/var/log/appname.log#这可能是我们更熟悉也更常用的管理方式。
-
自定义参数;
#在这种方式下,我们还可以使用自定义的.conf文件来变更默认配置,方法如下:#在jar包相同路径下创建一个.conf文件,名称应该与.jar的名称相同,如appname.conf#在其中配置相关变量,如:JAVA_HOME=/usr/local/jdkJAVA_OPTS=-Xmx1024MLOG_FOLDER=/custom/log
-
服务安全配置;
#作为应用服务,安全性是一个不能忽略的问题,如下一些操作可以作为部分基础设置参考:#为服务创建一个独立的用户,同时最好将该用户的shell绑定为/usr/sbin/nologin#赋予最小范围权限:chmod 500 app.jar#阻止修改:sudo chattr +i app.jar#对.conf文件做类似的工作:chmod 400 app.confsudo chown root:root app.conf
SpringCloud
如何理解分布式
狭义的分布是指,指多台PC在地理位置上分布在不同的地方;
分布式系统:多个能独立运行的计算机(称为节点)组成。各个节点利用计算机网络进行信息传递,从而实现共同的“目标或者任务” ;
分布式程序: 运行在分布式系统上的计算机程序;
分布式计算:利用分布式系统解决来计算问题。在分布式计算里,一个问题被细化成多个任务,每个任务可以被一个或者多个计算机来完成;
区分分布式计算和并行计算:共同点都是大任务划分为小任务。不同点: 分布式计算:基于多台PC,每台PC完成同一任务中的不同部分。分布式的计算被分解后的小任务互相之间有独立性,节点之间的结果几乎不互相影响,实时性要求不高。并行计算:基于同一个台PC,利用CPU的多核共同完成一个任务;
什么是微服务
微服务优缺点
优点:
提升开发交流,每个服务足够内聚,足够小,代码容易理解;
服务独立测试、部署、升级、发布;
按需定制的DFX( 面向产品生命周期的设计 ),资源利用率,每个服务可以各自进行x扩展( 服务实例水平扩展,保证可靠性与性能 )和z扩展( 数据分区,数据独立,可靠性保证; ),而且,每个服务可以根据自己的需要部署到合适的硬件服务器上;
每个服务按需要选择HA的模式,选择接受服务的实例个数;
容易扩大开发团队,可以针对每个服务(service)组件开发团队;
提高容错性(fault isolation),一个服务的内存泄露并不会让整个系统瘫痪;
新技术的应用,系统不会被长期限制在某个技术栈上;
缺点:
微服务提高了系统的复杂度;开发人员要处理分布式系统的复杂性;服务之间的分布式通信问题;服务的注册与发现问题;服务之间的分布式事务问题;数据隔离再来的报表处理问题;服务之间的分布式一致性问题;服务管理的复杂性,服务的编排;不同服务实例的管理。
核心组件介绍
注册中心
Eureka、Nacos、ZK
为各个客户端(微服务)提供服务注册与发现的功能,定时心跳检测,动态维护管理服务地址信息,方便服务间远程调用;
Nacos
nacos支持两种方式的注册中心,持久化和非持久化存储服务信息。
- 非持久直接存储在nacos服务节点的内存中,并且服务节点间采用去中心化的思想,服务节点采用hash分片存储注册信息;-- AP模式;
- 持久化使用Raft协议选举leader节点,同样采用过半机制将数据同步到其它节点上; – CP模式;
AP模式下Nacos集群支持多写多读,即集群中的任一台服务器都能够处理更新服务实例(即注册和摘除实例)的请求,同时也能够处理查询服务实例列表的请求。当集群中的某一台服务器更新本机的服务实例数据之后,会通知集群中的其它服务器更新各自的服务实例数据。在这个过程当中,数据没有在集群中所有的服务器得到同步时,向不同的服务器发送查询请求,可能会得到不同的服务实例列表,但在整个过程中,Nacos集群是持续可用的,也就是舍弃了CAP中的C,保证了AP。
CP模式下集群支持单写多读,即只有集群中的leader节点才能处理更新服务实例(即注册或删除)的请求,而所有的机器都可以处理查询请求。当集群中的非leader节点接收到更新服务实例的请求时,会转发给leader节点进行处理。leader节点更新完本地的服务实例数据之后,会把数据同步到集群中的其它节点。在这个过程当中,集群中的所有节点都是能够提供查询请求的,在数据没有同步完全之前,向不同的服务器发送查询请求,可能会得到不同的服务实例列表。因此,Nacos的CP模式并不严格。另外,在leader节点宕机导致重新选主时,集群不能处理更新服务实例的请求,这是CP模式的特点,即舍弃了A,尽力保证CP(如前所述,Nacos的CP模式不严格)。
详见: https://blog.csdn.net/swordyijianpku/article/details/105355179 、 https://blog.csdn.net/swordyijianpku/article/details/105393459
Zookeeper保证CP
当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30 ~ 120s, 且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。
Eureka保证AP
Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册或时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。
网关
详见: https://zhuanlan.zhihu.com/p/91865256、 https://zhuanlan.zhihu.com/p/101341556
API网关在企业架构中的地位
一个企业随着信息系统复杂度的提高,必然出现外部合作伙伴应用、企业自身的公网应用、企业内网应用等。
在架构上应该将这三种应用区别开,三种应用的安排级别、访问方式也不一样。
因此在我的设计中将这三种应用分别用不同的网关进行API管理,分别是:API网关(OpenAPI合伙伙伴应用)、API网关(内部应用)、API网关(内部公网应用)。
如下图:
- nginx – 静态资源服务器,最外层网关;
- zuul – 微服务网关;
- Gateway – 微服务网关;
服务网关 = 路由转发 + 过滤器
- 路由转发:接收一切外界请求,转发到后端的微服务上去;
- 过滤器:在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等,这些都可以通过过滤器完成(其实路由转发也是通过过滤器实现的);
为什么要有服务网关
上述所说的横切功能(以权限校验为例)可以写在三个位置:
- 每个服务自己实现一遍
- 写到一个公共的服务中,然后其他所有服务都依赖这个服务
- 写到服务网关的前置过滤器中,所有请求过来进行权限校验
第一种,缺点太明显,基本不用;第二种,相较于第一点好很多,代码开发不会冗余,但是有两个缺点:
- 由于每个服务引入了这个公共服务,那么相当于在每个服务中都引入了相同的权限校验的代码,使得每个服务的jar包大小无故增加了一些,尤其是对于使用docker镜像进行部署的场景,jar越小越好;
- 由于每个服务都引入了这个公共服务,那么我们后续升级这个服务可能就比较困难,而且公共服务的功能越多,升级就越难,而且假设我们改变了公共服务中的权限校验的方式,想让所有的服务都去使用新的权限校验方式,我们就需要将之前所有的服务都重新引包,编译部署。
而服务网关恰好可以解决这样的问题:
- 将权限校验的逻辑写在网关的过滤器中,后端服务不需要关注权限校验的代码,所以服务的jar包中也不会引入权限校验的逻辑,不会增加jar包大小;
- 如果想修改权限校验的逻辑,只需要修改网关中的权限校验过滤器即可,而不需要升级所有已存在的微服务。
Zuul或Consol或者Gateway可以接入注册中心,形成一个完整的微服务闭环,全链路动态扩容支持,这也是核心之一。
所以,需要服务网关!
引入网关的注意点
- 增加了网关,多了一层转发(原本用户请求直接访问open-service即可),性能会下降一些(但是下降不大,通常,网关机器性能会很好,而且网关与open-service的访问通常是内网访问,速度很快);
- 网关的单点问题:在整个网络调用过程中,一定会有一个单点,可能是网关、nginx、dns服务器等。防止网关单点,可以在网关层前边再挂一台nginx,nginx的性能极高,基本不会挂,这样之后,网关服务就可以不断的添加机器。但是这样一个请求就转发了两次,所以最好的方式是网关单点服务部署在一台牛逼的机器上(通过压测来估算机器的配置),而且nginx与zuul的性能比较,根据国外的一个哥们儿做的实验来看,其实相差不大,zuul是netflix开源的一个用来做网关的开源框架;
- 网关要尽量轻;(不要将过多的业务逻辑放置在网关)
服务网关基本功能
-
智能路由;-- 接收外部一切请求,并转发到后端的对外服务open-service上去;
注意:我们只转发外部请求,服务之间的请求不走网关,这就表示全链路追踪、内部服务API监控、内部服务之间调用的容错、智能路由不能在网关完成;当然,也可以将所有的服务调用都走网关,那么几乎所有的功能都可以集成到网关中,但是这样的话,网关的压力会很大,不堪重负。
-
权限校验; – 只校验用户向open-service服务的请求,不校验服务内部的请求。服务内部的请求有必要校验吗?
-
API监控;-- 只监控经过网关的请求,以及网关本身的一些性能指标(例如,gc等);
-
限流; – 与监控配合,进行限流操作;
-
API日志统一收集;-- 类似于一个aspect切面,记录接口的进入和出去时的相关日志;
Nginx
Nginx也可以实现网关,那为什么不同Nginx实现网关呢?
因为微服务网关是针对与整个微服务实现统一请求拦截,微服务网关是java语言写的;我们可以在这个基础上对网关进行功能扩展;如:安全控制,统一异常处理,XXS,SQL注入等;权限控制,黑白名单,性能监控,日志打印等;
某些情况下,我们会将Nginx作为最外层的反向代理服务器,实现微服务网关的负载均衡;我们还可以利用Nginx来部署前端资源服务;
对比Spring Cloud Netflix Zuul和Spring Cloud Gateway
Zuul:
使用的是阻塞式的 API,不支持长连接,比如 websockets。
底层是servlet,Zuul处理的是http请求
没有提供异步支持,流控等均由hystrix支持。
依赖包spring-cloud-starter-netflix-zuul。
Gateway:
底层依然是servlet,但使用了webflux,多嵌套了一层框架
依赖spring-boot-starter-webflux和/ spring-cloud-starter-gateway
提供了异步支持,提供了抽象负载均衡,提供了抽象流控,并默认实现了RedisRateLimiter。
负载均衡
Ribbon
客户端负载均衡;(负载均衡的功能在调用方)
负载均衡是对系统的高可用、网络压力的缓解和处理能力扩容的重要手段之一。
Ribbon结合服务发现,通过具体的负载均衡算法,实现客户端负载均衡,完成远程调用。
原理:
当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会被LoadBalancerInterceptor类的intercept函数所拦截。
详见: https://www.jianshu.com/p/1bd66db5dc46;
负载均衡策略
申明式调用
Feign
用于微服务间申明式调用,暴露API接口,底层运用了JDK动态代理,结合服务发现、负载均衡、HTTP客户端等技术进行实现。
Feign是在Ribbon的基础上进行了一次改进,采用申明式接口的方式,实现服务间调用。
断路器
Hystrix (客户端(服务消费者)技术)
在高并发的场景下,使用服务熔断器可以防止在具体的服务调用链路中因某个微服务的问题造成级联失败导致服务雪崩;
详见: https://blog.csdn.net/loushuiyifan/article/details/82702522
具体实现手段:服务隔离、服务降级、服务熔断;
配置中心
Nacos、SpringCloudConfig
常用:Nacos,实现动态配置统一管理实时更新;
微服务监控、链路跟踪 如何实现
SpringBoot服务监控
spring boot actuator 、spring boot admin
// 自行学习 (内容较多)
住着掌握 可以 监控 服务的 哪些信息,为什么要监控?
slueth+zipkin实现微服务链路跟踪
// 自行学习 (内容较多)
着重理解 链路跟踪的 意义,以及能具体监控到 服务间 调用 过程的 哪些数据?
分布式环境下微服务日志如何收集查看
SpringBoot整合logback,结合AOP实现日志数据埋点
// 自行学习 (已拓展)
ELK实现日志收集分析系统
// 自行学习 (内容较多 – 高级)
该知识点可以作为面试核心竞争力!
Mybatis
基本实现原理
利用反射及JDK动态代理技术打通Java类与SQL语句之间的相互转换。
Mybatis的运行过程分为两大步
- 第1步,读取配置文件缓存到Configuration对象,用于创建SqlSessionFactory;
- 第2步,SqlSession的执行过程。相对而言,SqlSessionFactory的创建还算比较容易理解,而SqlSession的执行过程就不那么简单了,它包括许多复杂的技术,要先掌握反射技术和动态代理,这里主要用到的是JDK动态代理;
MyBatis的主要构件及其相互关系
MyBatis的主要的核心部件有以下几个:
- SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能;
- Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护;
- StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合;
- ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数;
- ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
- TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换;
- MappedStatement MappedStatement维护了一条<select|update|delete|insert>节点的封装;
- SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回;
- BoundSql 表示动态生成的SQL语句以及相应的参数信息;
- Configuration MyBatis所有的配置信息都维持在Configuration对象之中;
源码分析见: https://www.cnblogs.com/grasp/p/11161176.html、 https://blog.csdn.net/weixin_43184769/article/details/91126687(易懂版)
Mybatis缓存
缓存说明
缓存可以简单理解为存在于内存中的临时数据;-- 读取效率极高;
查询数据库中的操作算是一个非常常用的操作,但是有些数据会被经常性的查询,而每一次都去数据库中查询这些重复的数据,会很消耗数据库的资源,同时使得查询效率也很低,而 MyBatis 中就通过缓存技术来解决这样的问题,也就是说:将一些经常查询,并且不经常改变的,以及数据的实时正确性对最后的业务结果影响(非关键性判断)不大的数据,放置在一个缓存容器中,当用户再次查询这些数据的时候,就不必再去数据库中查询,直接在缓存中提取就可以了;
MyBatis 提供了 一级缓存和二级缓存两种形式
- 一级缓存:它是 SqlSession 级别的缓存,SqlSession 类的实例对象中提供了一个 HashMap 的结构,可以用于存储缓存数据,当我们再次查询同一数据的时候,MyBatis 会先去 SqlSession 中查询,有的话,就直接调用;
- 二级缓存:是Mapper 级别的缓存,也就是说,如果多个 SqlSession 类的实例,去操作同一个Mapper配置文件中的SQL,这些实例对象可以共用二级缓存;
一级缓存
- 第一次查询 id 为某个值的用户信息时,先去 SqlSesion 的一级缓存中去寻找,如果找到了,就直接用,如果没有找到就去数据库中去查,然后将查到的内容存到一级缓存区域;
- 但是,如果在下一次操作中(同一个会话中),执行了增删改的操作,一级缓存区域内的内容会被清空,这是为了保证缓存中的数据的有效性,避免脏读的产生;
测试 (同一个sqlSession)
如果使用如下方式,则一级缓存不会命中,因为不是在同一个sqlSession中。
@Autowiredprivate TbTestMapper tbTestMapper;@Testpublicvoid userFirstCache1() { for (int i = 0; i < 3; i++) { // 每次Mapper调用findById方法都会创建一个session,导致mybatis一级缓存无法命中 TbTest tbTest = tbTestMapper.findById2(1L); log.info("查询信息:{}", tbTest); }}
如上测试,会发现三次都查询了数据库,为什么呢?这是因为每次Mapper调用findById方法都会创建一个session,并且在执行完毕后关闭session。所以三次调用并不在一个session中,一级缓存并没有起作用。
如果开启事务,则会将事务操作控制在一个事务中,那么Mybatis一级缓存会生效。
@Autowired private TbTestMapper tbTestMapper; @Test @Transactional//开启事务,将数据库请求中sqlSession中 public void test() { System.out.println("一级缓存范围: " + sqlSessionFactory.getConfiguration().getLocalCacheScope()); // session 会话级别 System.out.println("二级缓存是否被启用: " + sqlSessionFactory.getConfiguration().isCacheEnabled()); // 默认启用 TbTest tbTest = tbTestMapper.findById2(1L); System.out.println(tbTest); TbTest tbTest1 = tbTestMapper.findById2(1L); System.out.println(tbTest); System.out.println(tbTest==tbTest1); }
注意点:
mybatis一级缓存是SqlSession级别,只对同一个SqlSession中操作生效!
一级缓存中,对象存储的是同一个,需要避免对象修改污染!
坑点演示
@Autowired private TbTestMapper tbTestMapper; @Test public void test() { SqlSession sqlSession = sqlSessionFactory.openSession(); TbTestMapper tbTestMapper = sqlSession.getMapper(TbTestMapper.class); System.out.println("一级缓存范围: " + sqlSessionFactory.getConfiguration().getLocalCacheScope());// session 会话级别 System.out.println("二级缓存是否被启用: " + sqlSessionFactory.getConfiguration().isCacheEnabled()); // 默认启用 // 第一次查询 TbTest tbTest1 = tbTestMapper.findById2(1L); System.out.println(tbTest1); // TbTest updateTbTest = new TbTest(); updateTbTest.setName("mybatis"); updateTbTest.setId(1L); // 更新数据 this.tbTestMapper.updateById(updateTbTest); // 当前sqlsession跟第一二次查询未共用 // 第二次查询 TbTest tbTest2 = tbTestMapper.findById2(1L); System.out.println(tbTest2); System.out.println(tbTest1 == tbTest2); }
结果:(类似数据库事务隔离级别–可重复读–MVCC)
二级缓存
二级缓存是针对不同SqlSession直接的缓存,可以理解为mapper级别。这些SqlSession需要是同一个namespace。那namespace在哪里体现呢?
<!--mapper xml 文件中--><mapper namespace="com.itcast.cupid.server.mapper.OrderMapper">
二级缓存示意图
sqlSession1去查询用户id为1的订单信息,查询到用户信息会将查询数据存储到二级缓存中。sqlSession2去查询时便会直接通过二级缓存进行查询。
二级缓存与一级缓存区别,二级缓存的范围更大,多个sqlSession可以共享一个OrderMapper的二级缓存区域。数据类型仍然为HashMap。每一个namespace的mapper都有一个二缓存区域,两个mapper的namespace如果相同,这两个mapper执行sql查询到数据将存在相同的二级缓存区域中。
Mybatis源码中的org.apache.ibatis.session.Configuration类的部分源码。
cacheEnabled配置的是二级缓存,而localCacheScope配置的是一级缓存。默认情况下SpringBoot集成Mybatis时一级缓存和二级缓存都是开启状态。
public Configuration() { // ... this.cacheEnabled = true; // 默认开启二级缓存 this.localCacheScope = LocalCacheScope.SESSION; // 一级缓存作用域 // ...}
可以在配置文件中修改配置
在上面的Configuration类中我们已经看到默认开启了二级缓存,此开启操作可以通过在application中进行开启或关闭(false);
mybatis.configuration.cache-enabled=true
mybatis: configuration: cache-enabled: true
二级缓存使用
<mapper namespace="com.itcast.cupid.server.mapper.TbTestMapper"> <!--在具体mapper中使用缓存--> <cache/> <!-- 根据id查询用户 --> <select id="findById2" parameterType="Long" resultType="com.itcast.cupid.server.entity.TbTest" useCache="true"> SELECT * FROM tb_test where id = #{id} </select></mapper>
@Autowired private TbTestMapper tbTestMapper; @Test public void userFirstCache1() { for (int i = 0; i < 10; i++) { // 每次Mapper调用findById方法都会创建一个session,导致mybatis一级缓存无法命中,如果使用了 二级缓存 则 可以命中 TbTest tbTest = tbTestMapper.findById2(1L); log.info("查询信息:{}", tbTest); } }
@Autowired private TbTestMapper tbTestMapper; @Test public void userFirstCache2() { TbTest tbTest1 = tbTestMapper.findById2(1L); // session1 TbTest tbTest2 = tbTestMapper.findById2(1L); // session2 System.out.println(tbTest1==tbTest2); }
需要注意的地方,那就是我们最后做的判断
System.out.println(tbTest1 == tbTest2);
为什么得到的结果却是 false 呢?这是因为,在二级缓存中,存入的是值,而不是对象,当需要使用的时候,会创建出新的对象,然后将值传入,所以这里是不等的;
禁用指定方法的二级缓存
由于cache是针对整个Mapper中的查询方法的,因此当某个方法不需要缓存时,可在对应的select标签中添加useCache值为false来禁用二级缓存。
<select id="findById" parameterType="int" resultMap="BaseResultMap" useCache="false">
二级缓存注意点
使用二级缓存的时候,一定要谨慎,因为有时候不同的namespace下的 SQL配置中可能缓存着相同的数据,如我们上面的例子,UserMapper.xml 中有关于 user表的操作,但是如果在其他 Mmpper.xml 中仍然有针对 user 单表的操作,这会导致两方数据不一样,如果在我们 UserMapper.xml 进行了刷新缓存,但是另一个Mapper.xml 中可能仍有效,所以可能会出现错误。
小结
查询结果实时性要求不高的情况下可采用mybatis二级缓存降低数据库访问量,提高访问速度,同时配合设置缓存刷新间隔flushInterval来根据需要改变刷新缓存的频次。通常情况下,如果同时设置了一级缓存和二级缓存,会先使用二级缓存的数据,然后再使用一级缓存的数据,最后才会访问数据库。
参考: https://blog.csdn.net/a1036645146/article/details/106811411、 https://zhuanlan.zhihu.com/p/106258135(存在少许错误)
面试常见Mybatis相关问题
Mybaits的优点
- 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
- 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
- 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
- 能够与Spring很好的集成;
- 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
#{}和${}的区别
- #{}是预编译处理,${}是字符串替换。
- Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
- Mybatis在处理 时,就是把 {}时,就是把 时,就是把{}替换成变量的值。
- 使用#{}可以有效的防止SQL注入,提高系统安全性。
Mybatis执行批量插入,能返回数据库主键列表吗?
能,JDBC都能,Mybatis当然也能。
- 升级Mybatis版本到3.3.1。官方在这个版本中加入了批量新增返回主键id的功能;
- 在Dao中不能使用@param注解。
- Mapper.xml中使用list变量(parameterType=“Java.util.List”)接受Dao中的参数集合。
下面是具体代码过程,可供参考
<!-- 批量新增 主键回填 --> <insert id="batchInsert2" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="id" > INSERT INTO <!--<include refid="tb_test2" />--> tb_test2 (`name`) <!-- 注意不要添加id字段,id属于自增字段不用传入,传入id,回填的id值会出问题 --> VALUES <foreach collection="list" index="index" item="tbTest" separator=","> ( #{tbTest.name} ) </foreach></insert>
public int batchInsert2(List<TbTest> list);
@Test public void testBatchInsert() { TbTest tbTest1 = new TbTest(); tbTest1.setName("aaa"); TbTest tbTest2 = new TbTest(); tbTest2.setName("bbb"); List<TbTest> tbTests = Arrays.asList(tbTest1, tbTest2); int i = tbTestMapper.batchInsert2(tbTests); tbTests.forEach(System.out::println); }
结果
具体见: https://blog.csdn.net/a745233700/article/details/80977133、 https://blog.csdn.net/young_1004/article/details/82428660、https://www.cnblogs.com/zouhg/p/11634171.html、 https://www.cnblogs.com/lukelook/p/11099039.html … 自行百度,一大堆
Dubbo
什么是协议
协议是两个网络实体进行通信的基础,数据在网络上从一个实体传输到另一个实体,以字节流的形式传递到对端。在这个字节流的世界里,如果没有协议,就无法将这个一维的字节流重塑成为二维或者多维的数据结构以及领域对象。
基本实现原理
- 基于TCP连接,根据Dubbo协议规范进行服务间通信;
- 底层采用非阻塞式的网络IO模型,Netty框架的NIO,实现服务间数据交互,效率较高(比基于HTTP的RPC要高);
- 结合JDK动态代理、ZK注册中心、反射(服务端目标方法的获取及执行)、及线程间协作机制(线程池、Callable、Semaphore、Future…)等技术,最终实现了整个RPC远程调用流程;
详见: https://www.jianshu.com/p/a4048325c27b