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生命周期顺序(重点):
**实例化:**通过注解或者读取xml文件方式,利用类的无参构造+反射创建 Bean 的实例,在实例化的前后会执行对BeanDefinition的前置和后置方法。
依赖注入 :对@Autowired的Bean对象,@Value的常量进行赋值。
**执行Aware系列接口的回调:**如果 Bean 实现了特定的 Aware 接口,Spring 会调用对应的 setter 方法,将容器自身的资源信息注入给 Bean。如BeanNameAware:注入当前 Bean 在容器中的 id/name。
**BeanPostProcessor 的前置处理:**调用 BeanPostProcessor 接口的 postProcessBeforeInitialization() 方法,允许在 Bean 初始化前对 Bean 进行加工处理(例如生成代理对象,或者包装对象)。AOP 动态代理就是在这之后介入的。
初始化 Bean:
执行@PostConstruct 注解方法。
检查 Bean 是否实现了 InitializingBean 接口,如果实现,则调用 afterPropertiesSet() 方法。
**BeanPostProcessor 的后置处理:**调用 BeanPostProcessor 接口的 postProcessAfterInitialization() 方法。此时 Bean实例已经完整创建完毕,可以被容器使用了。这也是 AOP 动态代理发生的关键节点(通过后置处理器返回代理对象)。
**Bean 就绪:**Bean驻留在容器中,容器类型是Map<BeanName,Bean对象>,所以spring的单例模式并不是严格意义上的单例,一个Bean可以有多个不同的beanName。
**销毁阶段:**在该阶段,Spring 容器将调用 Bean 的销毁方法destory-method,可以通过 @PreDestroy 注解来标记销毁方法。这个阶段是在 Spring 容器关闭时执行的。
Bean生命周期详细流程图:
Bean生命周期和bean的代理对象结构简图如下:
IOC原理 概念: IOC(控制反转):
控制:把创建对象的控制权交给了spring容器,实现减耦合,功能复用,减少new对象对内存的损耗,通过反射的原理创建实例。
反转:依赖注入,不是对象去new新的对象给自己赋值,而是spring容器帮bean对象进行属性赋值。
IOC的加载流程图:
定义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,执行方法的时候,在不修改源码的基础上对方法进行增强。
先定义切面(用来生成动态代理对象),再定义切点(具体方法或者注解),然后定义增强的类型及逻辑,分为前置,后置,环绕,异常通知,最终通知。
实现动态代理的方法:
JDK动态代理:利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用invoke来处理。
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 { @Pointcut("@annotation(com.lh.plat.service.common.Log)") public void logPointCut () {} @AfterReturning(pointcut = "logPointCut()",returning = "val") public void doAfter (JoinPoint joinPoint, Object val) { String methodName = joinPoint.getSignature().getName(); } } @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Log { String module () default "" ; String op () default "" ; short logType () default 0 ; }
核心类解释 DefaultSingletonBeanRegistry(存放单例bean)
Map<String, Object> singletonObjects相当于bean容器,集中存放单例bean
全权管控所有单例生命周期如创建、缓存、获取、销毁。维护三级缓存,解决循环依赖;
我们日常用的 BeanFactory/ApplicationContext 最终都复用这套单例管理逻辑;
使用场景:
手动注册第三方组件到 Spring 容器
动态注册运行时生成的实例
如果手动调用,传入的对象不会经过 Spring 完整 Bean 生命周期:不会执行属性注入 、@PostConstruct、InitializingBean、afterPropertiesSet 等初始化流程;适合已经初始化完毕的成品对象注入;
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private final Map <String, Object> singletonObjects = new ConcurrentHashMap <>();private final Map <String, Object> earlySingletonObjects = new HashMap <>();private final Map <String, ObjectFactory<?>> singletonFactories = new HashMap <>();private final Set <String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap <>());public void registerSingleton (String beanName, Object singletonObject) throws IllegalStateException
使用示例:
1 2 3 4 5 6 7 8 9 10 11 DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry) applicationContext.getAutowireCapableBeanFactory();UserService userService = new UserService ();registry.registerSingleton("userService" , userService); UserService bean = applicationContext.getBean("userService" , UserService.class);System.out.println(bean == userService);
BeanDefinition(定义bean属性)
BeanDefinition 注册位置:DefaultListableBeanFactory内部的 Map<String, BeanDefinition> beanDefinitionMap
Spring 容器启动扫描阶段就全部解析生成完毕
内容:记录 Bean 的类路径、作用域、构造参数、属性值、初始化销毁方法等所有配置信息
作用:告诉 IOC 工厂:这个 Bean 该怎么创建
实现类:
GenericBeanDefinition:默认主流实现 ,绝大部分 @Component、@Bean 扫描出来的 Bean 定义都是它
AnnotatedGenericBeanDefinition:继承GenericBeanDefinition,额外携带注解元数据信息
ScannedGenericBeanDefinition:包扫描 @Component 得到的 BeanDefinition
AbstractAutowireCapableBeanFactory(核心逻辑)
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)
能力:
实现BeanDefinitionRegistry,负责管理BeanDefinition ,内部beanDefinitionMap属性存放初始化创建的BeanDefinition
属于BeanFactory子类,调用 getBean() 会根据 BeanDefinition 去获取/创建bean对象
继承了DefaultSingletonBeanRegistry,存放已创建的bean对象
工厂创建bean流程:
容器启动,ComponentScan解析器启动 ClassPathBeanDefinitionScanner → 扫描@Component/@Bean → 解析全部配置 ,生成一堆BeanDefinition,存入beanDefinitionMap
容器循环创建单例 Bean,首次需要获取Bean(getBean(“userService”)), 先去 DefaultSingletonBeanRegistry 的一级缓存查找,没有就依据BeanDefinition反射调用构造方法实例化对象、填充属性、执行初始化方法。
Bean完全创建完毕后,调用 DefaultSingletonBeanRegistry.registerSingleton() 将成品Bean放入一级缓存 singletonObjects 永久缓存
后续再getBean,直接从注册表缓存拿,不再走创建流程
BeanFactoryPostProcessor(BeanDefinition处理器) 容器启动早期执行,修改 Bean 定义元数据
实现类:
ConfigurationClassPostProcessor :最重要内置后置处理器,解析@Configuration、@Bean、@ComponentScan、@Import,扫描所有组件,生成 BeanDefinition,Spring 注解驱动的核心
PropertySourcesPlaceholderConfigurer :解析 ${} 占位符,读取 properties/yaml 配置文件填充属性
使用代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 @Component public class AConfig implements BeanFactoryPostProcessor { @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生命周期的两大阶段:实例化阶段和初始化阶段 按调用顺序为:
postProcessBeforeInstantiation:实例化bean之前,相当于new这个bean之前
postProcessAfterInstantiation:实例化bean之后,相当于new这个bean之后
postProcessPropertyValues:bean已经实例化完成,在属性注入时阶段触发,@Autowired,@Resource等注解原理基于此方法实现
postProcessBeforeInitialization:初始化bean之前,相当于把bean注入spring上下文之前
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 @Component public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization (Object bean, String beanName) throws BeansException { 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场景:
需要依赖注入完成之后做初始化操作(加载配置、初始化连接池、开启线程、缓存预热)
校验必填参数,属性为空直接抛出异常阻止 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:
使用@Mapper标注在接口上,MyBatis 底层用 JDK 动态代理生成代理对象。
核心MapperFactoryBean实现FactoryBean,其中getObject()方法专门来获取代理对象。
Spring 加载 Mapper 的整条链路:
@MapperScan → MapperScannerConfigurer,扫描指定包下所有 Mapper 接口
给每一个接口生成一个 BeanDefinition,其中beanClass = MapperFactoryBean,MapperFactoryBean里的mapperInterface = 当前 Mapper 接口
MapperFactoryBean底层会维护一个Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();,key存mapper的类,value存代理对象工厂,专门利用jdk反射创建代理对象。
IOC 根据 BeanDefinition 创建工厂对象MapperFactoryBean,MapperFactoryBean再注入 SqlSessionTemplate
当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进行事务操作的方式:
声明式事务管理
基于xml配置文件方式
基于注解方式
spring事务隔离级别:
DEFAULT 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别,Mysql 默认:可重复读。Oracle 默认:读已提交。
未提交读(read uncommited) :脏读,不可重复读,幻读都有可能发生
已提交读 (read commited):避免脏读。但是不可重复读和幻读有可能发生
可重复读 (repeatable read) :避免脏读和不可重复读.但是幻读有可能发生.
串行化的 (serializable) :避免以上所有读问题.
Spring事务实现具体流程:
如果方法标注了@Trasactional的执行方法,开启spring事务。
事务管理器先新建一个数据库连接connect,事务管理器会在thread local<map<datasource,connect>>注册datasource。
然后把autocommit改为false,方便回滚
之后target普通对象执行原生方法,如果出现异常进行回滚
事务的流程图如下:
事务失效原因:
方法没加@Trasactional,所以代理对象不会对该方法进行增强。
同一个类,一个没有事务的方法执行同类另一个有事务方法事务失效,因为自调用的方法并不是代理对象执行,而是该类的bean对象执行,除非第二个事务方法自己注入自己的依赖,然后用类似与this.userservice.test()方法调用。(@Async也是)
方法没加public
class没有被spring管理
异常被捕获到处理了
数据库不支持事务
Spring中的设计模式
单例模式 :每次读取的对象都是同一个,如spring-ioc原理,数据库连接池等,多次获取只获取同一个对象。
工厂模式 :专门造对象的 “工厂”,不用自己 new 对象,例如:
Spring IOC容器,BeanFactory是顶层工厂,来创建BeanDefinition,负责创建和获取 Bean;
ApplicationContext 是功能更完善的工厂,FactoryBean 专门创建复杂对象。
**代理模式:**通过反射生成代理对象,代理对象对原有方法进行增强,真正执行时候是代理对象执行的,如spring的aop和事务。
**观察者模式:**发布事件,监听事件,广播事件,如spring的ApplicationEvent。
责任链模式 :**按链式顺序执行处理器,灵活添加移除处理器,可扩展,每个处理器单一职责。**如spring-ai的ChatClient的defaultAdvisors,添加各种处理器链式执行,如日志打印,自动注入历史会话,检索向量数据库相关知识等。
适配器模式 :**把不兼容的接口,转成兼容的,让原本不能一起工作的类能一起工作。**如springmvc的HandlerAdapter,每个适配器都实现HandlerAdapter并且调用适配器的handle (),各种请求对应的适配器如下:
@RequestMapping和@Controller->RequestMappingHandlerAdapter
HttpRequestHandler->HttpRequestHandlerAdapter
Servlet->SimpleServletHandlerAdapter
Spring事务->TransactionManagerAdapter
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。
自动装配逻辑大致如下:
加载,从配置文件META-INF/spring.factories加载所有可能用到的自动配置类;
去重,并将exclude和excludeName属性携带的排除;
过滤,将满足条件(@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 @Autowired @Qualifier(value = "springComponent") private SpringComponent getGetGetSpringComponent;private SpringComponent springComponent;@Autowired public void setSpringComponent (SpringComponent springComponent) {this .springComponent = springComponent;} @Autowired private SpringComponent component;private SpringComponent getSpringComponent;@Autowired public SpringTest (SpringComponent springComponent) {this .getSpringComponent = springComponent;}
使用@Resource方式:
1 2 3 4 5 6 7 8 9 10 11 @Resource(name = "springComponent") private SpringComponent getComponent;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 (); } @Bean(autowire = Autowire.BY_TYPE) public WyfBean2 wyfBean2 () { return new WyfBean2 (); } } public class Test { public static void main (String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext (AppConfig.class); 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 () { 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 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=-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 logging: file: 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配置文件
在 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" > <bean id ="helloService" class ="org.hpit.demo.service.HelloService" > </bean > </beans >
在启动类App.java 中配置引入配置文件的注解@ImportResource
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SpringBootApplication @ServletComponentScan @ComponentScan(basePackages = {"com.hpit"}) @ImportResource(locations = {"applicationContext.xml"}) public class App {public static void main (String[] args) throws Exception {SpringApplication.run(App.class, args); } public MultipartConfigElement multipartConfigFactory () { MultipartConfigFactory configFactory = new MultipartConfigFactory (); configFactory.setMaxFileSize("128MB" ); 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 { @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 @Configuration public class MyEnvironment implements EnvironmentAware {@Value("${spring.datasource.url}") private String jdbcUrl;private Logger logger = Logger.getLogger(getClass());@Override public void setEnvironment (Environment environment) { logger.info("springel表达式获取的值:" + jdbcUrl); logger.info("JAVA_HOME" + environment.getProperty("JAVA_HOME" )); logger.info("spring.datasource.url:" + environment.getProperty("spring.datasource.url" )); 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 @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 缓存
导入redis依赖
1 2 3 4 5 <!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
编写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 @Configuration @EnableCaching publicclass RedisCacheConfig extends CachingConfigurerSupport { @Bean public CacheManager cacheManager (RedisTemplate<?,?> redisTemplate) { CacheManager cacheManager = new RedisCacheManager (redisTemplate); returncacheManager; } @Bean public RedisTemplate <String, String> redisTemplate (RedisConnectionFactory factory) { RedisTemplate <String, String> redisTemplate = new RedisTemplate <String, String>(); redisTemplate.setConnectionFactory(factory); RedisSerializer <String> redisSerializer = new StringRedisSerializer (); redisTemplate.setKeySerializer(redisSerializer); redisTemplate.setHashKeySerializer(redisSerializer); returnredisTemplate; } @Override public KeyGenerator keyGenerator () { System.out.println("RedisCacheConfig.keyGenerator()" ); returnnew KeyGenerator () { @Override public Object generate (Object o, Method method, Object... objects) { 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) 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; } }
使用注解
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 @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}" ; 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") @PreDestroy @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是单例原因:
类上加@Configration注解是保证是代理对象执行每个方法
执行每个方法后,如果加了@Bean,会去map容器查有没有该bean,没有会创建bean加入容器。
例如一个加了@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 @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执行流程: 简述流程:
DispatcherServlet(前端控制器)收到请求,会调用Handlermapping根据对应的url找到对应的Handler和处理器拦截器(如果有则生成),一起返回给DispatcherServlet。统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求
DispatcherServlet会根据Handler找到对应的HandlerAdaptor,如果有拦截器此时执行拦截器的方法,然后去执行Handler的业务代码(Handler相当于一个继承HttpServlet的类,里面包含了controller的业务逻辑),处理完业务逻辑利用HttpMessageConveter把响应消息转换成json之类的格式,如果配置了@ResponseBody直接返回结果,没有就放进ModleAndView返回给DispatcherServlet。
DispatcherServlet拿到ModleAndView会调用ViewResolver视图解析器进行解析,返回具体View给DispatcherServlet。
DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中),响应用户。
详细流程:
启动tomcat,创建DispatcherServlet,初始化各种处理器。
初始化HandlerMapping,会找三个handlermapping,第一个是beannameURLhandlermapping,第二个是RequestMappingHandlerMapping,第三个是RouterFunction。
收到请求,经过创建RequestMappingHandlerMapping的bean对象,经过afterPropertiesSet方法,期间是先生成一个map,k是path,v是requestmappingInfo的list,通过这个遍历这个list判断请求方法,匹配对应的RequestMappingInfo,最终生成map,k是path,v是对应的method
MethodArgumentResolver方法参数解析器,该解析器在创建bean之后,进行初始化afterPropertiesSet会把很多注解解析器加载到resolver的list里面,随后解析参数的注解如RequestParam等等,解析url请求参数给方法的参数赋值。
初始化HandlerAdaptor,根据handlermapping来找到对应的HandlerAdaptor
MethodReturnValueHandler返回值处理器,会判断有没有加ResponseBody,选择合适的handler去处理返回值。
HttpMessageConvert返回值转换器,默认有四种caonvert如下图,返回值是数组,字符串,都要相应的convert处理,如果返回值是对象,原生spring不支持,要自己配置jackson,但是springboot默认把使用Jackson2HttpMessageConvert加进来了,该jackson解析器会自动解析所有数据类型成json返回前端。
原生Servlet处理请求流程: **常用类:**HttpServletRequest ,HttpServletResponse ,HttpSession,RequestEntity
HttpServlet处理请求流程:
编写HttpServlet的实现类,重写doGet和doPost方法写处理逻辑,绑定对应的url然后注册
web服务器解析请求的url找到对应的HttpServlet类,先调用HttpServlet的service(ServletRequest req, ServletResponse res)方法,如果重写这个方法,优先级会高于doGet和doPost方法。
再调用本类的service(HttpServletRequest req, HttpServletResponse resp)方法,判断方法是get还是post还是Put等等
如果是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 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); } } @Configration public class HelloApplication { @Bean public ServletRegistrationBean MyServlet () { return new ServletRegistrationBean (new Test2Servlet (),"/test/hello" ); } } <?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
拦截器执行流程:
定义MyInterceptor 实现HandlerInterceptor接口
重写preHandle,postHandle,afterCompletion三个方法,
preHandle用于Controller方法处理之前
postHandle在preHandle返回true并且Controlller完成后,可以对ModelAndView进行操作
afterCompletion在preHandle返回true,DispatcherServlet进行视图的渲染之后,多用于清理资源
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) { registry.addInterceptor(new MyWebInterceptor ()).addPathPatterns("/**" ); } }
SpringMVC内置处理器 CharacterEncodingFilter:
字符编码过滤器
1.解决获取请求参数的乱码问题,可以使用SpringMVC提供的编码过滤器CharacterEncodingFilter 2.SpringMVC中处理编码的过滤器一定要配置到其他过滤器之前,否则无效
HttpMessageConverter:
报文信息转换器
将请求报文转换为Java对象,或将Java对象转换为响应报文。 @ResponseBody处理json的步骤:
导入jackson的依赖
在SpringMVC的核心配置文件中开启mvc的注解驱动,此时在HandlerAdaptor中会自动装配一个消息转换器:MappingJackson2HttpMessageConverter,可以将响应到浏览器的Java对象转换为Json格式的字符串。
在处理器方法上使用@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 @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 ); } } @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 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()); } }
重定向 重定向原理:
当控制器方法返回以 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 @PostMapping("/login") public String login () { return "redirect:http://auth.gulimall.com/login.html" ; } @PostMapping("/redirectWithHeadersAndCookie") public RedirectView redirectWithHeadersAndCookie (HttpServletResponse response) { RedirectView redirectView = new RedirectView ("http://localhost:8837/loginSuccess.html" ); redirectView.setStatusCode(HttpStatus.FOUND); redirectView.addStaticAttribute("X-Custom-Header" , "custom-value" ); Cookie customCookie = new Cookie ("user_token" , "abc123" ); customCookie.setPath("/" ); customCookie.setMaxAge(3600 ); customCookie.setHttpOnly(true ); response.addCookie(customCookie); return redirectView; } axios.post('/submit' , data) .then(response => { }).catch (error => { if (error.response && error.response.status === 302 ) { 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 Spring session: store-type: redis timeout: 30m @EnableRedisHttpSession @Configuration public class GulimallSessionConfig { @Bean public CookieSerializer cookieSerializer () { DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer (); defaultCookieSerializer.setDomainName("gulimall.com" ); defaultCookieSerializer.setCookieName("GULISESSION" ); return defaultCookieSerializer; } @Bean public RedisSerializer <Object> springSessionDefaultSerializer () { return new GenericJackson2JsonRedisSerializer (); } }
SpringSecurity 用户认证流程图如下:
开启步骤: 如下示例开启步骤基于前后端分离的场景
项目在云盘里的spring_security_springboot.zip
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) public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowired private TokenUtils tokenUtils; @Autowired private RedisTemplate redisTemplate; @Autowired private DefaultPasswordEncoder defaultPasswordEncoder; @Autowired private UserDetailsService userDetailsService; @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(); } @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" )); } @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 (); } } @Override protected void successfulAuthentication (HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityUser user = (SecurityUser)authResult.getPrincipal(); String token = tokenUtils.createToken(user.getCurrentUserInfo().getUsername()); redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList()); 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已经认证成功来到这。
从请求头拿token,解析出username去redis拿到权限列表。
把username,token,权限列表封装成UsernamePasswordAuthenticationToken,放进SecurityContextHolder的上下文。
完成之后可以从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) { String token = request.getHeader("token" ); if (token != null ){ String username = tokenUtils.getUserInfoFromToken(token); 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 ); } @Override public String encode (CharSequence charSequence) { return MD5.encrypt(charSequence.toString()); } @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) { String token = request.getHeader("token" ); if (token != null ){ tokenUtils.removeToken(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); } private long tokenEcpiration = 24 *60 *60 *1000 ; private String tokenSignKey = "123456" ; 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; } public String getUserInfoFromToken (String token) { String userinfo = Jwts.parser() .setSigningKey(tokenSignKey) .parseClaimsJws(token) .getBody() .getSubject(); return userinfo; } public void removeToken (String token) { } }
自动登录 步骤:
在PersistentTokenRepository初始化token持久化的数据源
HttpSecurity配置自动登录,设置token过期时间
按钮自动登录的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 (); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(this .passwordEncoder()); } @Bean public PersistentTokenRepository persistentTokenRepository () { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl (); jdbcTokenRepository.setDataSource(dataSource); return jdbcTokenRepository; } @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html" ) .loginProcessingUrl("/user/login" ) .defaultSuccessUrl("/success.html" ).permitAll() .failureForwardUrl("/fail" ) .and() .authorizeRequests() .anyRequest().authenticated() .and().rememberMe().tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(60 ) .userDetailsService(userDetailsService) .and() .logout() .logoutUrl("/logout" ).logoutSuccessUrl("/logout/success" ).permitAll() ; } }
流程:
用户先登录,点击多少天自动登录,认证成功生产token,去jdbc往数据库插入,浏览器里面会存储token并且带过期时间。
用户再次访问带着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历史版本对应如下: