Spring

原理

Spring原生jar包介绍

spring原生jar包为:beans,context,core,aop,expression

  • context:提供上下文环境 ,给 Spring 提供一个运行时的环境,用以保存各个对象的状态,Context 作为 Spring 的 Ioc 容器。构建与core封装包基础上的context封装包,提供了一种框架式的对象访问方法。
  • core:Spring框架最基础的部分,提供了框架的基本组成部分,包括控制反转和依赖注入功能。是发现、建立和维护每个 Bean 之间的关系所需要的一些列的工具。采用Factory(工厂模式)实现了IoC(控制反转),将应用的配置和代码分开。
  • bean:创建bean对象,交给Spring容器管理,实现Bean之间的解耦。
  • aop:提供了面向切面的编程实现,用户可以自定义拦截器、切点等。
  • expression:是一个支持查询和操作运行时对象的强大的表达式语言

Bean生命周期顺序(重点):

  1. **实例化:**通过注解或者读取xml文件方式,利用类的无参构造+反射创建 Bean 的实例,在实例化的前后会执行对BeanDefinition的前置和后置方法。
  2. 依赖注入:对@Autowired的Bean对象,@Value的常量进行赋值。
  3. **执行Aware系列接口的回调:**如果 Bean 实现了特定的 Aware 接口,Spring 会调用对应的 setter 方法,将容器自身的资源信息注入给 Bean。如BeanNameAware:注入当前 Bean 在容器中的 id/name。
  4. **BeanPostProcessor 的前置处理:**调用 BeanPostProcessor 接口的 postProcessBeforeInitialization() 方法,允许在 Bean 初始化前对 Bean 进行加工处理(例如生成代理对象,或者包装对象)。AOP 动态代理就是在这之后介入的。
  5. 初始化 Bean:
    1. 执行@PostConstruct 注解方法。
    2. 检查 Bean 是否实现了 InitializingBean 接口,如果实现,则调用 afterPropertiesSet() 方法。
  6. **BeanPostProcessor 的后置处理:**调用 BeanPostProcessor 接口的 postProcessAfterInitialization() 方法。此时 Bean实例已经完整创建完毕,可以被容器使用了。这也是 AOP 动态代理发生的关键节点(通过后置处理器返回代理对象)。
  7. **Bean 就绪:**Bean驻留在容器中,容器类型是Map<BeanName,Bean对象>,所以spring的单例模式并不是严格意义上的单例,一个Bean可以有多个不同的beanName。
  8. **销毁阶段:**在该阶段,Spring 容器将调用 Bean 的销毁方法destory-method,可以通过 @PreDestroy 注解来标记销毁方法。这个阶段是在 Spring 容器关闭时执行的。

Bean生命周期详细流程图:

f4528dca9730e0d89e8353a49fde20df

Bean生命周期和bean的代理对象结构简图如下:

image-20230215202654197

IOC原理

概念:

IOC(控制反转):

  • 控制:把创建对象的控制权交给了spring容器,实现减耦合,功能复用,减少new对象对内存的损耗,通过反射的原理创建实例。
  • 反转:依赖注入,不是对象去new新的对象给自己赋值,而是spring容器帮bean对象进行属性赋值。

IOC的加载流程图:

865fabca496db1684ef619bb9aabe2a3

定义bean方式:

第一种:注解

@Component,@Bean等

第二种:xml和set方法:

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
    <bean id="user" class="com.example.spring.User">
<property name="student" ref="studentImpl"></property>
</bean>
<bean id="studentImpl" class="com.example.spring.Student"></bean>

public class User {
private Student student;
public void setStudent(Student student) {
this.student = student;
}
public void test(){
this.student.test();
}
}
public class Student {
private String studentName;
public void test(){
System.out.println("Student");
}
}
public class MainTest {
public static void main(String[] args) {
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("app1.xml");
User user = applicationContext.getBean("user", User.class);
user.test();
}
}

第三种:有参数构造注入属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   <bean id="user" class="com.example.spring.User">
<constructor-arg name="student" ref="studentImpl"></constructor-arg>
</bean>
<bean id="studentImpl" class="com.example.spring.Student"></bean>

public class Student {
private String studentName;
public void test(){
System.out.println("Student");
}
}
public class User {
private Student student;
public User(Student student) {
this.student = student;
}

AOP原理

概念:

AOP(面向切面编程)

  • spring的aop可以利用cglib或者jdk动态代理实现,默认cglib,执行方法的时候,在不修改源码的基础上对方法进行增强。
  • 先定义切面(用来生成动态代理对象),再定义切点(具体方法或者注解),然后定义增强的类型及逻辑,分为前置,后置,环绕,异常通知,最终通知。

实现动态代理的方法:

  1. JDK动态代理:利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用invoke来处理。

  2. CGLIB动态代理:利用ASM开源包,把代理对象类的class文件加载进来,通过修改其字节码生成子类来处理(所以滥用CGLIB动态代理会导致方法区内存泄漏)。

AOP代码实例:

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
@Aspect	//切面
@Component
public class LogAspect {
//切点,表示写了@Log的方法都要进行增强
@Pointcut("@annotation(com.lh.plat.service.common.Log)")
public void logPointCut(){}

//切点是dao下所有方法
//@Pointcut("execution(* com.atguigu.dao.*.* (..))")
//public void test() {}

//方法返回数据之后进行增强,JoinPoint是连接点
@AfterReturning(pointcut = "logPointCut()",returning = "val")
public void doAfter(JoinPoint joinPoint, Object val){
String methodName = joinPoint.getSignature().getName();
//业务逻辑
}
}

//注解log
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
String module() default "";
String op() default "";
short logType() default 0;
}

核心类解释

DefaultSingletonBeanRegistry(存放单例bean)

  1. Map<String, Object> singletonObjects相当于bean容器,集中存放单例bean
  2. 全权管控所有单例生命周期如创建、缓存、获取、销毁。维护三级缓存,解决循环依赖;
  3. 我们日常用的 BeanFactory/ApplicationContext 最终都复用这套单例管理逻辑;
  4. 使用场景:
    1. 手动注册第三方组件到 Spring 容器
    2. 动态注册运行时生成的实例
  5. 如果手动调用,传入的对象不会经过 Spring 完整 Bean 生命周期:不会执行属性注入@PostConstruct、InitializingBean、afterPropertiesSet 等初始化流程;适合已经初始化完毕的成品对象注入;

关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 一级缓存:存储完整初始化完毕的单例Bean(核心!成品,可直接对外获取)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();

// 2. 二级缓存:实例化完成、但未完成属性填充、未初始化的早期Bean(解决普通循环依赖,通过懒加载,启动应用不注册bean,使用时再注入)
private final Map<String, Object> earlySingletonObjects = new HashMap<>();

// 3. 三级缓存:存放Bean工厂ObjectFactory,用于生成Bean代理对象(AOP场景循环依赖必备)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();

// 记录正在创建中的Bean名称集合(标记状态,防止并发重复创建)
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>());

//手动向Spring一级缓存(singletonObjects)注册一个外部自定义单例对象
public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException

使用示例:

1
2
3
4
5
6
7
8
9
10
11
// 获取Spring工厂底层注册表
DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry) applicationContext.getAutowireCapableBeanFactory();

// 自己new一个对象
UserService userService = new UserService();
// 手动注册进Spring单例池
registry.registerSingleton("userService", userService);

// 后续通过容器获取,拿到的就是上面注册的实例
UserService bean = applicationContext.getBean("userService", UserService.class);
System.out.println(bean == userService); //true

BeanDefinition(定义bean属性)

  1. BeanDefinition 注册位置:DefaultListableBeanFactory内部的 Map<String, BeanDefinition> beanDefinitionMap
  2. Spring 容器启动扫描阶段就全部解析生成完毕
  3. 内容:记录 Bean 的类路径、作用域、构造参数、属性值、初始化销毁方法等所有配置信息
  4. 作用:告诉 IOC 工厂:这个 Bean 该怎么创建
  5. 实现类:
    1. GenericBeanDefinition默认主流实现,绝大部分 @Component、@Bean 扫描出来的 Bean 定义都是它
    2. AnnotatedGenericBeanDefinition:继承GenericBeanDefinition,额外携带注解元数据信息
    3. ScannedGenericBeanDefinition:包扫描 @Component 得到的 BeanDefinition

AbstractAutowireCapableBeanFactory(核心逻辑)

  1. Bean 实例化、构造注入、setter 填充、Aware 回调、初始化、BeanPostProcessor 执行全部在这里完成

DefaultListableBeanFactory(Spring 默认工厂)

继承关系:

1
2
3
4
5
6
7
8
9
DefaultSingletonBeanRegistry (单例Bean缓存父类)
↑ 继承
FactoryBeanRegistrySupport
↑ 继承
AbstractBeanFactory(实现BeanFactory顶层接口,定义getBean主干逻辑)
↑ 继承
AbstractAutowireCapableBeanFactory(实现Bean创建、属性注入、初始化全生命周期)
↑ 继承
DefaultListableBeanFactory(默认工厂)->实现 BeanDefinitionRegistry(存储,创建,删除BeanDefinition)

能力:

  1. 实现BeanDefinitionRegistry负责管理BeanDefinition,内部beanDefinitionMap属性存放初始化创建的BeanDefinition
  2. 属于BeanFactory子类,调用 getBean() 会根据 BeanDefinition获取/创建bean对象
  3. 继承了DefaultSingletonBeanRegistry存放已创建的bean对象

工厂创建bean流程:

  1. 容器启动,ComponentScan解析器启动 ClassPathBeanDefinitionScanner → 扫描@Component/@Bean → 解析全部配置 ,生成一堆BeanDefinition,存入beanDefinitionMap
  2. 容器循环创建单例 Bean,首次需要获取Bean(getBean(“userService”)), 先去 DefaultSingletonBeanRegistry 的一级缓存查找,没有就依据BeanDefinition反射调用构造方法实例化对象、填充属性、执行初始化方法。
  3. Bean完全创建完毕后,调用 DefaultSingletonBeanRegistry.registerSingleton() 将成品Bean放入一级缓存 singletonObjects 永久缓存
  4. 后续再getBean,直接从注册表缓存拿,不再走创建流程

BeanFactoryPostProcessor(BeanDefinition处理器)

容器启动早期执行,修改 Bean 定义元数据

实现类:

  1. ConfigurationClassPostProcessor:最重要内置后置处理器,解析@Configuration、@Bean、@ComponentScan、@Import,扫描所有组件,生成 BeanDefinition,Spring 注解驱动的核心
  2. PropertySourcesPlaceholderConfigurer:解析 ${} 占位符,读取 properties/yaml 配置文件填充属性

使用代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
//调用时机在spring在读取beanDefinition信息之后,实例化bean之前。
//设置一次全局使用
@Component
public class AConfig implements BeanFactoryPostProcessor {

//把名称为a的bean指向B类
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition("a");
beanDefinition.setBeanClassName(B.class.getName());
}
}

BeanPostProcessor(Bean实例处理器)

干预bean的实例化和初始化过程

实现接口:

InstantiationAwareBeanPostProcessor在 Bean 实例化之前 / 刚完成实例化、还没填充属性的时候触发(正常BeanPostProcessor只在依赖注入之后触发),可以干预对象创建、AOP 创建代理,构造器、属性注入,例如AutowiredAnnotationBeanPostProcessor,CommonAnnotationBeanPostProcessor

主要的扩展点有以下5个方法,主要在bean生命周期的两大阶段:实例化阶段和初始化阶段
按调用顺序为:

  1. postProcessBeforeInstantiation:实例化bean之前,相当于new这个bean之前
  2. postProcessAfterInstantiation:实例化bean之后,相当于new这个bean之后
  3. postProcessPropertyValues:bean已经实例化完成,在属性注入时阶段触发,@Autowired,@Resource等注解原理基于此方法实现
  4. postProcessBeforeInitialization:初始化bean之前,相当于把bean注入spring上下文之前
  5. postProcessAfterInitialization:初始化bean之后,相当于把bean注入spring上下文之后

实现类:

  • AutowiredAnnotationBeanPostProcessor:解析 @Autowired、@Value 依赖注入,完成属性自动注入
  • CommonAnnotationBeanPostProcessor:解析 Java 自带注解:@PostConstruct、@PreDestroy、@Resource
  • ApplicationContextAwareProcessor:处理各种 Aware 回调(ApplicationContextAware、BeanFactoryAware 等)

使用代码示例:

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
/**
* 交给Spring管理才会生效
* 项目启动时每个bean创建都会执行
*/
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {

// 初始化之前执行
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 只打印我们自己的Bean日志,过滤框架内部Bean
if (bean instanceof DemoBean) {
System.out.println("前置回调 beforeInitialization,beanName:" + beanName);
}
return bean; // 可修改、包装、返回新对象
}

// 初始化全部完成之后执行
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof DemoBean) {
System.out.println("后置回调 afterInitialization,beanName:" + beanName);
}
return bean;
}
}

Aware系列接口

如果Bean手动实现这些接口,bean对象创建时候会自动执行对应方法。

实现Aware的接口:

  • ApplicationContextAware:实现它来获取 ApplicationContext 上下文,hutool的SpringUtil就是这样获取上下文并且获取或创建bean的。
  • BeanNameAware:获取当前 Bean 在容器中的 beanName
  • BeanFactoryAware:获取底层 BeanFactory
  • ResourceLoaderAware:资源加载器
  • EnvironmentAware:获取环境变量、配置参数

Aware 全家桶顺序:

1
2
3
1. BeanNameAware.setBeanName()
2. BeanFactoryAware.setBeanFactory()
3. ApplicationContextAware.setApplicationContext()

InitializingBean(初始化逻辑)

初始化执行顺序:@PostConstruct → InitializingBean(afterPropertiesSet) → xml init‑method / @Bean(initMethod) → DisposableBean(容器关闭时执行销毁方法)

实现InitializingBean场景:

  1. 需要依赖注入完成之后做初始化操作(加载配置、初始化连接池、开启线程、缓存预热)

  2. 校验必填参数,属性为空直接抛出异常阻止 Bean 创建,如下代码:

    1
    2
    3
    4
    5
    6
    @Override
    public void afterPropertiesSet() throws Exception {
    if (dataSource == null){
    throw new RuntimeException("数据源不能为空");
    }
    }

FactoryBean(生产复杂bean的工厂)

FactoryBean 本身也是一个 Bean,可以用来生产复杂、反射很难直接创建出来的对象。

例如:Mybatis 的 SqlSessionFactory、Redis 连接池、代理对象、需要复杂初始化逻辑的对象

MyBatis使用FactoryBean:

  1. 使用@Mapper标注在接口上,MyBatis 底层用 JDK 动态代理生成代理对象。
  2. 核心MapperFactoryBean实现FactoryBean,其中getObject()方法专门来获取代理对象。

Spring 加载 Mapper 的整条链路:

  1. @MapperScan → MapperScannerConfigurer,扫描指定包下所有 Mapper 接口
  2. 给每一个接口生成一个 BeanDefinition,其中beanClass = MapperFactoryBeanMapperFactoryBean里的mapperInterface = 当前 Mapper 接口
  3. MapperFactoryBean底层会维护一个Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();,key存mapper的类,value存代理对象工厂,专门利用jdk反射创建代理对象。
  4. IOC 根据 BeanDefinition 创建工厂对象MapperFactoryBean,MapperFactoryBean再注入 SqlSessionTemplate
  5. 当mapper被@Autowired之后就会去找名叫 userMapper 的 Bean,结果发现是FactoryBean,就调用getObject()获取代理对象,由代理对象执行逻辑。

AOP相关

@Aspect:定义切面类,用来做统一横切增强

@Pointcut:切点,定义哪些类 / 哪些方法需要被切面拦截(AspectJExpressionPointcut 表达式实现)

Advice:通知(前置、后置、异常、环绕通知),如@RestControllerAdvice,@ControllerAdvice

Advisor : Pointcut + Advice 绑定组合体

JdkDynamicAopProxy:JDK 动态代理实现(基于接口)

CglibAopProxy:CGLIB 字节码代理实现(基于类继承)

DefaultAopProxyFactory:代理工厂,自动选择 JDK/CGLIB 代理

实现 InstantiationAwareBeanPostProcessor,在 Bean 实例化后创建代理对象

ApplicationContext(高层上下文接口)

继承关系:

1
2
3
4
5
6
7
8
9
10
11
12
ApplicationContext
├─ ListableBeanFactory 批量获取Bean、按类型查找所有Bean
├─ HierarchicalBeanFactory 父子容器
├─ MessageSource 国际化消息解析
├─ ApplicationEventPublisher 事件发布(Spring事件监听机制)
└─ ResourcePatternResolver 资源加载(加载xml、classpath下配置文件)

AnnotationConfigApplicationContext(ApplicationContext实现类)
↓内部持有
DefaultListableBeanFactory:执行所有底层能力
├ 实现 BeanDefinitionRegistry:存放所有BeanDefinition(图纸配置)
└ 继承 DefaultSingletonBeanRegistry:三级缓存存放实例化好的单例Bean成品

具体实现类:

  • **AnnotationConfigApplicationContext:**注解驱动开发(SpringBoot 底层就是用它),读取@Configuration配置类
  • **ClassPathXmlApplicationContext:**传统 XML 配置方式,读取 classpath 下 xml beans 文件
  • FileSystemXmlApplicationContext:读取磁盘绝对路径的 xml 配置
  • **WebApplicationContext:**Web 环境专属上下文(SpringMVC 容器)

Spring事务

概念:

基于aop的思想,切入点相当于业务代码,切面相当于对切入点进行增强的方法,实际就是回滚。

Spring进行事务操作的方式:

  1. 声明式事务管理
  2. 基于xml配置文件方式
  3. 基于注解方式

spring事务隔离级别:

  1. DEFAULT 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别,Mysql 默认:可重复读。Oracle 默认:读已提交。

  2. 未提交读(read uncommited) :脏读,不可重复读,幻读都有可能发生

  3. 已提交读 (read commited):避免脏读。但是不可重复读和幻读有可能发生

  4. 可重复读 (repeatable read) :避免脏读和不可重复读.但是幻读有可能发生.

  5. 串行化的 (serializable) :避免以上所有读问题.

Spring事务实现具体流程:

  1. 如果方法标注了@Trasactional的执行方法,开启spring事务。
  2. 事务管理器先新建一个数据库连接connect,事务管理器会在thread local<map<datasource,connect>>注册datasource。
  3. 然后把autocommit改为false,方便回滚
  4. 之后target普通对象执行原生方法,如果出现异常进行回滚

事务的流程图如下:

image-20230215205929921

事务失效原因:

  1. 方法没加@Trasactional,所以代理对象不会对该方法进行增强。
  2. 同一个类,一个没有事务的方法执行同类另一个有事务方法事务失效,因为自调用的方法并不是代理对象执行,而是该类的bean对象执行,除非第二个事务方法自己注入自己的依赖,然后用类似与this.userservice.test()方法调用。(@Async也是)
  3. 方法没加public
  4. class没有被spring管理
  5. 异常被捕获到处理了
  6. 数据库不支持事务

Spring中的设计模式

image-20240610162549883

  1. 单例模式:每次读取的对象都是同一个,如spring-ioc原理,数据库连接池等,多次获取只获取同一个对象。
  2. 工厂模式:专门造对象的 “工厂”,不用自己 new 对象,例如:
    1. Spring IOC容器,BeanFactory是顶层工厂,来创建BeanDefinition,负责创建和获取 Bean;
    2. ApplicationContext 是功能更完善的工厂,FactoryBean 专门创建复杂对象。
  3. **代理模式:**通过反射生成代理对象,代理对象对原有方法进行增强,真正执行时候是代理对象执行的,如spring的aop和事务。
  4. **观察者模式:**发布事件,监听事件,广播事件,如spring的ApplicationEvent。
  5. 责任链模式:**按链式顺序执行处理器,灵活添加移除处理器,可扩展,每个处理器单一职责。**如spring-ai的ChatClient的defaultAdvisors,添加各种处理器链式执行,如日志打印,自动注入历史会话,检索向量数据库相关知识等。
  6. 适配器模式:**把不兼容的接口,转成兼容的,让原本不能一起工作的类能一起工作。**如springmvc的HandlerAdapter,每个适配器都实现HandlerAdapter并且调用适配器的handle (),各种请求对应的适配器如下:
    1. @RequestMapping和@Controller->RequestMappingHandlerAdapter
    2. HttpRequestHandler->HttpRequestHandlerAdapter
    3. Servlet->SimpleServletHandlerAdapter
    4. Spring事务->TransactionManagerAdapter
    5. AOP->AdvisorAdapter(通知适配器)

Spring中Bean是否线程安全?

  • 默认单例bean不是线程安全的,一个单例的bean要保持线程安全,除非用ThreadLocal,要不然他只要有成员变量或者注入一个bean,都会发生线程安全问题。
  • protopyte多例的bean可以保持线程安全,方法是在bean上加@Scope(value = “prototype”),相当于一个线程调用这个bean每次都是从容器中new一个bean,普通成员变量可以保证线程安全,静态变量就不行,另外如果只有注入的bean是多例的才能保证注入的bean也是线程安全的。
  • bean 分为有状态 bean 和无状态 bean,有状态 bean 即类定义了成员变量,可能被多个线程同时访问,则会出现线程安全问题;无状态 bean 没有定义成员变量,每个线程访问不会产生线程安全问题。

SpringBoot

原理

依赖启动器starter

在Spring Boot项目构建过程中,通过starter依赖启动器简化构建配置,例如,在Web开发时,只需在构建项目时选择对应的Web场景依赖启动器spring-boot-starter-web,Spring Boot项目便会自动导入spring-webmvc、spring-web、spring-boot-starter-tomcat等子依赖,并自动下载和获取Web开发需要的相关JAR包。

自动装配

  • Spring Boot的自动装配功能是通过使用@EnableAutoConfiguration注解来实现的。这个注解会引导Spring Boot根据项目中依赖的jar包和配置文件自动配置应用程序。
  • Spring Boot内部默认提供了很多自动化配置类(例如 MybatisAutoConfiguration),开发者无须手动在配置文件中进行相关配置(除非开发者需要更改默认配置),提高了程序的开发效率。并且通过自定义的@Conditional注解来实现条件装配,这些注解会在特定条件下才会实例化bean。
  • 比如当前类路径下有Mybatis 这个JAR包,MybatisAutoConfiguration注解就能根据相关参数来配置Mybatis 的各个 Spring Bean。

自动装配逻辑大致如下:

  1. 加载,从配置文件META-INF/spring.factories加载所有可能用到的自动配置类;
  2. 去重,并将exclude和excludeName属性携带的排除;
  3. 过滤,将满足条件(@Conditional) 的自动配类返回;

内嵌Tomcat、Jetty服务器

Spring Boot框架内嵌了Tomcat、Jetty和Undertow 服务器,而且可以自动将项目打包,并在项目运行时部署到服务器中。

使用jetty作为web容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!‐‐引入web模块‐‐>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring‐boot‐starter‐tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>

<!‐‐引入其他的Servlet容器‐‐>
<dependency>
<artifactId>spring‐boot‐starter‐jetty</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>

注解注入bean的方式

使用@Autowired方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//1.@Autowired加@Qualifier相当于@Resource
@Autowired
@Qualifier(value = "springComponent")
private SpringComponent getGetGetSpringComponent;

//2.set方法使用
private SpringComponent springComponent;
@Autowired
public void setSpringComponent(SpringComponent springComponent){
this.springComponent = springComponent;
}

//3.字段注入
@Autowired
private SpringComponent component;

//4.构造法方法的使用
private SpringComponent getSpringComponent;
@Autowired
public SpringTest(SpringComponent springComponent){
this.getSpringComponent = springComponent;
}

使用@Resource方式:

1
2
3
4
5
6
7
8
9
10
11
//***********************@Resource************************************
//1.@Resource使用字段注入
@Resource(name = "springComponent")
private SpringComponent getComponent;

//2.@Resource使用set方法使用
private SpringComponent getGetSpringComponent;
@Resource
public void setGetSpringComponent(SpringComponent springComponent){
this.getGetSpringComponent = springComponent;
}

使用@Bean方式:

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
public class AppConfig {
@Bean(autowire = Autowire.BY_TYPE)
public WyfBean1 wyfBean1(){
return new WyfBean1();
}
//autowire = Autowire.BY_TYPE很关键,往ioc容器注入bean对象,使用时候根据类型注入
//甚至不用再类里面写@Autowired了
@Bean(autowire = Autowire.BY_TYPE)
public WyfBean2 wyfBean2(){
return new WyfBean2();
}
}


public class Test {
public static void main(String[] args) {
//相当于AppConfig没加@Configuration自动给加了
AnnotationConfigApplicationContext applicationContext=new AnnotationConfigApplicationContext(AppConfig.class);
//调用wyfBean1的方法
WyfBean1 wyfBean1 = (WyfBean1) applicationContext.getBean("wyfBean1");
wyfBean1.test();
}
}


public class WyfBean1 {
private WyfBean2 wyfBean2;
public void setWyfBean2(WyfBean2 wyfBean2) {
this.wyfBean2 = wyfBean2;
}
public void test(){
//在WyfBean1下无@Autowired注入依赖
//@Bean的autowire属性自动根据类型获取到了代理对象并且通过setWyfBean2赋值给wyfBean2
System.out.println("wyfBean2的值是:"+wyfBean2);
}

}

springboot项目部署到Linux

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
#读取配置文件优先级(按优先级从高到低):
jar 包所在目录下的config/文件夹(即./config/bootstrap.yml)
jar 包所在的根目录(即./bootstrap.yml)
jar 包内部的yml配置

#启动命令:
#先关闭应用
ps -ef |grep smp-1.0.0.RELEASE |grep -v grep | awk '{print "kill -9 " $2}'| sh

#2表示错误输出,1是标准输出,
#>(覆盖重定向) >>(追加重定向)
#>/dev/null 2>&1 & 表示后台执行并且不输出日志到控制台
#>/dev/null标准错误和标准输出全都丢到/dev/null里面
#/dev/null 是一个特殊的设备文件,会丢弃所有写入的数据,再加上&后台运行

#通用命令(日志文件和配置文件写在jar包目录下的config里面,加载外部依赖jar包):
nohup java -jar -Xms256m -Xmx256m JAR_NAME -Dloader.path=./lib --server.port=2030 >/dev/null 2>&1 &

#日志输出到指定文件,覆盖重定向
nohup java -Xms512m -Xmx512m -cp ./lib -jar JAR_NAME >/logs/app.trace &

#使用JAVA_OPTS拼接启动参数启动
JAVA_OPTS=-Dspring.cloud.nacos.discovery.namespace=inecuas
JAVA_OPTS="$JAVA_OPTS "-Dspring.cloud.nacos.discovery.server-addr=localhost:8848""
JAVA_OPTS="$JAVA_OPTS "-Dspring.cloud.nacos.config.namespace=inecuas""
JAVA_OPTS="$JAVA_OPTS "-Dspring.cloud.nacos.config.server-addr=localhost:8848""
JAVA_OPTS="$JAVA_OPTS "-Dspring.config.location=/cfg/application.yml""
JAVA_OPTS="$JAVA_OPTS "-Dlogging.config=/cfg/logback.xml""
nohup java -Xms512m -Xmx512m $JAVA_OPTS -cp ./lib -jar JAR_NAME >/logs/app.trace &

配置操作

yml指定运行配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
application,yml中配置:
spring:
profiles:
active: dev
-----------------
spring:
config:
activate:
on-profile: dev

或者
java -jar spring-boot-02-config-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev;
可以运行application-dev.yml文件

日志配置

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
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%‐5level:级别从左显示5个字符宽度
%logger{50}表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符

示例:%d{yyyy‐MM‐dd HH:mm:ss.SSS}[%thread]%‐5level %logger{50}‐%msg%n

#日志配置,path和name只能设置一个生效,设置path不能只能日志名称默认名称是spring.log
#设置name要指定路径
logging:
file:
# path: C:\Java\Projects\TestProjects\lh-test-project\springcloud-test\src\main\resources\logs\
name: C:\Java\Projects\TestProjects\lh-test-project\springcloud-test\src\main\resources\logs\wyf.log
level:
src/main/java/com/example: info
src/test/java/com/example: info
root: info


#在控制台输出的日志的格式
logging.pattern.console=%d{yyyy‐MM‐dd}[%thread]%‐5level %logger{50}‐%msg%n
#指定文件中日志输出的格式
logging.pattern.file=%d{yyyy‐MM‐dd}=== [%thread]=== %‐5level === %logger{50}==== %msg%n

读取xml配置文件

  1. 在 resource 下创建 spring 传统配置文件,可以定义springboot没被扫描到的bean。
1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!--在传统spring配置文件中托管springboot默认无法扫描到的bean -->
<bean id="helloService" class="org.hpit.demo.service.HelloService"></bean>

</beans>
  1. 在启动类App.java 中配置引入配置文件的注解@ImportResource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootApplication
@ServletComponentScan //开始servlet扫描
@ComponentScan(basePackages = {"com.hpit"})
@ImportResource(locations = {"applicationContext.xml"}) //导入spring配置文件
public class App {

public static void main(String[] args) throws Exception {
SpringApplication.run(App.class, args);
}

//配置文件上传@Bean
public MultipartConfigElement multipartConfigFactory(){
MultipartConfigFactory configFactory = new MultipartConfigFactory();
configFactory.setMaxFileSize("128MB");// KB MB 设置单个上传文件大小
configFactory.setMaxRequestSize("1024MB");
configFactory.setLocation("/");//设置文件上传路径
return configFactory.createMultipartConfig();
}}

CommandLineRunner

项目启动任务

1
2
3
4
5
6
7
8
9
10
@Component
@Order(value = 1)//设置启动执行顺序
public class MyCommandRunner1 implements CommandLineRunner {
/**
* TODO 系统启动即会执行Run方法
*/
@Override
public void run(String... arg0) throws Exception {
//执行启动任务
}}

EnvironmentAware

读取系统环境变量

配置类实现EnvironmentAware:

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
/**
* TODO 读取spring以及系统环境变量主要是@Configuration,实现接口:EnvironmentAware就能获取到系统
环境信息; **@author 郑江山
*
*/
@Configuration
public class MyEnvironment implements EnvironmentAware {

@Value("${spring.datasource.url}")//使用el表达式读取spring主配置文件
private String jdbcUrl;
private Logger logger = Logger.getLogger(getClass());

@Override
public void setEnvironment(Environment environment){
// springEL表达式获取的值
logger.info("springel表达式获取的值:"+ jdbcUrl);
//获取系统属性:
logger.info("JAVA_HOME"+ environment.getProperty("JAVA_HOME"));
//获取spring主配置文件中的属性
logger.info("spring.datasource.url:"+ environment.getProperty("spring.datasource.url"));
//获取前缀是“spring.datasource”的所有属性值
RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(environment,
"spring.datasource.");
logger.info("通过前缀获取的url:"+ propertyResolver.getProperty("url"));
logger.info("通过前缀获取的driverClassName:"+ propertyResolver.getProperty("driverClassName"));
}}

如果在Controller中读取系统变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* TODO 在普通的控制器和Servie业务中也可以直接实现EnvironmentAware来获取系统环境变量,但是在获取系统环境变量的时机为系统加载的时候
**@author 郑江山
*/
@RestController
@RequestMapping("/system")
public class SystemEnvironmentController implements EnvironmentAware {
private String java_home;
private Logger logger = Logger.getLogger(getClass());

@RequestMapping("/javahome")
public String getJAVAHOME(){
return java_home; }

@Override
public void setEnvironment(Environment environment){
java_home = environment.getProperty("JAVA_HOME");
logger.info("控制器中获取的系统环境变量:"+ java_home);
}}

Springboot集成redis 缓存

  1. 导入redis依赖
