使用过的Java Web框架
SSH
SSH是Struts + Spring + Hibernate的一个集成框架,Struts作为系统架构的控制器层,提供MVC架构分离,Spring负责模型层,Hibernate作为数据持久层。
SSM
SSM是Spring + Spring MVC + Mybatis,Spring MVC负责原来Struts前端控制器的部分,Mybatis替换Hibernate做数据持久层。
Spring Boot
Spring MVC的升级,简化了应用的开发和部署,SSH或SSM需要进行很多的XML配置,开发起来不够便利,Spring Boot的出现让Java Web开发得到了一个飞跃的提升。
至于数据持久层,可以用Hibernate也可以用Mybatis,目前国内主流还是使用Mybaits,或许是因为大厂们都在用吧。
关于微服务
在单体应用中,Spring Boot已经足以满足绝大部分的开发需求,在这个基础上,单体应用暴露出来的性能、稳定性的不足,可以通过微服务的方式进行弥补,甚至是向上发展,满足更高的服务要求。
Spring Boot作为Web应用基石,很有必要对此进行更深入的学习。
Spring Boot模块
starter(启动器)
以前当我们需要引用第三方依赖的时候,除了添加依赖包,还要写各种配置文件,Spring Boot中是约定大于配置的,引用starter就可以直接使用这个第三方依赖,我们可以不用额外编写配置文件,因为里面已经进行了默认配置,比如:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
里面提供了Servlet容器Tomcat,并为Spring MVC提供了大量的自动配置,现在只需要创建一个Web启动类就可以运行起来一个Web应用:
@SpringBootApplication
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
虽然starter内提供了自动配置,但是有些依赖仍然需要我们进行手动配置,比如说数据库连接信息:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://ip:3306/mydb?createDatabaseIfNotExist=true&character_set_server=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
注:添加的依赖是用Maven进行管理。
什么是约定大于配置?
比如Tomcat默认启动端口是8080,如果还需要我们手动配置,要配置的信息实在太多了,开发上会有诸多不便,有了约定大于配置这个原则,我们可以在不对Tomcat进行任何配置的情况下启动应用,只有在我们需要的时候才进行修改和定制,这是Spring Boot相比Spring MVC更加灵活、方便、简单,并替代成为主流框架的原因。
启动类
启动类使用了一个注解:@SpringBootApplication,表示该类是SpringBoot的入口启动类
这个注解是一个组合注解,包含了以下三个注解:
-
@SpringBootConfiguration
表示该类是一个配置类,是@Configuration的派生注解,和@Configuration效果一样,但有一点区别,应用中可以有多个@Configuration作为配置类,但应该只有一个@SpringBootConfiguration来表示启动类,以便Spring可以找到自动配置,比如在单元测试中,如果有两个,那么单元测试的时候会抛出IllegalStateException异常,说找到了多个启动类入口,不知道该选哪一个。
注意:以上的场景我们可以在单元测试中验证,但是正常启动应用不会出现,因为我们启动应用需要选择指定类,Spring可以识别到,而在单元测试中不行。
-
@EnableAutoConfiguration
表示开启自动配置,Spring会去加载需要配置的Beans,默认扫描的是当前启用注解的Class对象所在的目录作为root目录,这个目录下的所有子目录都会被扫描,和@ComponentScan组合使用
-
@ComponentScan
定义扫描的路径,并从中找出标识了需要装配的类,将其自动装配到Spring的Bean容器中
使用场景:如果代码包在使用了@SpringBootApplication注解的Class类所在包或下级包中,那么@SpringBootApplication中整合的@ComponentScan注解会自动扫描这些包,并注入到容器中;如果有一些代码包不在,那么就可以使用@ComponentScan注解手动添加指定
自动配置原理
首先通过@EnableAutoConfiguration注解,告诉Spring Boot开启自动配置功能,进入源码查看这个注解,可以看到其中引用了两个重要的注解:
先了解Spring Boot启动类使用的注解以及子层级引用关系:
- @SpringBootApplication
-
@SpringBootConfiguration
- @Configuration
-
@EnableAutoConfiguration
-
@AutoConfigurationPackage
- @Import({Registrar.class})
-
@Import({AutoConfigurationImportSelector.class})
-
-
@ComponentScan
-
我们看这个注解:
@Import({AutoConfigurationImportSelector.class})
该类实现类ImportSelector接口:
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 判断是否开启了自动配置
if (!this.isEnabled(annotationMetadata)) {
// 没有开启
return NO_IMPORTS;
} else {
// 开启了,去扫描自动配置类
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
// 获取@EnableAutoConfiguration注解的信息
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
// 从jar中的spring.factories文件中获取自动配置相关的类数组,注意只筛选自动配置相关的
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
// 去重
configurations = this.removeDuplicates(configurations);
// 获取要排除的配置类
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
// 判断被排除的类是不是自动配置类,不是就抛出异常
this.checkExcludedClasses(configurations, exclusions);
// 排除
configurations.removeAll(exclusions);
// 使用spring.factories配置文件中配置的过滤器对自动配置类进行过滤
configurations = this.getConfigurationClassFilter().filter(configurations);
// 封装成事件对象传入并通过监听器进行事件广播
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
不清楚ImportSelector接口类的可以看我这篇文章:juejin.cn/post/719806…
以上大概就是自动装配的工作流程,有疑惑的点进去源码看看就好。
所以现在知道自动配置的工作原理,只需要引入starter Jar包:
-
@EnableAutoConfiguration开启自动配置,即开关生效
-
@Import({AutoConfigurationImportSelector.class})生效,执行AutoConfigurationImportSelector里面的代码
-
AutoConfigurationImportSelector代码的作用是:
- 扫描所有引入的starter里面的spring.factories文件,将所有的自动配置类扫描出来
- 将扫描到的自动配置类进行一些去重、排除
- 得到最终的结果并注入
自动配置类中会有一些默认配置,比如源码中内嵌的Tomcat有一个additional-spring-configuration-metadata.json文件,里面写了关于端口的配置:
{ "name": "server.port", "defaultValue": 8080 },
在配置类中它将这个默认配置文件加载进来了,所以开发者引入这个starter的时候,自动配置也就完成了。
@ComponentScan
用来指定扫描路径,我们知道SpringBoot会扫描启动类所在包路径下有指定注解的类,并将其自动装配到Bean容器中,指定注解如:@RestController、@Service、@Component、@Repository等。
当我们要引入外部包的类,可以使用@ComponentScan来扫描,像这样:
@ComponentScan(value = "com.ext.controller")
@ComponentScan注解提供了丰富的扫描支持,还支持自定义过滤。
IOC和AOP
这是Spring框架的两大特性,IOC指控制反转,AOP指切面编程。
IOC
是什么?
这是一种设计思想,将设计好的对象交给Spring容器去管理,将对象的控制权交给容器,由容器来管理对象的整个生命周期。当我们需要某个对象时,就去找容器获取。
解决了什么问题?举个例子说明
调用者和被调用者之间不再相互依赖,由第三方容器来进行管理。降低了对象之间的耦合度
例子:
我们要对用户表进行增删改查,那么步骤如下:
-
接口层创建UserMapper,数据操作层
-
接口层创建UserService对象
-
接口层创建UserService对象和UserMapper对象,并注入mapper到service中
UserService service = new UserService(new UserMapper());
-
接口层调用UserService方法
这种方式的问题在于,接口层与UserService和UserMapper之间有了依赖关系,代码耦合了,如果实例化UserService对象的时候需要注入更多的类,那么就会存在更多的依赖关系,这不便于代码的维护,不是一种好的代码设计方式。
IOC能帮助我们设计出松耦合的程序,在上面的例子中,通过IOC改造就会变成这样:
- 接口层从IOC容器中获取实例化后的UserService对象,UserService对象会在创建的时候由IOC自动注入UserMapper对象属性
- 接口层调用UserService方法
这种方式将对象控制权交给了IOC容器,由容器来管理对象的创建、查找、注入等,实现对象与对象之间的松耦合,还便于功能的复用,使得整体的程序架构更加灵活。
AOP
是什么?
AOP称为面向切面编程,是一种在运行期间通过动态代理实现对程序功能的统一处理的技术。
解决了什么问题?举个例子说明
对业务逻辑的各个部分进行隔离,提高程序的复用性和开发效率。
例子:
为前端调用的部分接口增加日志功能,输出接口请求参数。
如果没有AOP,我们可以这样写:
private static final Logger log = LoggerFactory.getLogger(TestController.class);
@PostMapping("/t1")
public void t1(@RequestBody User user) {
log.info(user.toString());
}
@PostMapping("/t2")
public void t2(@RequestBody User user) {
log.info(user.toString());
}
@PostMapping("/t3")
public void t3(@RequestBody User user) {
log.info(user.toString());
}
这种方式会导致代码冗余,代码复用性低,可维护性低。
有了AOP,我们可以这样做:
@Aspect // 声明切面
@Component // 加入到IOC容器
public class MyAop implements Ordered {
private static final Logger log = LoggerFactory.getLogger(LogbackAop.class);
@Override
public int getOrder() {
return 1;
}
@Pointcut("execution(public * com.cc.controller..*.*(..))")
public void pointCut() {}
@Before("pointCut()")
public void before(JoinPoint joinPoint) throws InterruptedException {
log.info("do something");
}
}
这样就会在每个接口方法执行前进行拦截,先输出一段log再继续执行,这可以减少我们很多冗余代码。
AOP实现原理:动态代理
AOP的实现原理是动态代理,简单说就是用一个动态生成的代理去执行目标方法,我们可以很方便的借助这个代理,在执行这个方法前后做一些额外的事情,大概像这样:
// 本来执行目标方法是这样的:
Function function = new Function();
function.work();
// 有了动态代理后:
Function function = new Function();
Proxy proxy = new Proxy(function);
// 代理执行前做一些事情
proxy.doSomething();
// 代理执行方法
proxy.work();
// 代理执行后做一些事情
proxy.doSomething();
关于动态代理更多信息,可以看我这篇文章:juejin.cn/post/716948…
Spring容器和Bean的生命周期
关于IOC容器
是管理Bean的容器,在Spring中,要求所有的IOC容器都实现BeanFactory接口。
BeanFactory负责配置、创建、管理Bean,它有一个子接口ApplicationContext,也称为Spring上下文,我们平时用的比较多的就是ApplicationContext。
延迟加载与立即加载:
BeanFactory按需加载Bean,而ApplicationContext是在启动时加载所有的Bean。所以在内存比较小的系统中,我们可以选择用BeanFactory,不过我们最常用的还是ApplicationContext,因为ApplicationContext增强了BeanFactory,提供了更多的特性。
Bean的生命周期
-
实例化:为Bean对象分配内存空间,然后根据不同容器有不同的初始化逻辑:
- BeanFactory容器:按需加载,用到的时候才进行实例化
- ApplicationContext容器:容器启动后,便实例化所有的Bean,这是我们用Spring Boot默认的方式
这一步只是实例化,并没有进行属性的依赖注入。
-
属性赋值:为Bean设置相关属性和依赖
-
初始化:
-
检查Aware相关接口并设置相关依赖,如Bean实现了:
- BeanNameAware接口,注入当前Bean对应的Bean Name
- BeanClassLoaderAware接口,注入加载当前Bean的ClassLoader
- BeanFactoryAware接口,注入当前BeanFactory容器的引用
示例:
@Component public class SpringAwareModel implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware { @Override public void setBeanName(String s) { System.out.println("setBeanName: " + s); } @Override public void setBeanClassLoader(ClassLoader classLoader) { System.out.println("setBeanClassLoader: " + classLoader); } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { System.out.println("setBeanFactory: " + beanFactory); } }
@Autowired private ApplicationContext context; @Test public void t1() { SpringAwareModel bean = context.getBean(SpringAwareModel.class); }
setBeanName: springAwareModel setBeanClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2 setBeanFactory: org.springframework.beans.factory.support.DefaultListableBeanFactory
-
是否实现BeanPostProcessor接口:执行初始化的前置处理
-
是否实现InitializingBean接口:会在Bean属性赋值后执行InitializingBean的afterPropertiesSet()方法
-
是否实现BeanPostProcessor接口:执行初始化的后置处理
注意:BeanPostProcessor接口会作用于所有的Bean,所以不要在特定类中使用,像这样:
@Component public class InitializingBeanImpl implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("初始化前置处理:" + beanName); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("初始化后置处理:" + beanName); return bean; } }
初始化前置处理:springAwareModel afterPropertiesSet 初始化后置处理:springAwareModel
-
-
初始化完成后就可以使用这个Bean了,当不需要Bean时,就会进行销毁
- Bean是否实现了DisposableBean,是就会执行实现方法destroy()
Bean什么时候会被销毁?
- Spring容器销毁
- Bean实现了DisposableBean接口,在业务层手动执行destroy()方法
- Bean的作用域不是单例或全局,用完就会被释放
Bean的作用域
即Bean的存活范围:
作用域 | 说明 |
---|---|
singleton | 单例,无状态的Bean |
prototype | 每次注入都是一个新的对象,适合有状态的Bean,要注意创建对象的开销 |
request | web容器中的对象,每次请求都创建一个对象 |
session | web容器中的对象,一次会话创建一个对象 |
application | web容器全局的对象 |
Bean是线程安全的吗?
不是,默认情况下Bean是单例无状态的,大部分情况下可以说是线程安全的,但是如果Bean是有状态的,那就需要开发者自己来保证线程安全,最简单的就是把Bean的作用域从默认的singleton改成prototype。
无状态是指没有数据存储功能
但单例模式下还可以是用ThreadLocal封装变量来保证线程安全。
程序只执行一次的方法
需求:程序启动后调用一次方法,以便于预加载一些数据,并要求这个方法在整个程序执行过程中只执行一次
有两个方法:
- ApplicationRunner
- CommandLineRunner
这两个接口都有一个实现方法run(),都只会执行一次。
执行顺序问题
当有多个类并且保证执行顺序的时候,可以使用@Order注解来设定执行顺序。
WebMVC配置类
什么是Web MVC?
MVC是指一种设计模式:Model(模型)、View(视图)、Controller(控制器),在Spring MVC中,前端的请求会到达前端控制器:DispatcherServlet,去找到对应的控制层接口,然后返回结果给前端。
在Spring Boot中,通过Java Bean的形式来代替Spring MVC中XML的配置方式,可以定义拦截器(Interceptor)、过滤器(Filter)、消息转换器(MessageConverter)、静态资源访问等配置信息。
在Spring Boot1.5前是重写WebMvcConfigurerAdapter来做配置,Spring Boot2.0后官方推荐两种方式:
-
实现WebMvcConfigurer接口
该接口提供了很多关于配置的空方法,开发者根据需要自行设置即可
-
继承WebMvcConfigurationSupport
是MVC是基本实现,里面包含了WebMvcConfigurer接口中的方法
WebMvcConfigurer和WebMvcConfigurationSupport这两种方式有什么区别?
当我们两种方法都不使用的时候,Spring Boot会自动启用WebMvcAutoConfiguration配置类,里面有关于过滤器、静态资源等的默认配置。但是当我们使用了继承WebMvcConfigurationSupport的方式后,WebMvcAutoConfiguration会因为这个注解导致无法生效:
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
即WebMvcConfigurationSupport会覆盖WebMvcAutoConfiguration的配置。
所以当我们需要保留默认配置并做一些拓展,应该选择:实现WebMvcConfigurer接口
当我们不需要保留默认配置,则应该选择:继承WebMvcConfigurationSupport
过滤器(Filter)和拦截器(Interceptor)
过滤器和拦截器的区别是什么?
- 触发时机:
- 过滤器是在进入容器后,请求进入Servlet之前预处理
- 拦截器是拦截用户请求,在方法执行前、执行后调用
- 实现不同:
- 过滤器基于回调函数,拦截器基于动态代理
- 生命周期:
- 过滤器的生命周期由Servlet管理,拦截器通过IOC容器管理
- 修改Request
- 过滤器可以修改Request;拦截器不可以
- 执行顺序:
- 先执行过滤器,再执行拦截器
- 使用场景
- 过滤器:URL权限控制、过滤敏感词汇、设置字符编码
- 拦截器:登录验证、权限验证、日志记录
Spring里用了多少种设计模式
-
工厂模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类
BeanFactory,根据传入的唯一标识来获取Bean对象。
-
单例模式:创建一个全局唯一的对象
Bean默认就是使用单例创建,容器中只有一个,也可以通过设置Bean的作用域来改变Bean的状态。
-
代理模式:创建一个可以代理对象,负责执行被代理对象的方法
AOP就是通过动态代理实现的,动态代理又分JDK和CGLIB动态代理
-
原型模式:用于创建重复的对象,同时能保证性能
Bean的其中一个作用域叫prototype,意思是在每次获取时通过克隆生成一个新的实例。
-
策略模式:类的行为可以在运行时动态的更改
Resource接口,有UrlResource、ClassPathResource、FileSystemResource等实现类,针对不同的底层资源提供一致的访问接口。
-
适配器模式:作为两个不兼容的接口之间的桥梁,做适配
AOP的增强,将Advice封装成拦截器类型返回给容器,所以用适配器对Advice做了转换。
-
观察者模式:对象被修改时,通知所有监听它的对象
ApplicationListener接口
-
责任链模式:每一个对象对其下家的引用而连接起来形成一条链,请求在这个链上传递,直到链上的某一个对象决定处理该请求,发起请求的客户端不知道链上的哪一个对象来处理,使得系统可以在不影响客户端的情况下动态的组织和分配责任
在DispatcherServlet中获取与请求匹配的处理器就用到的责任链模式。
内嵌Tomcat以及替换
SpringMVC在部署的时候还需要外置的Tomcat环境,SpringBoot中已经不需要了,其内置了Tomcat,可以打成jar包直接启动,只需要在pom.xml中添加build配置:
<build>
<!--jar包文件名称-->
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
然后通过命令启动:
java -jar app.jar
如何使用外部Tomcat环境启动?什么场景下用?
修改pom.xml配置,将项目打成war包,然后放到Tomcat的webapps目录下即可。
<groupId>org.example</groupId>
<artifactId>sbdemo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging> 在这一行进行配置
既然内置了Tomcat,那么为什么还要用外置的Tomcat环境呢:
- 有些公司有一套独立成熟的Tomcat部署流程,可能还有固定的服务器配置,所以保持原因不变即可
- 内置Tomcat出现安全问题的时候,改用外置的更快速,当然也可以为此进行Spring Boot小版本的更新
- 有些生产环境网络没有提供足够的外网端口,那么就要多个服务放在一个Tomcat里面了,不过也可以使用Nginx进行服务转发
分析下来,Spring Boot应用部署到生产环境,其实用内置Tomcat并没有什么问题,加上微服务盛行的时代,内置Tomcat的方式会更加适用。
除了内置Tomcat还有哪些启动方式?什么场景下用?
Spring Boot的Web环境默认使用Tomcat作为内置服务器,但我们还有更多的选择:
- Tomcat
- Jetty
- Undertow
- Netty(特殊一点)
切换内置服务器为Jetty、Undertow的配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
上面配置的意思是去掉默认的Tomcat依赖,使用jetty,同理,undertow类型也是,照着修改即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
Servlet是一种Java EE规范,Tomcat、Jetty、Undertow都遵循了这个规范,但是netty比较特殊,它是一个异步事件驱动的基于NIO的网络应用程序框架,支持扩展实现自己的Servlet容器。