一.框架是什么?
一堆jar包和配置文件,对其他工具进行包装集成更好用
库补充:日志和lombok
日志:
种类:
1.统一的日志接口SLF4J,实现接口的日志有Log4j、Logback
2.java自带日志框架JUL
级别:
- TRACE: 细粒度的信息,通常用于调试。
- DEBUG: 调试信息,帮助开发者理解程序运行状态。
- INFO: 重要的运行信息,表明程序的正常运行状态。
- WARN: 警告信息,表明可能的问题。
- ERROR: 错误信息,指示程序中发生了异常。
- FATAL: 严重错误,程序可能无法继续运行。
其中Logback是以后springboot默认的,学下这个。
1.导包 logback-classic (其他包是不全的)
2.配置文件 logback.xml
配置在控制台输出格式,配置文件输出格式,配置默认级别以及输出到哪里
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>100MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!--mybatis log configure-->
<logger name="com.apache.ibatis" level="TRACE"/>
<logger name="java.sql.Connection" level="DEBUG"/>
<logger name="java.sql.Statement" level="DEBUG"/>
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
<!-- 日志输出级别,logback日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
3.在 Java 代码中使用 Logback 记录日志
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyClass {
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
public void myMethod() {
logger.debug("This is a debug message");
logger.info("This is an info message");
logger.warn("This is a warning message");
logger.error("This is an error message");
}
}
lombok:
- @Slf4j
使用 SLF4J 作为日志记录框架。它会自动生成一个名为log
的日志记录器@Slf4j public class MyClass { public void myMethod() { log.debug("This is a debug message"); log.info("This is an info message"); log.warn("This is a warning message"); log.error("This is an error message"); } }
- @Data
包含@ToString
、@EqualsAndHashCode
、@Getter
、@Setter
和@RequiredArgsConstructor
- @NoArgsConstructor / @AllArgsConstructor
生成无参构造函数和全参构造函数
二.Mybatis框架
知识思维导图:ProcessOn Mindmap
1.其中CRUD->使用mybatis进行增删改查案例
<mapper namespace="com.yang.Mapper.studentMapper">
<select id="getAllStudent" resultType="com.yang.pojo.Student">
select * from student
</select>
<select id="getStudentById" resultType="com.yang.pojo.Student">
select * from student where id=#{id}
</select>
<insert id="addStudent">
insert into student (id,name,age) values (#{id},#{name},#{age})
</insert>
<update id="updateStudent">
update student set name=#{name},age=#{age} where id=#{id}
</update>
<delete id="deleteStudent" >
delete from student where id=#{id}
</delete>
</mapper>
SQL映射文件中的配置比较完整的写法是:
<select id="selectByName" resultType="student" parameterType="java.lang.String">
select * from t_student where name = #{name, javaType=String, jdbcType=VARCHAR}
</select>
其中sql语句中的javaType,jdbcType,以及select标签中的parameterType属性,都是用来帮助mybatis进行类型确定的。不过这些配置多数是可以省略的。因为mybatis它有强大的自动类型推断机制。
- javaType:可以省略
- jdbcType:可以省略
- parameterType:可以省略
2.其余没仔细写的可以看MyBatis · 语雀(动力节点的很不错,超详细)
原理
Mybatis获取连接数据库是对jdbc进行了封装
SqlSessionFactoryBuilder
1.会读核心配置文件xml得到当前环境:拿到xml文件中对应的事务管理工厂和数据源
(id TransactionFactory DataSource)
2.解析Mapper文件,获取所有的SQL映射对象Map
3.执行build方法创建SqlSessionFactory
(解读DefaultSqlSessionFactory类-openSessionFromDataSource)
1.得到SqlSessionFactoryBuilder解析完后的环境 (id TransactionFactory DataSource)
2.从环境中拿到TransactionFactory,传入 DataSource,得到一个事务管理器Transaction
3.得到执行器,包装事务管理器Transaction
4.执行openSeesion方法得到sqlSession
(解读DefaultSqlSessionFactory类-openSession)
1.openSession就是执行上述步骤得到sqlSession包装了执行器
并添加对应执行方法 执行语句从sql映射对象map中拿
2.sqlSession执行数据库操作时,会用事务管理器控制事务,从数据源得到一个数据库连接,然后用这个连接执行sql语句,异常时事务管理器会回滚事务,成功后提交事务,最后将连接归还给数据源。
总结:sqlSession->执行器->事务管理器->数据源
执行语句从sql映射对象map中拿
如果抽象成对象:
SqlSessionFactoryBuilder读配置文件得到DataSource和事务管理器-> SqlSessionFactory -> SqlSession
三.spring框架
1.spring分为八大模块
我们要学的:
Spring Core --> 实现了ioc管理bean,容器为BeanFactory
Spring AOP -->面向切面编程
Spring Context--> 构建于core封装包基础上的context封装包,提供了框架式的对象访问方法(如:容器ApplicationContext),集成了很多包(如:SpEL-->spring-expression)
Spring DAO-->简化JDBC,处理数据库相关,如处理事务和数据库异常
Spring ORM --> spring支持与ORM框架集成
Spring Web --> web技术 包装了servlet websocket
Spring MVC --> 全名 spring web MVC,包装了spring web,以MVC架构实现web项目
Spring Webflux --> ?
使用spring只需要导入相关依赖就可以了,spring一共就有20多个包
1.普通项目基础导包(ioc aop SpEL) --> spring-context
web项目基础导包(ioc aop SpEL web) --> spring-webmvc
(二选一)
2.在以上基础上使用其他模块内容
用Spring+AspectJ框架实现的aop,要导入spring-aspects
用Spring管理Mybatis要导入Mybatis写好的包,mybatis-spring
以及Sring提供整合的工具spring-jdbc(包含事务管理)
用Spring集合Junit框架,要导入spring-test
ps:spring-core是所有依赖的依赖,它依赖了commons-logging,所以spring在打开的时候会有日志输出。我们可以导入logback依赖,配置xml,显示的就是此日志输出了。
spring核心框架体系结构(各个jar包作用)_spring-instrument-tomcat-优快云博客
2.spring-context(ioc)
实现过程
设计模式:ioc 控制反转思想
完成Spring对Bean的管理(Bean对象的创建以及Bean对象中属性的赋值)
DI(依赖注入) :ioc思想通过DI实现。
DI常见的实现方式包括两种:set注入 构造注入
实现控制反转,三个技术:解析配置文件 工厂模式 反射
1.解析xml配置文件或扫描注解
Spring会从上到下读xml文件,解析出每个bean的定义
如果发现某个bean的ref是另一个bean,会先将此bean解析
再接着解析其他bean
解析完创建BeanDefinition 是按解析顺序创建的
2.容器初始化【实例化bean】
容器有什么?
- BeanFactory:基础容器,用于管理简单的 Bean。
- ApplicationContext:功能更全的容器,提供国际化、事件传播、AOP 等功能。
容器有数据结构 Map<String,Object>中,key就是对象的id或name,value就是对象
容器有getBean方法 -- 工厂设计模式(通过容器工厂得到我们想要的类)
遍历BeanDefinition,创建bean实例,区分是单例还是原型模式
(这里还可以指定加载顺序,会先排队交换位置再加载 属性:depends-on )
[单例且懒加载为false]在容器启动时就会实例化 Bean,并将其存放在 Map
中。
[单例且懒加载为true]容器只在第一次调用 getBean
时创建 Bean,并将其存储以供后续调用使用。
[原型]每次调用 getBean
方法都会创建一个新的 Bean 实例,不会在容器中缓存。
[ps:在实例化的时候
1.没配工厂:会看有没有指定构造方法注入,没有的话会得到无参构造方法实例化对象
2.如果配置了factory:在实例化时,会去找工厂然后执行工厂方法,得到实例化对象]
总结:实例化bean用到的属性有(id name class [scope lazy-init depends-on factory-method factory-bean] )
3.依赖注入【属性赋值】:
- 构造注入(已经在实例化的时候注入了)
- set注入
遍历map中的bean
将property中的内容进行赋值
1.注入简单类型 vlaue (基本数据类型及包装类 String URI URL....)
[可用BeanUtils.isSimpleValueType()判断]
2.注入Bean ref
3.注入数组 array-ref/value
4.注入List list-ref/value
5.注入Set set-ref/value
6.注入Map map-<entry key/key-ref="1" value/value-ref="北京大兴区"/>
执行set方法(set方法是name第一个字母大写后拼接得到的)
也可以自动装配(autowire),也是基于set方法的。
分为byName何byType(前置一定要规范!属性名为aaa,那set方法应为setAaa)
byName-->根据属性名去配置文件找对应name或id的bean类,执行对应set方法
byType-->根据属性的类型,去配置文件中找对应类型的bean,执行对应set方法
如果有歧义,可以在加上autowire-candidate是否为候选人或者设置primary优先级
总结:属性赋值常用到的属性有(autowire autowire-candidate primary)
4.初始化bean:
之后调用初始化bean的方法 init-method
以上内容都是在new容器时就会进行的。所有的bean对象(作用域是单例的)会在new时就全部创建好放到容器中了,之后拿到的都是这个对象。
5.使用bean:
getBean会判断容器中有没有,有的话就直接返回,没有的话看是懒加载了还是原型。
如果是懒加载,创建对象-->依赖注入-->调用 init-method方法-->放入容器,下次拿直接拿容器对象
如果是原型,创建对象-->依赖注入-->调用 init-method方法-->new新对象返回,下次拿也是这几步
6.销毁bean:
在容器中的bean都会销毁,调用销毁方法destroy-method
(原型不交给容器管,所以没这步)
以上步骤就是按bean生命周期来讲解的。
由容器管理的单例bean,生命周期完整。
原型bean不由容器管理,生命周期只到使用bean。
使用方式
1.配置xml文件方式
将[配置名].xml放到resoures底下,spring也是从target下读文件的
配置文件常用内容:
bean中可以写:(beans上有些也可以配,配了全部bean都有此特性)
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd" default-lazy-init="true">
<!--导入其他配置文件-->
<import resource="other.xml"/>
<!--注册bean对象-->
<bean name="Car" class="com.yang.pojo.Car"/>
<!--给此对象起别名-->
<alias name="Car" alias="smallCar"/>
<!--设置此对象的获得方式 1.singleton(默认) 此对象每次获取都是同一个对象
2.prototype 每此获取会new一个新对象-->
<bean class="com.yang.pojo.Customer" scope="prototype"/>
<!--工厂的方法得到对象,其中class是工厂类,得到的是car对象,yeah也是car对象的名字
没有实例化factory对象,所以method必须是static的-->
<bean name="yeah" class="com.yang.pojo.Factory" factory-method="getStaticCar"/>
<!--也可以实例化factory类 这里的method必须是非static的
factory还是实例化对象的名字-->
<bean name="factory" class="com.yang.pojo.Factory"/>
<bean factory-bean="factory" factory-method="getCar"/>
<!--注入-->
<bean name="小张" class="com.yang.pojo.Customer">
<constructor-arg index="0" value="小张"/>
<property name="id" value="2"/>
<!-- <constructor-arg name="id" value="5"/>-->
</bean>
</beans>
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
2.注解方式
1.解析配置文件,可以用java类替代配置文件@Configuration
注意配置扫包,扫哪些包上的注解@ComponentScan
在这里面也可以注册bean,用@Bean注解,这样可以弥补接下来的注解没有工厂模式的缺陷。
在这里获得的对象也是我们通过方法创建出来的可以加其他处理。
(@Bean里面可以配置名字,默认名是方法名,配置自动装配,初始化方法和摧毁方法)
ps:对于编写的类型,如果要注册为Bean,那么只能在配置类中完成
@Configuration
@ComponentScan({"com.yang.pojo","com.yang.dao"})
public class Config {
@Bean(name="customer")
public Customer customer() {
//工厂模式 可以加其他修饰
Customer customer = new Customer();
return customer;
}
//也可以自动装配,将装配对选以参数形式传来,会自动找注册过的bean
@Bean(initMethod = "function")
public Customer customer2(Car car) {
Customer customer = new Customer();
customer.setCar(car);
return new Customer();
}
}
ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
2.对class上面加注解
声明bean的注解 :@Component @Controller @Service @Repository
这四个作用一样分包更清晰
@Component会看@Autowired是否加构造方法上了,有执行;没有执行无参构造方法,如果没有无参会执行有参的,前提是所有参数的类型都是可以在上下文中找到的bean。
并且会给bean起默认名字:Bean类名首字母小写
也可以自己起:@Component("myCar")
其他:
- scope-->@Scope("prototype")
- lazy-init-->@Lazy(true)
- depends-on-->@DependsOn("customer")
- factory-method-->用配置在配置类中替代了
3.对属性加注解
@Value 简单类型
自动装配:
- @Autowired注解默认根据类型装配byType,如果想根据名称装配,需要配合@Qualifier注解一起用。@Autowired并不是只能用于字段,对于构造方法(指定构造方法)或是Setter都可以。(如果自动装配产生冲突,优先@Component的)
- @Resource注解默认根据名称装配byName,未指定name时,使用属性名作为name。通过name找不到的话会自动启动通过类型byType装配。只能用字段和Setter。
4.初始化bean
给方法上添加@PostConstruct ,说明这是一个初始化方法
3.spring-aspects(aop)
面向切面
切面就是与业务逻辑无关的代码单独提取出来形成一个独立切面(切点+通知)
1.确定切点(方法)
2.确定通知(增强方法)
3.确定通知位置(方法前后还是环绕)
Spring对AOP的实现包括以下3种方式:
- 第一种方式:Spring框架结合AspectJ框架实现的AOP,基于注解方式。
- 第二种方式:Spring框架结合AspectJ框架实现的AOP,基于XML方式。
- 第三种方式:Spring框架自己实现的AOP,基于XML配置方式。
注解方式:【记住这种】
(先再配置类上添加aop注解可用@EnableAspectJAutoProxy)
1.切面类@Aspect@Component
2.编写方法
3.指定其切入位置
- 前置通知:@Before 目标方法执行之前的通知
- 后置通知:@AfterReturning 目标方法执行之后的通知
- 环绕通知:@Around 目标方法之前添加通知,同时目标方法执行之后添加通知,需显示调用目标方法
- 异常通知:@AfterThrowing 发生异常之后执行的通知
- 最终通知:@After 放在finally语句块中的通知
可以用JoinPoint得到切点的具体信息,其中环绕通知还可以用ProceedingJoinPoint来获得切点具体信息。比如:获得目标方法的第一个参数joinPoint.getArgs()[0].toString();
我们还可以通过args(参数名)
进行参数绑定,直接获取参数(例子在下面)
环绕信息必须调用目标方法表示继续,如果有参数,也可以将新的参数传入目标方法。
4.里面填execution表达式指定位置
填写格式:修饰符 包名.类名.方法名称(方法参数)
如:@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
方法参数中填..代表参数什么类型都可以
也可以将这个切点放到一个方法上,大家都可以用,在任一类中写这个方法都可以,统一切点
@Pointcut("execution(* com.yang.pojo.*(..))")
public void pointcut() {}
(用aop的类,再getBean时,返回的是一个代理类,不是单纯的目标类
可以用getClass查看其类型,如:com.yang.pojo.Customer$$SpringCGLIB$$0)
举例:
@Aspect
@Component
public class MainAspects {
@Before(value = "execution(* com.yang.pojo.Customer.function(String))")
public void before(JoinPoint joinPoint) {
System.out.println(joinPoint.getArgs()[0]);
System.out.println("我是之前执行的内容!");
}
}
@Aspect
@Component
public class MainAspects {
@Before(value = "execution(* com.yang.pojo.Customer.function(String)) && args(str)",argNames ="str" )
public void before(String str) {
//命名绑定模式就是根据下面的方法参数列表进行匹配
//这里args指明参数,注意需要跟原方法保持一致,然后在argNames中指明
System.out.println(str);
System.out.println("我是之前执行的内容!");
}
}
@Aspect
@Component
public class MainAspects {
@Around(value = "execution(* com.yang.pojo.Customer.function(..))")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println(joinPoint.getArgs()[0].toString());
System.out.println("我是之前执行的内容!");
//必须手动调用方法
//Object proceed = joinPoint.proceed();
//还可以传入新的参数列表给目标方法
Object proceed = joinPoint.proceed(new String[]{
"1234"});
System.out.println("我是之后执行的内容!");
}
}
4.spring-jdbc 和 mybatis-spring(整合mybatis)
回顾:mybatis
SqlSessionFactoryBuilder读配置文件得到DataSource和事务管理器-> SqlSessionFactory -> SqlSession
现在这些对象全用sring来管理,在mybatis-spring依赖中,为我们提供了SqlSessionFactoryBean和SqlSessionTemplate可以注册相当于SqlSessionFactory和SqlSession
Spring:
DataSource和TransManager-->SqlSessionFactoryBean
(这里可以选择不配置 SqlSessionTemplate,因为它可以在需要时被自动创建)
SqlSessionFactoryBean在创建时除了可以传入数据源,还可以传入Mapper.xml的资源以及mybatis核心文件资源。(mybatis核心文件的enviroment等会被忽略)
之后写Mapper接口实现类用sqlSessionTemplate.getMapper拿到对应Mapper接口动态生成的对象,就可以用实现类自动装配了。
动态生成:
- mybatis通过Mapper接口的类名为namesapce,方法名为id找到对应方法然后实现方法。
- 如果是注解,直接拿到上面方法然后实现方法。
也可以直接用注解@MapperScan("com.yang.dao"),扫描包下所有接口,生成实现类放入Spring,直接getBean获取,Mapper接口直接可以用来自动装配。(本质仍是自动创建SqlSessionTemplate然后调用getMapper)
SqlSessionTemplate介绍
它其实就是官方封装的一个工具类,我们可以将其注册为Bean,这样我们随时都可以向IoC容器索要对象,而不用自己再去编写一个工具类了,我们可以直接在配置类中创建。其实现了SqlSession接口,且线程安全。使用了SqlSessionTemplate之后,我们不再需要通过SqlSessionFactory.openSession()方法来创建SqlSession实例;使用完成之后,也不要调用SqlSession.close()方法进行关闭。另外,对于事务,SqlSessionTemplate 将会保证使用的 SqlSession 是和当前 Spring 的事务相关的。
对于这种别人编写的类型,如果要注册为Bean,那么只能在配置类中完成。2.不保留mybatis核心配置文件
配置方式
我们只需配数据源bean就可以正常使用了,事务管理器bean会默认给一个,但不一定与数据源匹配,所以要是想用@Transcational还是要显示配置一个事务管理器。
1.编写数据源DataSource,此类是一个java提供的接口,常用实现此接口的数据源为HikariDataSource。之后由数据源得到SqlSessionFactoryBean
先导包<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.0.1</version> </dependency>
之后编写DataSource和SqlSessionFactoryBean
@Configuration @ComponentScan({"com.yang"}) @EnableTransactionManagement //@MapperScan("com.yang.dao") public class Config { @Bean public DataSource dataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mybatis"); dataSource.setUsername("root"); dataSource.setPassword("123456"); return dataSource; } @Bean public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); //bean.addMapperLocations(new ClassPathResource("mybatis-config.xml")); //bean.setMapperLocations(new ClassPathResource("CarMapper.xml")); return bean; } @Bean public TransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
2.配置类添加MpperScan注解--@MapperScan("com.yang.dao"),将Mapper接口交给Spring
3.使用直接getBean即可
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); CarMapper bean = context.getBean(CarMapper.class); int insert = bean.insert(new Car()); System.out.println(insert);
ps:跑完出现
因为HikariDataSource里用了SLF4J,配置下日志即可。
事务功能具体使用下面介绍。
5.spring-jdbc(事务)
用aop可以实现事务,但事务很常用,spring对其进行了二次封装实现了声明式事务,不用自己编程实现事务了。
专门开发了一套API(对AOP进行封装)
核心接口为PlatformTransactionManager
PlatformTransactionManager:spring事务管理器的核心接口。在Spring6中它有两个实现:
- DataSourceTransactionManager:支持JdbcTemplate、MyBatis、Hibernate等事务管理。
- JtaTransactionManager:支持分布式事务管理。
注解实现声明式事务:
(配置类添加@EnableTransactionManagement
注解即可,这样就可以开启Spring的事务支持了)
1.获取DataSourceTransactionManager(我们整合Mybatis,所以用这个)
(先配DataSource)
@Bean
public TransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
2.添加事务的方法上加注解@Transcational
其中事务属性有什么?
事务中的重点属性:(动力节点这里讲的很细Spring · 语雀)
- 事务传播行为 eg: @Transactional(propagation = Propagation.REQUIRED)
- 事务隔离级别 eg: @Transactional(isolation = Isolation.READ_COMMITTED)
- 事务超时
- 只读事务
- 设置出现哪些异常回滚事务
- 设置出现哪些异常不回滚事务
传播行为:
在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。(一共7种,以下常用)
- REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
- REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】
隔离级别:
默认级别为:不可重复读
- ISOLATION_READ_UNCOMMITTED(读未提交):其他事务会读取当前事务尚未更改的提交(相当于读取的是这个事务暂时缓存的内容,并不是数据库中的内容)
- ISOLATION_READ_COMMITTED(读已提交):其他事务会读取当前事务已经提交的数据(也就是直接读取数据库中已经发生更改的内容)
- ISOLATION_REPEATABLE_READ(可重复读):其他事务会读取当前事务已经提交的数据并且其他事务执行过程中不允许再进行数据修改(注意这里仅仅是不允许修改数据)
- ISOLATION_SERIALIZABLE(串行化):它完全服从ACID原则,一个事务必须等待其他事务结束之后才能开始执行,相当于挨个执行,效率很低
事务不起作用:在service层,方法public,事务管理器和SqlSessionFactoryBean必须读同一个DataSource对象(如果SqlSessionFactoryBean单独读配置文件是不会生效的!!!)
6.spring-test(整合Junit)
7.spring-webmvc (context功能都有额外有web功能)
回顾:
用servlet:1.创建servlet 2.在web.xml中配置servlet做url映射
执行流程
现在用spring-mvc:
SpringMVC执行流程:
1.通过处理器映射器获得处理器执行链。
1.1一次请求对应一个处理器执行链对象。
1.2该对象有两个重要的属性:
Object类型的handler对象,是HandlerMethod对象,代表要执行的处理器方法。
list集合,里面放的是当前请求要执行的所有拦截器。
2.通过处理器执行链获得处理器适配器。
3.通过处理器执行链执行当前请求对应的所有拦截器的preHandle方法。
4.通过处理器适配器执行处理器方法。
5.通过处理器执行链执行当前请求对应的所有拦截器的postHandle方法。
6.处理分发请求:
6.1通过视图解析器对象的resolveViewName方法,将逻辑视图名解析成物理视图名,返回一个视图对象。
6.2通过视图对象render方法,将模板字符串转变成html代码响应给浏览器,完成渲染。
6.3通过处理器执行链执行当前请求对应的所有拦截器的afterCompletion方法。简单来说:DispatcherServlet接收请求(消息转换器将响应头或响应体转化为参数或pojo传入方法),得到执行链对象,包括要执行的方法(Controller)和拦截器,之后执行拦截器的preHandle再执行Controller,执行完毕后,返回一个ModelAndView对象,之后执行拦截器的postHandle方法,ViewResolver(视图解析器)将逻辑视图转为物理视图(加上前缀和后缀),返回一个视图对象,之后视图对象将内容转为html,消息转化器将html文件写入响应体,DispatcherServlet发给浏览器,完成渲染,最后执行拦截器的afterCompletion方法。
如果我们是发送的ajax请求,只要json字符串,需要加@ResponseBody,这样就不会走视图解析器了,消息转化器将返回的内容写到当前页面。(如果我们的请求是静态资源,其实DispatcherServlet接收请求首先会判断请求是否是静态资源路径,默认/**,也就是所有的请求都会判断一下,从静态资源存放路径下找资源,如果有直接返回,没有走上面流程;静态资源还有缓存机制,具体就看springboot章节,记住上面的就行)
整个过程我们只需要编写对应请求路径的的Controller以及配置好我们需要的ViewResolver即可,之后还可以继续补充添加拦截器,而其他的流程已经由SpringMVC帮助我们完成了。
配置
在web.xml中配置DispatcherServlet,在初始时读取spring的配置文件,做url映射
采用全注解的方式配置:
1.继承AbstractAnnotationConfigDispatcherServletInitializer类,代替web.xml文件,初始化servlet上下文(tomcat会自动找这个配置类)
2.实现里面的三个方法:
第一个是读取spring的配置类
第二个是读取springMVC的配置类,此配置类上必须加@EnableWebMvc
(第13章 全注解开发 · 语雀 配置Bean汇总)
第三个是配置DispatcherServlet的映射路径
(ps: / 表示除了xxx.jsp以外的所有的路径请求 /*表示所有的路径请求)
(配置分开,也就是springMVC的配置类是负责controller的,在spring上扫controller是没用的,不认识)public class MainInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class[]{SpringConfig.class}; } @Override protected Class<?>[] getServletConfigClasses() { return new Class[]{SpringMvcConfig.class}; } @Override protected String[] getServletMappings() { return new String[]{"/"}; } }
spring配置类:负责mybatis整合,事务...
@Configuration @ComponentScan({"com.yang.pojo"}) @EnableTransactionManagement @MapperScan("com.yang.dao") public class SpringConfig { @Bean public DataSource dataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mybatis"); dataSource.setUsername("root"); dataSource.setPassword("123456"); return dataSource; } @Bean public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); //bean.addMapperLocations(new ClassPathResource("mybatis-config.xml")); //bean.setMapperLocations(new ClassPathResource("CarMapper.xml")); return bean; } @Bean public TransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
springMVC配置类:负责controller,视图解析器,拦截器,异常...
@Configuration @ComponentScan({"com.yang.controller"}) @EnableWebMvc public class SpringMvcConfig { }
处理静态资源
/ 表示除了xxx.jsp以外的所有的路径请求 /*表示所有的路径请求
也就是说静态资源html,img也走了DispatcherServlet,找不到对应映射路径,报错
解决方案:让静态资源通过Tomcat提供的默认Servlet进行解析,我们需要让配置类实现一下
WebMvcConfigurer
接口,这样在Web应用程序启动时,会根据我们重写方法里面的内容进行进一步的配置public class SpringMvcConfig implements WebMvcConfigurer
@Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer){ configurer.enable(); //开启默认的Servlet } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**").addResourceLocations("/static/"); //配置静态资源的访问路径 }
之后就可以编写controller和所需要的视图解析器了。
配置视图解析器(以后前后端分离不会这样搞啊,复制过来就好了)
先导包:<dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring6</artifactId> <version>3.1.1.RELEASE</version> </dependency>
@Configuration @ComponentScan({"com.yang.controller"}) @EnableWebMvc public class SpringMvcConfig{ //我们需要使用ThymeleafViewResolver作为视图解析器,并解析我们的HTML页面 @Bean public ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine springTemplateEngine){ ThymeleafViewResolver resolver = new ThymeleafViewResolver(); resolver.setOrder(1); //可以存在多个视图解析器,并且可以为他们设定解析顺序 resolver.setCharacterEncoding("UTF-8"); //编码格式是重中之重 resolver.setTemplateEngine(springTemplateEngine); //和之前JavaWeb阶段一样,需要使用模板引擎进行解析,所以这里也需要设定一下模板引擎 return resolver; } //配置模板解析器 @Bean public SpringResourceTemplateResolver templateResolver(){ SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver(); resolver.setSuffix(".html"); //需要解析的后缀名称 resolver.setPrefix("/"); //需要解析的HTML页面文件存放的位置,默认是webapp目录下,如果是类路径下需要添加classpath:前缀 return resolver; } //配置模板引擎Bean @Bean public SpringTemplateEngine springTemplateEngine(ITemplateResolver resolver){ SpringTemplateEngine engine = new SpringTemplateEngine(); engine.setTemplateResolver(resolver); //模板解析器,默认即可 return engine; } }
其中配置html文件所在位置,我们改为classpath:直接放到resourses目录下,webapp目录就没用了。
之后就可以编写controller了。
编写controller
例子:
@Controller
public class MainController {
@ResponseBody
@RequestMapping("/main")
public String hello(){
return "HelloWorld!";
}
@RequestMapping("index")
public String index(){
return "index";
}
}
1)RequestMapping注解
位置:此注解可以放到类上或方法上
属性:
- value和path,可以放路径数组
eg. @RequestMapping(value = {"/testValue1", "/testValue2"})
路径风格:ant("/x?z/testValueAnt") 以及 RESTful(/login/{id}/{username}/{password}) - method,放RequestMethod枚举的数组
RequestMethod枚举类型里有所有的请求方式
共9种,前5种常用:
GET POST PUT POST DELETE (搭配restful风格来使用)HEAD
注意事项:
get请求支持缓存。 也就是说当第二次发送get请求时,会走浏览器上次的缓存结果,不再真正的请求服务器。(有时需要避免,怎么避免:在get请求路径后添加时间戳)
post请求不支持缓存。每一次发送post请求都会真正的走服务器
衍生Mapping
@RequestMapping(value="/login", method = RequestMethod.POST)
@PostMapping("/login") 等同的
其余的也有 -
params,来指定请求必须携带哪些请求参数
eg:
@RequestMapping(value="/login", params={"username", "password"}) 表示:请求参数中必须包含 username 和 password,才能与当前标注的方法进行映射。@RequestMapping(value="/login", params={"username=!admin","password=123"}) -
headers,headers和params原理相同,用法也相同。
当前端提交的请求头信息和后端要求的请求头信息一致时,才能映射成功。
RESTful 风格
发送:
风格:http://localhost:8080/mvc/index/123456
REST对请求方式的约束是这样的:
- 查询必须发送GET请求
- 新增必须发送POST请求
- 修改必须发送PUT请求
- 删除必须发送DELETE请求
(发送put delete请求的前提是一个post请求)
接收:
@RequestMapping("/index/{str}") public String index(@PathVariable("str") String str) { System.out.println(str); return "index"; }
路径接收的名与形参一致可以省略("str"),直接写@PathVariable即可
2)获取请求数据
获取请求提交的数据
1.使用原生的Servlet API进行获取
2.使用RequestParam注解标注
@RequestParam中填写参数名称,参数的值会自动传递给形式参数,我们可以直接在方法中使用,注意,如果参数名称与形式参数名称相同,即使不添加@RequestParam
也能获取到参数值
eg:
public ModelAndView index(@RequestParam("username") String a)
或者 public ModelAndView index(String username)
spring6+要添加参数在pom.xml中,才可以省略
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>17</source>
<target>17</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
一旦添加@RequestParam
,那么此请求必须携带指定参数,我们也可以将require属性设定为false来将属性设定为非必须,还可以设置默认值
@RequestParam(value = "username",required = false,defaultValue = "admin")
2.直接传给对象
public ModelAndView index(User user)
注意必须携带set方法或是构造方法中包含所有参数,请求参数会自动根据类中的字段名称进行匹配
3.如果是用restful风格,要用注解@PathVariable加到形参上
4.如果前端返回json数据,直接用pojo对象接收,在pojo形参上加上@RequestBody,消息转化器会自动转为pojo对象
获取请求头信息
使用RequestHeader注解,和RequestParam注解功能相似,RequestParam注解的作用:将请求参数
映射到方法的形参
上。
获取客户端提交的Cookie数据
使用CookieValue注解
如果乱码:
消息转化器底层还是tomcat将请求报文封装成HttpServletRequest,然后用getParameter来得到值,传给形参,所以在DispatcherServlet之前执行request.setCharacterEncoding("UTF-8")
过滤器Filter可以在Servlet执行之前执行。有同学又说了:监听器不行吗?不行。因为我们需要对每一次请求解决乱码,而监听器只在服务器启动阶段执行一次。因此这里解决每一次请求的乱码问题,应该使用过滤器Filter。
SpringMVC已经将这个字符编码的过滤器提前写好了,我们直接配置好即可:CharacterEncodingFilter(具体自己看去)
3)数据共享
1.request域下共享
(request生命周期是一次请求,用在转发时共享数据)
使用ModelAndView
(如果返回的是字符串也会封装为ModelAndView对象,将逻辑视图转为物理视图)
(注意ModelAndView不是出现在方法的参数位置,而是在方法体中new的)
@RequestMapping("index")
public ModelAndView index(String name) {
System.out.println(name);
ModelAndView modelAndView = new ModelAndView("index");
modelAndView.addObject("name", name);
return modelAndView;
}
我们也可以直接用ModelAndView中的Model,可以在形参中获得
使用Model接口,Map接口,ModelMap类均为Model
eg:
@RequestMapping(value = "/index")
public String index(Model model){ //这里不仅仅可以是Model,还可以是Map、ModelMap
model.addAttribute("name", "yyds");
return "index";
}
2.session域下共享
(session生命周期是一次会话,用在重定向数据共享和保持登录状态)
使用SessionAttributes注解
@Controller
@SessionAttributes(value = {"x", "y"})
public class SessionScopeTestController {
@RequestMapping("/testSessionScope2")
public String testSessionAttributes(ModelMap modelMap){
// 向session域中存储数据
modelMap.addAttribute("x", "我是埃克斯");
modelMap.addAttribute("y", "我是歪");
return "view";
}
}
SessionAttributes注解使用在Controller类上。标注了当key是 x 或者 y 时,数据将被存储到会话session中。如果没有 SessionAttributes注解,默认存储到request域中。
3 application(ServletContext)
(ServletContext生命周期服务器,用在统计网站在线人数)
直接使用Servlet API
4)转发和重定向
原理:我们最后全返回一个ModelAndView对象,交给视图解析器之后返回一个view对象
view对象有很多种,转发默认InternalResourceView,重定向默认RedirectView,Thymeleaf解析器创建ThymeleafView。在SpringMVC中是怎么通过代码完成转发的?
@RequestMapping("/a")
public String toA(){
// 返回的是一个逻辑视图名称
return "a";}
注意:当 return"a";的时候,返回了一个逻辑视图名称。这种方式跳转到视图,默认采用的就是 forward方式跳转过去的。只不过这个底层创建的视图对象:ThymeleafView怎么转发?语法格式是什么呢?
return "forward:/b";转发到 /b,这是一次请求,底层创建的视图对象是:InternalResourceView对象。
怎么重定向?语法格式是什么呢?
return "redirect:/b";转发到 /b,这是两次请求,底层创建的视图对象是,RedirectView对象。
5)ajax请求和json
ajax是刷新局部,之前完成此操作就是response.getWriter().print("hello");
将页面写入东西或修改东西。
现在mvc怎么实现呢?
我们可以使用 @ResponseBody 注解来启用对应的消息转换器。而这种消息转换器只负责将Controller返回的信息以响应体的形式写入响应协议,不会走视图解析器了。@ResponseBody +return "hello" --> 用StringHttpMessageConverter消息转换器,将hello直接写入页面
@ResponseBody +return bean --> 用MappingJackson2HttpMessageConverter消息转换器,将bean转我json字符串再写入页面
(用MappingJackson2HttpMessageConverter消息转换器,要导入json和java互相转化的工具包,如下)
ps:如果自己用org.json.JSONObject写了JSONObject,然后加上@ResponseBody返回此json对象,会出现错误406,下面这个包和这个json对象转化时会出错。<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.0</version> </dependency>
@RestController这一个注解代表了:@Controller + @ResponseBody。
@RestController 标注在类上即可。被它标注的Controller中所有的方法上都会自动标注 @ResponseBody。
使用注解@RequestBody 放到形参上,将前端发的json数据转为pojo对象
@RequestMapping("/send") @ResponseBody public String send(@RequestBody User user){ System.out.println(user); System.out.println(user.getUsername()); System.out.println(user.getPassword()); return "success"; }
拦截器
柏码 - 让每一行代码都闪耀智慧的光芒!(详细看这里)
我们之前讲解的过滤器是作用于Servlet之前,只有经过层层的过滤器才可以成功到达Servlet,拦截器是在DispatcherServlet将请求交给对应Controller中的方法之前进行拦截处理。
1.创建拦截器 实现 HandlerInterceptor
接口
实现方法 preHandle --> 在Controller中的方法之前运行,如果返回true,才会继续执行controller 方法,返回false是空白,不会继续执行
实现方法postHandle --> 在Controller中的方法之后运行,多级的拦截器会倒着执行
实现方法afterCompletion--> 页面渲染后,执行此方法,多级的拦截器会倒着执行
2.注册
在拦截器注册中注册此拦截器,并设置拦截的路径
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MainInterceptor())
.addPathPatterns("/**") //添加拦截器的匹配路径,只要匹配一律拦截
.excludePathPatterns("/home"); //拦截器不进行拦截的路径
}
异常处理
自定义一个异常处理控制器,一旦出现指定异常,就会转接到此控制器执行。
柏码 - 让每一行代码都闪耀智慧的光芒!(详细看这里)
web错误积累
如果前端发送请求的方式和后端的处理方式不一致时,会出现405错误。
更好的整合方式:第14章 SSM整合 · 语雀
8. Spring补充
Spring高级特性:
柏码 - 让每一行代码都闪耀智慧的光芒!
- Bean Aware
- 任务调度(多线程 异步执行 底层aop开线程)
- 监听器
- SpEL
都是基于ioc,aop的,用到了可以看上面文章
Spring导入外部文件:
- 引入其他配置class @Import({DataSourceConfig.class, MyBatisConfig.class})
- .properties的键值对文件 @PropertySource
此注解是标注在类上的,但是不一定要只能标注在@Configuration注解的类上,只要被Spring管理到的Bean都是可以标注的,然后使用@Value("${}")的方式注入的(SpEL); 只要PropertySource的文件引入一次,该Spring容器中bean都可以使用@Value来设置属性
9.Spring Security 6
两万字详解,带你彻底搞懂 Spring Security 6.0 的实现原理_springsecurity6-优快云博客
简易原理
用户通过浏览器或客户端发送 HTTP 请求到服务器
DelegatingFilterProxy过滤器拦截所有的请求,通过FilterChainProxy根据请求URL走不一样的SecurityFilterChain执行流程(比如根据配置的antMatchers)。
执行过滤器流程即可。
(执行的每一个过滤器,大多数并不是简单的implents Filter,而是实现了OncePerRequestFilter,OncePerRequestFilter是Security加强的过滤器, 确保每个请求只被处理一次)
对于默认filter执行(没做任何配置)
开启默认:1.导包 2.下述代码 3.写一个Security配置类加上@EnableWebSecurity
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
//不用重写任何内容
//这里实际上会自动注册一个Filter,SpringSecurity底层就是依靠N个过滤器实现的,我们之后再探讨
}
1.拦截所有的http请求
2.SecurityContextPersistenceFilter:
SecurityContextHolder会看当前请求中有没有用户信息(有没有cookie存有效seesionId)
有的话从HttpSession中读后放进去(即使SecurityContextHolder在登录后放入信息了,也会从cookie中读,保证信息最新),不用进行身份认证了,然后会自动从这个信息中判断此用户有没有权限访问
没有的话要身份认证,得到用户的信息
3.CsrFilter:防止跨站请求伪造 以及其他防止攻击的保护(默认开启)
4.身份认证:
[如果经过第一次filter,SecurityContextHolder不为空,就不会执行了]
(通过表单方式也就是接收username和password)
没登录就是没有输入用户名和密码,访问受保护的url,会触发AuthenticationEntryPoint跳转到登录页面
(默认都要身份认证)
(或者配置exceptionHandling的authenticationEntryPoint,返回json数据)
认证:UsernamePasswordAuthenticationFilter
捕获登录请求,调用 AuthenticationManager
进行认证。
认证通过,将用户身份信息(即Authentication
对象,实现类为XXXToken,我们也可以自己实现这个)放到SecurityContextHolder中并发送一个cookie存放
seesionId, seesion中存放用户信息,之后重定向到默认"/"
(或者可以自己配置SuccessHandler,发送json数据)
认证失败就跳转到登录页面
(或者可以自己配置FailureHandler,发送json数据)
5.授权检查
[从SecurityContext
中读取用户信息 ]
如果用户没有访问权限,则会返回403禁止访问状态,抛出AccessDeniedException
(默认是都有访问权的)
(或者可以配置exceptionHandling的accessDeniedHandler,返回json数据)
6.交给DispatherServlet来处理
7.LogoutFilter:清除会话seesionId的cookie,清除SecurityContextHolder内容
(这个也可以配置url显示的让用户使用,logout->logoutUrl)
(退出成功后的逻辑也可以配置logout->logoutSuccessHandler)
对于后续的请求,Spring Security 将从 SecurityContext
中读取用户信息,执行相应的认证和授权逻辑。
上面的过滤器大多数都是实现了OncePerRequestFilter,所以如果输入url1会执行上述所有流程,但是后台转发给url2,这是在一个请求中,所以实现OncePerRequestFilter的过滤器不会再执行,那么转发到url2之前,会执行的有授权检查等一些filter。
注意:表单直接提交到/doLogin,这是security管理的身份认证的loginProcessingUrl,默认叫/doLogin,那这个就完全交给security管理,不会走DispatherServlet;还是跟之前一样的流程,只是到授权就结束了。同理,交给logoutUrl也是一样的,完全交给security管理,流程一样,跳过交给DispatherServlet来处理。
(这两在配置完后一定要permitAll(),要不还是会执行身份验证,不放行)
可选:当我们开启记住我功能后,还会额外发给一个cookie交remeber-me给浏览器,即使关闭会话,下次访问也会自动带上此cookie,那么就不用登录了,SecurityContextHolder会读到。
修改默认配置
自定义一个SecurityFilterChain,在这里面可以对上述流程做修改。
上面标黄的地方都可以修改
1.可以关闭CsrFilter
2.哪些url要认证
3.哪些url需要什么授权
4.登录方式不同可以配置不同登录方式里面具体的操作
默认情况下,如果未显式配置其他登录方式(如OAuth2、JWT等),formLogin
会是默认的登录 方式。可配置login页面,logout页面...以及上面自定义的SuccessHandler和FailureHandler
[这里不演示handler配置,这篇中有配置柏码 - 让每一行代码都闪耀智慧的光芒!]
5.配置exceptionHandling
6.是否开启记住我功能
当登录时勾选记住我,页面会发一个remember-me:on的选项
然后security就分配一个携带Token的Cookie,并且此Cookie默认会被保留14天,只要我们不清理浏览器的Cookie,那么下次携带此Cookie访问服务器将无需登陆,直接继续使用之前登陆的身份
记住我信息是存放在内存中的,我们需要保证服务器一直处于运行状态,如果关闭服务器的话,记住我信息会全部丢失,因此,如果我们希望记住我能够一直持久化保存,我们就需要进一步进行配置 。(具体看白马)
(注解好好看看)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
//以下是csrf相关配置
.csrf(conf -> {
conf.disable(); //此方法可以直接关闭全部的csrf校验,一步到位
conf.ignoringRequestMatchers("/xxx/**"); //此方法可以根据情况忽略某些地址的csrf校验
})
//以下是验证请求拦截和放行配置
.authorizeHttpRequests(auth -> {
auth.requestMatchers(new AntPathRequestMatcher("/static/**")).permitAll(); //将所有的静态资源放行,一定要添加在全部请求拦截之前
auth.anyRequest().authenticated(); //将所有请求全部拦截,一律需要验证
})
//以下是表单登录相关配置
.formLogin(conf -> {
conf.loginPage("/login"); //将登录页设置为我们自己的登录页面
conf.loginProcessingUrl("/doLogin"); //登录表单提交的地址,可以自定义
// 一定要跟表单提交地址一致,<form action="/mvc/doLogin" method="post">
//springSecurity会处理这个逻辑,不用自己写哦
//跟之前一样走UserDetailsService一系列流程,默认就是/doLogin
conf.defaultSuccessUrl("/"); //登录成功后跳转的页面
conf.permitAll(); //将登录相关的地址放行,否则未登录的用户连登录界面都进不去
//用户名和密码的表单字段名称,不过默认就是这个,可以不配置,除非有特殊需求
conf.usernameParameter("username");
conf.passwordParameter("password");
})
//以下是退出登录相关配置
.logout(conf -> {
conf.logoutUrl("/doLogout"); //退出登录地址,跟上面一样可自定义
conf.logoutSuccessUrl("/login"); //退出登录成功后跳转的地址,这里设置为登录界面
conf.permitAll();
})
.rememberMe(conf -> {
//conf.alwaysRemember(false); //这里不要开启始终记住,我们需要配置为用户自行勾选,默认关
//conf.rememberMeParameter("remember-me"); //记住我表单字段,默认就是这个,可以不配置
//conf.rememberMeCookieName("xxxx"); //记住我设置的Cookie名字,也可以自定义,不过没必要
})
.build();
}
认证
上述讲到流程中有身份认证流程,如果是表单形式(默认),怎么进行验证的呢?
首先AuthenticationManager(其实现类是ProviderManager),会遍历所有找到适合处理当前认证方式的AuthenticationProvider。
比如最常见的用户名密码的认证实现是DaoAuthenticationProvider
,而JwtAuthenticationProvider
提供了JWT Token的认证。
AuthenticationProvider首先调用UserDetailsService获取用户信息,然后将获取到的密码(通常是编码后的密码)委托给PasswordEncoder进行验证。如果认证失败,AuthenticationProvider会抛出AuthenticationException的子类表示认证失败。
当认证成功时,AuthenticationManager
会返回一个UsernamePasswordAuthenticationToken
对象(Authentication对象实现类)作为认证结果,这个对象除了包含用户的基本信息外,最重要的是认证通过状态以及该用户拥有的权限列表,这些信息在后续的鉴权模块会用到。
认证结果会被放入SecurityContext
,这样后续的模块(包括鉴权和用户业务模块等)如果需要这个结果(包括用户信息和权限列表),就可以通过以下方法获取:SecurityContextHolder.getContext().getAuthentication()
。
使用上:默认有AuthenticationManager,然后找AuthenticationProvider(公司会自定义),把用户输入的密码用PasswordEncoder加密,调用UserDetailsService(默认基于内存,可以配置成基于数据库的)的查找用户信息方法找到密码,之后用PasswordEncoder进行验证(需要创建一个PasswordEncoder实现类,一般是用BCryptPasswordEncoder)。
授权也是一样的,UserDetailsService返回的用户信息也有权限,校验一下。
UserDetailService
1)基于内存
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
//这里将BCryptPasswordEncoder直接注册为Bean,Security会自动进行选择
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails user = User
.withUsername("user")
.password(encoder.encode("password")) //这里将密码进行加密后存储
.roles("USER")
.build();
System.out.println(encoder.encode("password")); //一会观察一下加密出来之后的密码长啥样
UserDetails admin = User
.withUsername("admin")
.password(encoder.encode("password")) //这里将密码进行加密后存储
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
2)基于数据库
表是固定的,sql语句给写好了
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public DataSource dataSource(){
//数据源配置
return new PooledDataSource("com.mysql.cj.jdbc.Driver",
"jdbc:mysql://localhost:3306/test", "root", "123456");
}
@Bean
public UserDetailsService userDetailsService(DataSource dataSource,
PasswordEncoder encoder) {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
return manager;
}
}
3)自定义验证
上面的InMemoryUserDetailsManager和JdbcUserDetailsManager都是UserDetailsManager的实现类,而UserDetailsManager继承了UserDetailsService接口。
UserDetailsManager这个接口有以下功能,所以我们可以实现UserDetailsManager定义我们的XXXManager。或者直接实现UserDetailsService也可以,只有一个方法。
@Service
public class AuthorizeService implements UserDetailsService {
@Resource
UserMapper mapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = mapper.findUserByName(username);
if(account == null)
throw new UsernameNotFoundException("用户名或密码错误");
return User
.withUsername(username)
.password(account.getPassword())
.roles(account.getRole()) //添加角色,一个用户可以有一个或多个角色
.build();
}
}
注意:数据库里面存的就是加密后的,用户输入进行的密码也是进行加密的
如果这里数据看中密码是明文的,会不匹配。
授权验证
可以看到上述返回的UserDetails有roles属性,此属性就是基于角色授权来告诉url哪些角色可以访问它。(也可基于权限,自己查)
配置:
.authorizeHttpRequests(auth -> {
//静态资源依然全部可以访问
auth.requestMatchers("/static/**").permitAll();
//只有具有以下角色的用户才能访问路径"/"
auth.requestMatchers("/").hasAnyRole("user", "admin");
//其他所有路径必须角色为admin才能访问
auth.anyRequest().hasRole("admin");
})
或者我们用注解的方式
@PreAuthorize :方法执行前判断你有没有权限执行
@PostAuthorize :方法执行后判断结果你有没有权限拿
@Controller
public class HelloController {
@PreAuthorize("hasRole('user')") //直接使用hasRole方法判断是否包含某个角色
@GetMapping("/")
public String index(){
return "index";
}
}
除了Controller以外,只要是由Spring管理的Bean都可以使用注解形式来控制权限,我们可以在任意方法上添加这个注解,只要不具备表达式中指定的访问权限,那么就无法执行方法并且会返回403页面。
@Service
public class UserService {
@PreAuthorize("hasAnyRole('user')")
public void test(){
System.out.println("成功执行");
}
}
添加一个过滤器
自定义过滤器需要实现 Filter
接口或继承 OncePerRequestFilter
抽象类。推荐使用 OncePerRequestFilter
,因为它可以确保每个请求只被处理一次。
之后在sercurityFilterChain中添加即可。【具体在springboot中看】