1
2
3
4
5
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 编写redis配置类,解决自动生成key的乱码,启动缓存,自定义 key等

    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
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    /**
    * redis 缓存配置; **注意:RedisCacheConfig 这里也可以不用继承:CachingConfigurerSupport,也就是直接一个普通的Class 就好了;
    **这里主要我们之后要重新实现 key 的生成策略,只要这里修改 KeyGenerator,其它位置不用修改就生效
    了。
    **普通使用普通类的方式的话,那么在使用@Cacheable 的时候还需要指定 KeyGenerator 的名称;这样编码的时候比较麻烦。
    **@author Zjs
    *@version v.0.1
    */
    @Configuration
    @EnableCaching//启用缓存,这个注解很重要;
    publicclass RedisCacheConfig extends CachingConfigurerSupport {
    /**
    *缓存管理器.*@param redisTemplate
    *@return
    */
    @Bean
    public CacheManager cacheManager(RedisTemplate<?,?> redisTemplate){
    CacheManager cacheManager = new RedisCacheManager(redisTemplate);
    returncacheManager;
    }

    /**
    * redis 模板操作类,类似于 jdbcTemplate 的一个类; *
    *虽然 CacheManager 也能获取到 Cache 对象,但是操作起来没有那么灵活;
    **这里在扩展下:RedisTemplate 这个类不见得很好操作,我们可以在进行扩展一个我们
    **自己的缓存类,比如:RedisStorage 类; **@param factory : 通过 Spring 进行注入,参数在 application.properties 进行配置;
    *@return
    */
    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){
    RedisTemplate<String, String> redisTemplate = new RedisTemplate<String, String>();
    redisTemplate.setConnectionFactory(factory);
    //key 序列化方式;(不然会出现乱码;),但是如果方法上有 Long 等非 String 类型的话,会报类型转换错误;
    //所以在没有自己定义 key 生成策略的时候,以下这个代码建议不要这么写,可以不配置或者自己实现 ObjectRedisSerializer
    //或者 JdkSerializationRedisSerializer 序列化方式;
    RedisSerializer<String> redisSerializer = new StringRedisSerializer(); //Long 类型不可以会出现异常信息;
    redisTemplate.setKeySerializer(redisSerializer);
    redisTemplate.setHashKeySerializer(redisSerializer);
    returnredisTemplate; }

    /**
    *自定义 key.
    *此方法将会根据类名+方法名+所有参数的值生成唯一的一个 key,即使@Cacheable 中的 value 属性一样,key 也会不一样。
    */
    @Override
    public KeyGenerator keyGenerator(){
    System.out.println("RedisCacheConfig.keyGenerator()");
    returnnew KeyGenerator(){
    @Override
    public Object generate(Object o, Method method, Object... objects){
    // This will generate a unique key of the class name, the method name
    //and all method parameters appended.
    StringBuilder sb = new StringBuilder();
    sb.append(o.getClass().getName());
    sb.append(method.getName());
    for (Object obj : objects){
    sb.append(obj.toString());
    }
    System.out.println("keyGenerator="+ sb.toString());
    returnsb.toString();}}
    }

SpringBoot根据aop自定义注解:

定义注解:

1
2
3
4
5
6
7
8
9
@Documented
@Inherited
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME) //注解会在class字节码文件中存在,在运行时可以通过反射获取到
public @interface MyAnnotation {
Class<?>[] groups() default {};
int[] vals() default {};
String value() default "wyf777";
}

定义aop:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Aspect
@Component
@Slf4j
public class MyAnnotationAOP {

//切点
@Pointcut("@annotation(com.example.annotation.MyAnnotation)")
public void logger(){
}

//环绕通知
@Around("logger()")
public Object around(ProceedingJoinPoint point){
//获得方法名称
Signature methodName=point.getSignature();
//日志输出
log.info(methodName+"进来了");
Long l1=System.currentTimeMillis();
//让方法执行
Object obj=null;
try {
obj=point.proceed(point.getArgs());
} catch (Throwable throwable){
throwable.printStackTrace();
}
log.info(methodName+"走了"+"\t耗时"+(System.currentTimeMillis()-l1));

//可以控制方法的返回值
return obj;
}

//切点之前
/*@Before("logger()")
public void before(JoinPoint point){
Signature methodName=point.getSignature();
log.info(methodName+"进来了");
}*/

//切点执行之后,未返回之前
/*@After("logger()")
public void after(JoinPoint point){
Signature methodName=point.getSignature();
log.info(methodName+"进来了");
}*/

//返回之后
/*@AfterReturning("logger()")
public void afterReturning(JoinPoint point){
Signature methodName = point.getSignature();
log.info(methodName +"进来了");
}*/

//抛出异常后
//@AfterThrowing("logger()")
// public void afterReturning(JoinPoint point){
// Signature methodName = point.getSignature();
// log.info(methodName +"进来了");
//}

}

使用注解

1
2
3
4
5
6
7
8
9
@GetMapping("/test1")
@MyAnnotation
public String test1(){
return "wyf";
}

运行接口结果是打印:
com.wyf.controller.test1()进来了
com.wyf.controller.test1()走了耗时124

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//先编写@interface,然后根据你想模仿的注解对照写,例如写校验的注解
//另外Constraint里面的通过哪个类校验可以自己创建。
@Documented
@Constraint(validatedBy = {ListValConstraintValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String message() default "{com.atguigu.common.valid.ListVal.message}";
//default的数据在ValidationMessages.properties里面,可以自己定义

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

int[] vals() default {};
}

Springboot注解:

启动类注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootApplication启动类核心注解如下:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan()
@Filter()
@Component
@Service
@Bean
@Configration


@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited

列举常用注解(面试常问)

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
@Component
@Service
@Controller
@Repository
@SpringBootApplication
@PostConstruct
@Configration
@Transactional
@ComponentScan
@RequestMapping
@Autowired
@Resource
@Conditional
@Bean
@Lazy
@ControllerAdvice
@Validated
@Async
@Value


//根据名称进行注入
@Qualifier(value = "userDaoImpl1")
//不写参数根据类型注入,写参数根据名称注入
@Resource(name = "userDaoImpl1")
//销毁方法的注解方式,等同于destory-method=destoryTest
@PreDestroy
//导入spring配置文件
@ImportResource(locations = {"applicationContext.xml"})

Controller上的注解

1
2
3
4
5
6
@RequestBody List<AttrGroupRelationVO> list	参数在请求体里,只能是post请求,一个controller只能有一个RequestBody 

@PathVariable("attrgroupId") Long attrgroupId 参数在rul的/{...}/中,属于可变变量。

@RequestParam Map<String, Object> params 参数在请求头中,作为params,可作为url上?后面的键值对。
如果controller方法没加注解的参数如(String name),那么也是作为键值对附在?后面。

@ConfigurationProperties

使用该注解该类里面的变量可以在properties文件里面进行配置,别的bean注入这个bean就可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1.首先导入依赖
<dependency>
<groupId> org.springframework.boot </groupId>
<artifactId> spring-boot-configuration-processor </artifactId>
<optional> true </optional>
</dependency>

2.编写配置实体类
@ConfigurationProperties(prefix = "gulimall.thread")
@Data
@Component
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;}

3.编写配置类
@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {

}

@ConditionalOn相关注解

满足指定条件的时候才将某个 bean 加载到应用上下文中,主要放在类上。

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
@ConditionalOnProperty(prefix="user",value="name",havingValue="wyf",matchIfMissing=true)
表示yml中的user.name值是wyf才加载这个bean,默认不写为也会加载,因为有matchIfMissing=true

@ConditionalOnBean 和 ConditionalOnMissingBean
表示需要某个 Bean 已经存在应用上下文时才会加载,如@ConditionalOnBean(OtherBean.class)

@ConditionalOnExpression
有更复杂的多个配置属性一起判断,那么我们就可以用这个表达式了,如
@ConditionalOnExpression("${user.name=wyf} and ${other.username=lzx}")

@ConditionalOnSingleCandidate
这个注解和@ConditionalOnBean 类似,只有指定类已存在于 BeanFactory 中,并且可以确定单个候选项才会匹配成功。

@ConditionalOnResource
要加载的 bean 依赖指定资源是否存在于 classpath 中,存在才加载这个bean,如
@ConditionalOnResource(resources="/logback.xml")

@ConditionalOnJndi
只有指定的资源通过 JNDI 加载后才加载 bean,如@ConditionalOnJndi("java:comp/env/foo")

@ConditionalOnJava
只有运行指定版本的 Java 才会加载 Bean,如@ConditionalOnJava(JavaVersion.EIGHT)

@ConditionalOnWebApplication 和@ConditionalOnNotWebApplication
只有运行在 web 应用里才会加载这个 bean和非 web 环境才加载 bean

@ConditionalOnCloudPlatform
这个注解冷的我呼吸都要停止了,只有运行在指定的云平台上才加载指定的 bean,CloudPlatform 是 org.springframework.boot.cloud 下一个 enum 类型的类,如@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)

@PostConstruct

什么时候被调用?

1
2
3
4
@PostConstruct 方法在Spring容器初始化bean完成后执行。
具体来说,当Spring完成依赖注入并且 bean 的所有属性都已设置好之后,@PostConstruct 注解的方法会被调用。
启动优先级是:构造方法> @Autowired > @PostConstruct

@Configration

  • @Configuration 的底层原理主要是基于 Spring 中的 IoC(控制反转)和 AOP(面向切面编程)机制实现的。
  • 在 Spring 容器启动时,会扫描所有被 @Configuration 注解的类,通过解析其中的@Bean注解和其他相关注解,来生成对应的 Bean 定义并注册到 Spring 容器中。
  • 还支持使用 @Import 注解来引入其他配置类,从而将多个配置类组合起来形成一个完整的配置信息

@Configration保证bean是单例原因:

  1. 类上加@Configration注解是保证是代理对象执行每个方法
  2. 执行每个方法后,如果加了@Bean,会去map容器查有没有该bean,没有会创建bean加入容器。
  3. 例如一个加了@Bean的方法里面调用了另一个加了@Bean的方法,去获取返回的bean结果,如果map容器里有会直接拿到,保证单例。

@ConfigurationProperties

配置文件值注入,编写了一个javaBean来和配置文件进行映射,还可以对参数进行校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--导入配置文件处理器,配置文件进行绑定就会有提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐configuration‐processor</artifactId>
<optional>true</optional>
</dependency>

@Data
@ConfigurationProperties(prefix = "person")
@Validated
public class MyConfig {
//@Email
private String name;
private Map<String,String> maps;
private List<String> list;
}

启动类加:@EnableConfigurationProperties(MyConfig.class)
这样就能直接注入自定义的配置了

//${random.uuid}是yml中可以使用的函数
person.last‐name=张三${random.uuid}

@ControllerAdvice

1
2
3
4
5
6
7
8
9
//controller层的全局异常处理
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseStatus(value= HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Object defaultErrorHandler(HttpServletRequest req, Exception e){
return "出现异常"+e.toString();
}}

SpringMVC

介绍

  • 基于原生的Servlet,通过功能强大的前端控制器DispatcherServlet,对请求和响应进行统一处理。
  • 分为模型,视图,控制器三类。
  • springmvc极大简化了原生servlet代码量,一个HttpServlet的实现类才是一个url请求,使用注解的方式就能定义请求方式、url的路径、参数,返回值等。

SpringMVC执行流程:

简述流程:

  1. DispatcherServlet(前端控制器)收到请求,会调用Handlermapping根据对应的url找到对应的Handler和处理器拦截器(如果有则生成),一起返回给DispatcherServlet。统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求
  2. DispatcherServlet会根据Handler找到对应的HandlerAdaptor,如果有拦截器此时执行拦截器的方法,然后去执行Handler的业务代码(Handler相当于一个继承HttpServlet的类,里面包含了controller的业务逻辑),处理完业务逻辑利用HttpMessageConveter把响应消息转换成json之类的格式,如果配置了@ResponseBody直接返回结果,没有就放进ModleAndView返回给DispatcherServlet。
  3. DispatcherServlet拿到ModleAndView会调用ViewResolver视图解析器进行解析,返回具体View给DispatcherServlet。
  4. DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中),响应用户。

详细流程:

  1. 启动tomcat,创建DispatcherServlet,初始化各种处理器。

  2. 初始化HandlerMapping,会找三个handlermapping,第一个是beannameURLhandlermapping,第二个是RequestMappingHandlerMapping,第三个是RouterFunction。

  3. 收到请求,经过创建RequestMappingHandlerMapping的bean对象,经过afterPropertiesSet方法,期间是先生成一个map,k是path,v是requestmappingInfo的list,通过这个遍历这个list判断请求方法,匹配对应的RequestMappingInfo,最终生成map,k是path,v是对应的method

  4. MethodArgumentResolver方法参数解析器,该解析器在创建bean之后,进行初始化afterPropertiesSet会把很多注解解析器加载到resolver的list里面,随后解析参数的注解如RequestParam等等,解析url请求参数给方法的参数赋值。

  5. 初始化HandlerAdaptor,根据handlermapping来找到对应的HandlerAdaptor

  6. MethodReturnValueHandler返回值处理器,会判断有没有加ResponseBody,选择合适的handler去处理返回值。

  7. HttpMessageConvert返回值转换器,默认有四种caonvert如下图,返回值是数组,字符串,都要相应的convert处理,如果返回值是对象,原生spring不支持,要自己配置jackson,但是springboot默认把使用Jackson2HttpMessageConvert加进来了,该jackson解析器会自动解析所有数据类型成json返回前端。

    image-20230223215819871

原生Servlet处理请求流程:

**常用类:**HttpServletRequest ,HttpServletResponse ,HttpSession,RequestEntity

HttpServlet处理请求流程:

  1. 编写HttpServlet的实现类,重写doGet和doPost方法写处理逻辑,绑定对应的url然后注册
  2. web服务器解析请求的url找到对应的HttpServlet类,先调用HttpServlet的service(ServletRequest req, ServletResponse res)方法,如果重写这个方法,优先级会高于doGet和doPost方法。
  3. 再调用本类的service(HttpServletRequest req, HttpServletResponse resp)方法,判断方法是get还是post还是Put等等
  4. 如果是get方法调用本类的doGet(req, resp),执行自定义的处理逻辑。

原生Servlet处理请求代码:

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
41
42
43
44
45
46
47
48
49
50
51
52
//一个HttpServlet的实现类就是一个url请求,所以说springmvc极大简化了原生servlet代码量
//重写doGet和doPost方法写处理逻辑
import javax.servlet.http.HttpServlet;

public class Test2Servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json; charset=utf-8");
//处理逻辑...
String jsonStr = "{\"name\":\"fly\",\"type\":\"虫子1\"}";
PrintWriter out = null;
out = resp.getWriter();
out.write(jsonStr);
}
}

//使用配置类给web注册一个servlet接口
@Configration
public class HelloApplication {
//关键是这里,给web注册一个servlet接口
@Bean
public ServletRegistrationBean MyServlet(){
//servlet接口的访问路径和controller接口的访问路径相同
return new ServletRegistrationBean(new Test2Servlet(),"/test/hello");
}
}

//或者使用xml方式注册servlet接口
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0">
<display-name>ServerTest</display-name>

<servlet>
<!-- Servlet类的别名 ,可以不与java类名相同 -->
<servlet-name>TestOne</servlet-name>
<!-- 所在的包,为了避免出现错误,一定要写全名 -->
<servlet-class>com.example.servlet.Test2Servlet</servlet-class>
</servlet>

<!-- 绑定对应的url -->
<servlet-mapping>
<servlet-name>TestOne</servlet-name>
<!-- 提供访问的网址url,供浏览器使用 -->
<url-pattern>/test/hello</url-pattern>
</servlet-mapping>
</web-app>

web拦截器HandlerInterceptor

拦截器执行流程:

  1. 定义MyInterceptor 实现HandlerInterceptor接口

  2. 重写preHandle,postHandle,afterCompletion三个方法,

  3. preHandle用于Controller方法处理之前

  4. postHandle在preHandle返回true并且Controlller完成后,可以对ModelAndView进行操作

  5. afterCompletion在preHandle返回true,DispatcherServlet进行视图的渲染之后,多用于清理资源

  6. mvc配置,继承WebMvcConfigurer,重写addInterceptors添加拦截器

拦截器代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyWebInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("处理请求前执行的方法");
return false;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("preHandle返回true并且Controlller完成后执行的方法");
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
System.out.println("完成请求之后执行的方法");

}
}

配置类代码:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class WebConfig implements WebMvcConfigurer {

//添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//重写addInterceptors方法并为拦截器配置拦截规则
registry.addInterceptor(new MyWebInterceptor()).addPathPatterns("/**");
//排除路径
//registry.addInterceptor(new MyWebInterceptor()).addPathPatterns("/**").excludePathPatterns("/Hello");
}
}

SpringMVC内置处理器

CharacterEncodingFilter:

字符编码过滤器

1.解决获取请求参数的乱码问题,可以使用SpringMVC提供的编码过滤器CharacterEncodingFilter
2.SpringMVC中处理编码的过滤器一定要配置到其他过滤器之前,否则无效

HttpMessageConverter:

报文信息转换器

将请求报文转换为Java对象,或将Java对象转换为响应报文。
@ResponseBody处理json的步骤:

  1. 导入jackson的依赖
  2. 在SpringMVC的核心配置文件中开启mvc的注解驱动,此时在HandlerAdaptor中会自动装配一个消息转换器:MappingJackson2HttpMessageConverter,可以将响应到浏览器的Java对象转换为Json格式的字符串。
  3. 在处理器方法上使用@ResponseBody注解进行标识

HiddenHttpMethodFilter:

http方法过滤器

将 POST 请求转换为 DELETE 或 PUT 请求,
HiddenHttpMethodFilter 处理put和delete请求的条件:
1.当前请求的请求方式必须为post
2.当前请求必须传输请求参数_method

HandlerExceptionResolver:

处理异常解析器

处理控制器方法,执行过程中所出现的异常的接口:
HandlerExceptionResolver接口的实现类有:
DefaultHandlerExceptionResolver和
SimpleMappingExceptionResolver

解决跨域:

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
//解决跨域方法1:重写springmvc的配置类,添加规则
@Configuration
public class WebConfig implements WebMvcConfigurer {
//配置跨域,所有请求都能通过
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.allowCredentials(true)
.allowedHeaders("*")
.maxAge(3600);
}
}


//解决跨域方法2:添加SpringMvc的CorsFilter过滤器
@Configuration
public class SecurityConfig {

@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
final CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOriginPattern("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}

全局异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 全局异常处理注解:@ControllerAdvice
*/
@ControllerAdvice
public class GlobalExceptionHandler {

@ResponseStatus(value= HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Object defaultErrorHandler(HttpServletRequest req, Exception e){
return Result.fail(400,e.getMessage());
//return "出现异常"+e.toString();
}
}

重定向

重定向原理:

当控制器方法返回以 redirect: 开头的字符串时,Spring MVC 会自动将其处理为一个重定向响应(HTTP 状态码 302,临时重定向),浏览器会接收到这个响应并向新的 URL 发起一个 GET 请求,并且mvc还可以往页面添加cookie和header。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//springmvc默认会读取resources/static下面的html页面
//如该目录下有一个login.html,可以直接访问http://localhost:8621/login.html

//常用重定向方式:
@PostMapping("/login")
public String login(){
//return “index”;
//注意post请求不能进行return ”index“,需要return "redirect:http://gulimall.com";
//重定向另一个一个页面,携带数据给前端处理
return "redirect:http://auth.gulimall.com/login.html";
}

//重定向往网址添加header和cookie
@PostMapping("/redirectWithHeadersAndCookie")
public RedirectView redirectWithHeadersAndCookie(HttpServletResponse response) {
// 创建 RedirectView,指定重定向目标地址
RedirectView redirectView = new RedirectView("http://localhost:8837/loginSuccess.html");
redirectView.setStatusCode(HttpStatus.FOUND); //设置302状态码(默认就是 302)

// 添加Header,post请求会添加到url上
redirectView.addStaticAttribute("X-Custom-Header", "custom-value");
// 或通过 HttpServletResponse 添加 Header(更灵活)
// response.setHeader("X-Another-Header", "another-value");

// 添加 Cookie
Cookie customCookie = new Cookie("user_token", "abc123");
customCookie.setPath("/"); // 设置 Cookie 生效路径
customCookie.setMaxAge(3600); // 有效期 1 小时(秒)
customCookie.setHttpOnly(true); // 增强安全性,防止 JS 读取
response.addCookie(customCookie);
return redirectView;
}


//axios处理后端的重定向:
//通过 AJAX(如 fetch、axios)发送 POST 请求,浏览器不会自动跟随 302 重定向进行页面跳转(AJAX 仅接收响应,不会触发页面导航)。
//解决方法:在 AJAX 响应中手动处理重定向,获取重定向 URL 后通过 window.location.href 跳转:
// 以 axios 为例
axios.post('/submit', data)
.then(response => {
// 正常响应处理(如果需要)
}).catch(error => {
// 302 会被 axios 视为错误(状态码在 2xx 之外)
if (error.response && error.response.status === 302) {
// 获取重定向地址(通常在响应头 Location 中)
const redirectUrl = error.response.headers.location;
if (redirectUrl) {
window.location.href = redirectUrl; // 手动跳转
}
}
});



SpringMvc注解

1
2
3
4
5
6
7
8
@RestController
@GetMapping
@RequestParam(value = "redirect_url",required = false) 获取param参数
@PathVariable
@RequestBody
@ResponseBody
@RequestHeader
@CookieValue(value = "sso_token",required = false) 获取cookie值

SpringSession

作用:

Session 共享:指在一个浏览器访问多个Web服务时,服务端的Session数据需要共享。
Session 共享应用场景:单点登录,Web 服务器集群等场景

依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

配置:

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
//session规定是一个父域名只有一个sessionid,简单就是说一台机器所有浏览器只能有一个用户登录。
//配置文件
Spring
session:
store-type: redis
timeout: 30m

//加注解
@EnableRedisHttpSession
@Configuration
public class GulimallSessionConfig {

// cookie序列化工具,设置session的域名范围为父域名.gulimall.com
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer defaultCookieSerializer=new DefaultCookieSerializer();
defaultCookieSerializer.setDomainName("gulimall.com");
defaultCookieSerializer.setCookieName("GULISESSION");
return defaultCookieSerializer;
}

//将redis里面的session序列化为json
@Bean
public RedisSerializer<Object> springSessionDefaultSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}

SpringSecurity

用户认证流程图如下:

image-20230417182114720

开启步骤:

如下示例开启步骤基于前后端分离的场景

项目在云盘里的spring_security_springboot.zip

实现WebSecurityConfigurerAdapter

SpringSecurity的基础设置,继承WebSecurityConfigurerAdapter重写configure方法。

设置哪些url不会拦截,设置拦截进入的登录页面地址,点击确认进入的地址,成功地址,失败地址,csrf,登入登出的过滤器。

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
41
42
43
44
45
46
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
//这个注解表示可以加在controller方法上进行校验
//如在controller上加:@Secured(value ={"ROLE_wyf","ROLE_lzx"}),对角色进行过滤,前缀是ROLE_
//@PreAuthorize(value = "hasAnyAuthority('admins')")//用户权限里面只要有一个admins,对用户权限进行过滤
//@PostFilter("filterObject.username=='admin1'") //对返回值进行过滤,比如返回很多用户,过滤掉username=admin1的
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private TokenUtils tokenUtils;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private DefaultPasswordEncoder defaultPasswordEncoder;
@Autowired
private UserDetailsService userDetailsService;

/**
*配置设置
*/
//设置退出的地址和token,redis操作地址
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new UnauthEntryPoint())//没有权限访问
.and().csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and().logout().logoutUrl("/admin/acl/index/logout")//退出路径
.addLogoutHandler(new TokenLogoutHandler(tokenUtils, redisTemplate))
.and()
.addFilter(new TokenLoginFilter(authenticationManager(), tokenUtils, redisTemplate))
.addFilter(new TokenAuthFilter(authenticationManager(), tokenUtils, redisTemplate))
.httpBasic();
}
//调用userDetailsService和密码处理
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
}
//不进行认证的路径,可以直接访问
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**");
}
}

实现UserDetailsService

用户提交表单先进入这个类,根据用户名查询用户是否存在。

存在就把用户信息封装成UserDetails的实现类,然后去到UsernamePasswordAuthenticationFilter去跟提交的表单数据进行认证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询数据
User user = userService.selectByUsername(username);
//判断
if(user == null){
throw new UsernameNotFoundException("用户不存在");
}
com.atguigu.security.entity.User curUser = new com.atguigu.security.entity.User();
BeanUtils.copyProperties(user,curUser);
//根据用户查询用户权限列表
List<String> permissionValueList = permissionService.selectPermissionValueByUserId(user.getId());
SecurityUser securityUser = new SecurityUser();
securityUser.setCurrentUserInfo(curUser);
securityUser.setPermissionValueList(permissionValueList);
return securityUser;
}
}

继承UsernamePasswordAuthenticationFilter

登录过滤器,登录提交表单先调用UserDetailService查真正的用户信息然后到这个类。

把提交的表单与真正的用户信息比对,同时编写成功和失败的方法。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private TokenUtils tokenUtils;
private RedisTemplate redisTemplate;
private AuthenticationManager authenticationManager;

public TokenLoginFilter(AuthenticationManager authenticationManager, TokenUtils tokenUtils, RedisTemplate redisTemplate){
this.authenticationManager = authenticationManager;
this.tokenUtils = tokenUtils;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
}
/**
*获取表单提交用户名和密码
*把loadUserByUsername获取真的账号消息与提交的表单进行尝试认证
*认证成功会把主体UserDetails也就是SecurityUser放入Authentication
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//获取表单提交数据
try {
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword(), new ArrayList<>()));
} catch (IOException e){
e.printStackTrace();
throw new RuntimeException();
}
}
/**
*认证成功调用的方法
*拿着attemptAuthentication提交的账号密码与userService查到数据库的主体做对比
* attemptAuthentication认证完成的Authentication中读取主体
*成功就调这个方法,生成token放入redis
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
//认证成功,得到认证成功之后用户信息
SecurityUser user = (SecurityUser)authResult.getPrincipal();
//根据用户名生成token
String token = tokenUtils.createToken(user.getCurrentUserInfo().getUsername());
//把用户名称和用户权限列表放到redis
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList());
//返回token
ResponseUtil.out(response, R.ok().data("token",token));
}
//认证失败调用的方法
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}

继承BasicAuthenticationFilter

权限过滤器,经过UsernamePasswordAuthenticationFilter已经认证成功来到这。

  1. 从请求头拿token,解析出username去redis拿到权限列表。
  2. 把username,token,权限列表封装成UsernamePasswordAuthenticationToken,放进SecurityContextHolder的上下文。
  3. 完成之后可以从SecurityContextHolder上下文的Authentication从而获取用户信息和权限。
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
public class TokenAuthFilter extends BasicAuthenticationFilter {
private TokenUtils tokenUtils;
private RedisTemplate redisTemplate;
public TokenAuthFilter(AuthenticationManager authenticationManager, TokenUtils tokenUtils, RedisTemplate redisTemplate){
super(authenticationManager);
this.tokenUtils = tokenUtils;
this.redisTemplate = redisTemplate;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//获取当前认证成功用户权限信息
UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
//判断如果有权限信息,放到权限上下文中
if(authRequest != null){
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
chain.doFilter(request,response);
}

private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){
//从header获取token
String token = request.getHeader("token");
if(token != null){
//从token获取用户名
String username = tokenUtils.getUserInfoFromToken(token);
//从redis获取对应权限列表
List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username);
Collection<GrantedAuthority> authority = new ArrayList<>();
for(String permissionValue : permissionValueList){
SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
authority.add(auth);
}
return new UsernamePasswordAuthenticationToken(username,token,authority);
}
return null;
}
}

继承UserDetails

用户详细信息类,在UserDetailsService中查询数据库的真实数据然后封装成UserDetails,设置用户的密码权限等。

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
41
42
43
44
45
46
47
48
49
50
@Data
public class SecurityUser implements UserDetails {
//当前登录用户
private transient User currentUserInfo;

//当前权限
private List<String> permissionValueList;
public SecurityUser(){}
public SecurityUser(User user){
if (user != null){
this.currentUserInfo = user;
}
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities(){
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList){
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}

@Override
public String getPassword(){
return currentUserInfo.getPassword();
}
@Override
public String getUsername(){
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired(){
return true;
}
@Override
public boolean isAccountNonLocked(){
return true;
}
@Override
public boolean isCredentialsNonExpired(){
return true;
}
@Override
public boolean isEnabled(){
return true;
}
}

实现PasswordEncoder

自定义PasswordEncoder并且加入ioc容器,用于密码加密和匹对,MD5是自己写的工具类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
public DefaultPasswordEncoder(){
this(-1);
}
//进行MD5加密
@Override
public String encode(CharSequence charSequence){
return MD5.encrypt(charSequence.toString());
}
//进行密码比对,charSequence是加密过的,encodedPassword是传入的密码
@Override
public boolean matches(CharSequence charSequence, String encodedPassword){
return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
}
}

实现LogoutHandler

退出处理器,redis删除用户信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TokenLogoutHandler implements LogoutHandler {
private TokenUtils tokenUtils;
private RedisTemplate redisTemplate;

public TokenLogoutHandler(TokenUtils tokenUtils, RedisTemplate redisTemplate){
this.tokenUtils = tokenUtils;
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
//1 从header里面获取token
//2 token不为空,移除token,从redis删除token
String token = request.getHeader("token");
if(token != null){
//移除
tokenUtils.removeToken(token);
//从token获取用户名
String username = tokenUtils.getUserInfoFromToken(token);
redisTemplate.delete(username);
}
ResponseUtil.out(response, R.ok());
}
}

实现AuthenticationEntryPoint

未授权处理类,用作WebSecurityConfigurerAdapter中配置处理异常的方法。

1
2
3
4
5
6
7
public class UnauthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e)
throws IOException, ServletException {
ResponseUtil.out(httpServletResponse, R.error());
}
}

工具类TokenUtils

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
41
@Component
public class TokenUtils {

public static void main(String[] args){
TokenUtils tokenUtils =new TokenUtils();
String wyf = tokenUtils.createToken("wyf");
System.out.println(wyf);
}

//token有效时长,1day
private long tokenEcpiration = 24 *60 *60 *1000;
//编码秘钥
private String tokenSignKey = "123456";

//1 使用jwt根据用户名生成token
public String createToken(String username){
String token = Jwts
.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis()+ tokenEcpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}

//2 根据token字符串得到用户信息
public String getUserInfoFromToken(String token){
String userinfo = Jwts.parser()
.setSigningKey(tokenSignKey)
.parseClaimsJws(token)
.getBody()
.getSubject();
return userinfo;
}

//3 删除token
public void removeToken(String token){

}
}

自动登录

步骤:

  1. 在PersistentTokenRepository初始化token持久化的数据源
  2. HttpSecurity配置自动登录,设置token过期时间
  3. 按钮自动登录的name设置是”remember-me”

如下示例基于前后端不分离的场景,继承WebSecurityConfigurerAdapter如下:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)//开启注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsServiceImpl userDetailsService;
//注入本项目数据源
@Autowired
private DataSource dataSource;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
*设置认证的地方是userDetailsService,设置加密器,这里没设置用户名密码就去userDetailsService去获取
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.inMemoryAuthentication().withUser("oo").password("xx");
auth.userDetailsService(userDetailsService).passwordEncoder(this.passwordEncoder());
}
//初始化token持久化的数据源是哪个,判断是否重启创建表persistent_logins
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
/**
*登录设置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单登录/认证配置//任何请求都需要身份验证
// http.formLogin().and().authorizeRequests().anyRequest().authenticated();

//配置认证
http.formLogin()
.loginPage("/login.html")//配置哪个页面为登录页面,可以是resource里面的
.loginProcessingUrl("/user/login")//调用的登录接口,这个controller不用自己写,系统会自动生产这个接口,自动识别username和password进行登录
.defaultSuccessUrl("/success.html").permitAll()//登录成功之后跳转到哪个 url
.failureForwardUrl("/fail")//登录失败之后跳转到哪个 url

//url的权限认证
.and()
.authorizeRequests()
//.antMatchers("/test/hello","/test").permitAll()//指定URL 无需保护,直接访问
//.antMatchers("/test/hello2").hasAuthority("xx")//指定url需要用户有admins权限才能访问
//.antMatchers("/test/hello2").hasAnyAuthority("admins,xx")//指定url需要用户有指定的任意一个权限才能访问,权限逗号隔开
//.antMatchers("/test/hello2").hasRole("wyf")//指定url需要用户有指定的角色才能访问,并且初始化时候前面要加"ROLE_"
.anyRequest().authenticated()//其他的url需要认证

//配置自动登录,还需要PersistentTokenRepository加入ioc,按钮的name是remember-me
.and().rememberMe().tokenRepository(persistentTokenRepository())
//.alwaysRemember(true)
.tokenValiditySeconds(60)//token是60s过期
.userDetailsService(userDetailsService)

//退出登录设置
.and()
.logout()
.logoutUrl("/logout").logoutSuccessUrl("/logout/success").permitAll()
//.and().csrf().disable()//关闭 csrf
;
}

}

流程:

  1. 用户先登录,点击多少天自动登录,认证成功生产token,去jdbc往数据库插入,浏览器里面会存储token并且带过期时间。
  2. 用户再次访问带着token去数据库查,匹配的话就登录成功。

加密

这种加密方式每次加密的密文都不一样,即使明文一样密文也不一样,类似盐值加密。

1
2
3
4
5
6
package org.springframework.security.crypto.bcrypt;

BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();
String encode = encoder.encode(vo.getPassword()); 加密
memberEntity.setPassword(encode);
boolean ismatch = encoder.matches(password, passwordDB); 匹对密文和明文是否一样

**(不常用)**用户可以在application.yml或者WebSecurityConfigurerAdapter的实现类重写

void configure(AuthenticationManagerBuilder auth)方法设置用户名密码,但只支持一个公共用户使用情况。

SpringCloud

历史版本对应图

SpringCloud版本与SpringBoot历史版本对应如下:

Release Train Release Train
2022.0.x aka Kilburn 3.0.x
2021.0.x aka Jubilee 2.6.x, 2.7.x (Starting with 2021.0.3)
2020.0.x aka Ilford 2.4.x, 2.5.x (Starting with 2020.0.3)
Hoxton 2.2.x, 2.3.x (Starting with SR5)
Greenwich 2.1.x
Finchley 2.0.x
Edgware 1.5.x
Dalston 1.5.x