Spring基础

Spring核心是解耦,而不是简化代码,就像一个工具库一样

IoC容器

创建Spring项目

maven导入

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.9</version>
</dependency>

Spring配置文件

name属性是别名,class属性是文件路径

1
<bean name="student" class="com.test.bean.Student"/>

scope属性切换模式,默认singleton单例模式,调用时始终为一个对象;prototype原型模式,可以创建多个对象

1
<bean name="student" class="com.test.bean.Student" scope="prototype"/>

调用对象时自动运行初始化与销毁方法,两个属性的参数为方法名

1
<bean name="student" class="com.test.bean.Student" init-method="init" destroy-method="destroy"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Student {
String name;
int age;

private void init(){
System.out.println("我是初始化方法!");
}

private void destroy(){
System.out.println("我是销毁方法!");
}

public Student(){
System.out.println("我被构造了!");
}
}

默认执行顺序是配置文件的bean顺序,depends-on属性可以改变执行顺序,指定某个bean在它之前先加载

1
2
<bean name="student" class="com.test.bean.Student"  depends-on="card"/>
<bean name="card" class="com.test.bean.Card" />

Main方法调用类

获取Spring配置文件;个人倾向使用指定某个类,写起来简单一点;单例模式下调用的是同一个对象

1
2
3
4
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-beans.xml");
Student student = context.getBean(Student.class); //指定Student类
Student student2 = (Student) context.getBean("student"); //多态获取Student类,两种写法一样
System.out.println(student == student2); //true

依赖注入DI

property标签,能给Bean成员属性赋值,前提是有set方法

1
2
3
4
5
6
7
8
9
10
@Data
public class Student {
String name;
int age;
}

<bean name="student" class="com.test.bean.Student">
<property name="name" value="幽蓝"/>
<property name="age" value="9"/>
</bean>

ref属性引用另一个bean,再将对应的类型注册为bean

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Card {
}

@Data
public class Student {
Card newCard;
}

<bean name="student" class="com.test.bean.Student">
<property name="newCard" ref="card"/>
</bean>

<bean name="card" class="com.test.bean.Card"/>

String类型不需要type属性,其他类型需要type属性指定相同类型

1
2
3
4
5
6
7
8
9
10
11
12
13
List<String>list;
List<Double>list;

<bean name="student" class="com.test.bean.Student">
<property name="list">
<list>
<!--<value>幽蓝</value>-->
<value type="double">100.0</value>
<value type="double">90.0</value>
<value type="double">80.0</value>
</list>
</property>
</bean>

map集合

1
2
3
4
5
6
7
8
9
10
Map<String, Integer> score;

<bean name="student" class="com.test.bean.Student">
<property name="score">
<map>
<entry key="语文" value="100"/>
<entry key="数学" value="90"/>
</map>
</property>
</bean>

自动装配,查找是否存在对应名称、类型、构造方法的Bean,其余两个暂不了解

1
2
3
4
5
<bean name="student" class="com.test.bean.Student" autowire="byName"/>
byType
constructor
default
no

constructor-arg标签指定构造函数,建议用name属性指向形参,看起来更直观一点

1
2
3
4
5
6
7
8
9
10
11
public Student(String name, int age) {
this.name = name;
this.age = age;
}

<bean name="student" class="com.test.bean.Student">
<constructor-arg name="name" value="幽蓝"/>
<constructor-arg name="age" value="9"/>
type="java.lang.String" value="幽蓝"
index="0" value="幽蓝"
</bean>

工厂模式

默认情况下,反射机制通过构造方法创建Bean对象;工厂模式下,反射机制先找到工厂类,再从工厂方法创建Bean对象

1
2
3
4
5
6
7
8
9
10
11
12
public class Student {
Student() {
System.out.println("我被构造了");
}
}

public class StudentFactory {
public static Student getStudent(){
System.out.println("欢迎光临电子厂");
return new Student();
}
}

class指定工厂类,通过它找到工厂方法;factory-method指定工厂方法,缺点是无法通过配置文件对Bean依赖注入

1
<bean class="com.test.bean.StudentFactory" factory-method="getStudent"/>

第二种方式,可以进行依赖注入,首先去掉static关键字

1
2
3
4
5
6
public class StudentFactory {
public Student getStudent(){
System.out.println("欢迎光临电子厂");
return new Student();
}
}

普普通通注册成bean,再使用factory-bean绕一圈,就可以依赖注入了

1
2
<bean name="studentFactory" class="com.test.bean.StudentFactory"/>
<bean factory-bean="studentFactory" factory-method="getStudent"/>

获取工厂bean生产出的bean的实例

1
Student bean = (Student) context.getBean("studentFactory");

获取工厂类的实例

1
StudentFactory bean = (StudentFactory) context.getBean("&studentFactory");

面向切面AOP

SpringAOP

maven导入

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.9</version>
</dependency>

引入aop相关标签,就能使用aop操作

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

我想say方法执行后,调用after方法,写好类放一边

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
@Log
public class AopTest {
public void after(){
log.info("我是方法执行之后的日志!");
}
}

@Data
public class Student {
String name;
String age;

public Student(String name, String age) {
this.name = name;
this.age = age;
}

public void say(String text) {
System.out.println("我叫" + name + "今年" + age + "岁了,我说:" + text);
}
}

public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-beans.xml");
Student student = context.getBean(Student.class);
student.say("我想睡觉!!!!!!");
}
}

aop:config标签添加AOP配置,expression属性切入方式,execution链式切入

  • 修饰符:*代表任意修饰符
  • 包名:如com.test(*代表全部,比如com.*代表com包下的全部包)
  • 类名:*代表包下的所有类
  • 方法名称:*代表全部方法
  • 方法参数:填写对应的参数,比如(String, String),*代表任意一个参数,使用..代表所有参数
1
2
3
4
5
6
7
8
9
10
11
12
13
<bean name="student" class="com.test.bean.Student">
<constructor-arg name="name" value="幽蓝"/>
<constructor-arg name="age" value="9"/>
</bean>

<bean name="aopTest" class="com.test.aop.AopTest"/>

<aop:config>
<aop:aspect ref="aopTest">
<aop:pointcut id="stu" expression="execution(* com.test.bean.Student.say(String))"/>
<aop:after-returning method="after" pointcut-ref="stu"/>
</aop:aspect>
</aop:config>

@annotation注解切入,参数是注解名

1
2
3
4
5
6
<aop:config>
<aop:aspect ref="aopTest">
<aop:pointcut id="stu2" expression="@annotation(Deprecated)"/>
<aop:before method="before" pointcut-ref="stu2"/>
</aop:aspect>
</aop:config>

环绕方法相当于完全代理say方法,joinPoint.proceed()执行say方法,类似重写的super

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String say(String text) {
System.out.println("我叫" + name + "今年" + age + "岁了,我说:" + text);
return text;
}

public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
Object res = joinPoint.proceed();
System.out.println("我是环绕方法");
return res;
}

<aop:config>
<aop:aspect ref="aopTest">
<aop:pointcut id="stu" expression="execution(* com.test.bean.Student.say(String))"/>
<aop:around method="around" pointcut-ref="stu"/>
</aop:aspect>
</aop:config>

接口实现AOP

MethodBeforeAdvice方法执行前的动作,args方法执行前的实参列表,target执行此方法的实例对象

1
2
3
4
5
6
7
8
9
10
11
public class AopTest implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("通过Advice实现AOP");
}
}

<aop:config>
<aop:pointcut id="stu" expression="execution(* com.test.bean.Student.say(String))"/>
<aop:advisor advice-ref="before" pointcut-ref="stu"/>
</aop:config>

AfterReturningAdvice方法执行后的动作,returnValue返回形参,其余一样

1
2
3
4
5
6
public class AopAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("方法的返回值:" + returnValue);
}
}

注解开发

注解配置文件

批量扫描是个数组;@Configuration注册配置;@Component注册成bean;@Scope切换成原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//批量扫描
@ComponentScans({
@ComponentScan("com.test.bean"),
@ComponentScan("com.test.xxxx"),
...
})

//扫描包
@ComponentScan("com.test.bean")
@Configuration
public class MainConfiguration {}

@Component
@Scope("prototype")
@Data
public class Student {
String name;
int age;
}

Main方法调用类

获取配置文件的对象变了,其他跟IoC一样;原型模式下是两个不同的对象

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class);
Student student = context.getBean(Student.class);
Student student2 = context.getBean(Student.class);
System.out.println(student == student2); //false
}
}

自动注入

@Resource自动装配,类似new对象;默认ByName,如果找不到则ByType,能添加到set方法、字段上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class Student {
String name;
int age;

@Resource
Card card;

@Resource
public void setCard(Card card) {
System.out.println("通过方法");
this.card = card;
}
}

JDK17没有这个注解,maven导入

1
2
3
4
5
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>

@PostConstruct添加构造后执行的方法;Constructor(构造方法) -> @Resource(自动注入) -> @PostConstruct

1
2
3
4
@PostConstruct
public void init(){
System.out.println("我是初始化方法!");
}

如果是其他框架提供的类想要注册为bean,且无法修改源码,用最原始的@Bean来注册

1
2
3
4
5
6
7
@Configuration
public class MainConfiguration {
@Bean
public Test test(){
return new Test();
}
}

注解AOP操作

定义say方法,以及main方法调用say方法,跟之前aop一样╮(╯▽╰)╭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class Student {
String name;
int age;

public String say(String text) {
System.out.println("我叫" + name + ",今年" + age + "岁了");
return text;
}
}

public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class);
Student student = context.getBean(Student.class);
System.out.println(student.say("想摆烂"));
}
}

@EnableAspectJAutoProxy开启AOP注解支持

1
2
3
4
5
6
7
@ComponentScans({
@ComponentScan("com.test.aop"),
@ComponentScan("com.test.bean")
})
@EnableAspectJAutoProxy
@Configuration
public class MainConfiguration {}

@Aspect开启切面,@Before@AfterReturning选择切点,returning属性作为方法某个参数的实参

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
@Aspect
public class AopTest {
@Before("execution(* com.test.bean.Student.say(..))")
public void before(){
System.out.println("我是方法执行前的事情");
}

@AfterReturning(value = "execution(* com.test.bean.Student.say(..))", returning = "returnVal")
public void after(Object returnVal){
System.out.println("我是方法执行后,结果为:" + returnVal);
}
}

环绕方法

1
2
3
4
5
6
7
@Around("execution(* com.test.bean.Student.say(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("我是方法执行前的事情");
Object val = point.proceed();
System.out.println("我是方法执行后");
return val;
}

其他注解配置

@Import将某个类当作Bean来注册,用来解决多个配置文件问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@ComponentScans({
@ComponentScan("com.test.aop"),
@ComponentScan("com.test.bean")
})
@Import(TestConfiguration.class)
@Configuration
public class MainConfiguration {}

//另一个注解配置
public class TestConfiguration {
@Bean
public Teacher teacher(){
return new Teacher();
}
}

public class Teacher {
}

深入Mybatis框架

spring整合数据源

maven导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.9</version>
</dependency>

创建一个数据源的实现类,再给SqlSessionFactoryBean实例,相当于通过IoC容器配置了SqlSessionFactory,再传入一个DataSource的实现

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
@ComponentScans({
@ComponentScan("com.test.aop"),
@ComponentScan("com.test.bean")
})
@Configuration
@EnableAspectJAutoProxy
@MapperScan("com.test.mapper")
public class MainConfiguration {
@Bean
public DataSource dataSource(){
PooledDataSource dataSource = new PooledDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/study");
dataSource.setDriver("com.mysql.cj.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("d6NFV8SYsEWPgTvq");
return dataSource;
}

@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(@Autowired DataSource source){
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(source);
return bean;
}
}

用spring开发将不再需要mybatis配置文件

HikariCP连接池

maven导入

1
2
3
4
5
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>

新版本提示NOP什么什么的,暂时无法解决,用旧版本吧

1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.25</version>
</dependency>

不用PooledDataSource数据源了,改成HikariDataSource数据源,url和driver修改下,其他一样

1
2
3
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/study");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");

Mybatis事务管理

事务原则:原子性、一致性、隔离性、持久性
ISOLATION_REPEATABLE_READ(可重复读):其他事务会读取当前事务已经提交的数据并且其他事务执行过程中不允许再进行数据修改
可重复读

  • 脏读:读取到了被回滚的数据,它毫无意义。
  • 虚读(不可重复读):由于其他事务更新数据,两次读取的数据不一致。
  • 幻读:由于其他事务执行插入删除操作,而又无法感知到表中记录条数发生变化,当下次再读取时会莫名其妙多出或缺失数据,就像产生幻觉一样

Spring事务管理

@EnableTransactionManagement开启Spring的事务支持;以下为事务管理器

1
2
3
4
@Bean
public TransactionManager transactionManager(@Autowired DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}

事务传播
@Transactional执行事务操作,参数propagation传播等级

1
@Transactional(propagation = Propagation.SUPPORTS)

集成JUnit测试

maven导入

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.12</version>
</dependency>

@ExtendWithJUnit提供的spring扩展;@ContextConfiguration加载配置文件,支持XMl和类

1
2
3
4
5
6
7
8
9
10
11
12
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = MainConfiguration.class)
public class TestMain {

@Resource
TestService service;

@Test
public void test(){
service.test();
}
}

Spring高级

Bean Aware

实现Aware接口的bean,初始化过程中可以获得一些资源,比如:BeanNameAware获取资源

1
2
3
4
5
6
7
8
@Component
public class Student implements BeanNameAware { //我们只需要实现这个接口就可以了

@Override
public void setBeanName(String name) { //Bean在加载的时候,容器就会自动调用此方法,将Bean的名称给到我们
System.out.println("我在加载阶段获得了Bean名字:"+name);
}
}

BeanClassLoaderAware获取类加载器

任务调度

异步任务

配置类添加@EnableAsync表示开启异步

1
2
3
4
5
@EnableAsync
@Configuration
@ComponentScan("com.test.bean")
public class MainConfiguration {
}

@Async标记为异步,当方法被调用时,会新开一个线程执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class Student {
public void syncTest() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"我是同步执行的方法,开始...");
Thread.sleep(3000);
System.out.println("我是同步执行的方法,结束!");
}

@Async
public void asyncTest() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"我是异步执行的方法,开始...");
Thread.sleep(3000);
System.out.println("我是异步执行的方法,结束!");
}
}

试着调用吧

1
2
3
4
5
6
public static void main(String[] args) throws InterruptedException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class);
Student student = context.getBean(Student.class);
student.asyncTest(); //异步执行
student.syncTest(); //同步执行
}

定时任务

配置类添加@EnableScheduling表示开启定时

1
2
3
4
5
@EnableScheduling
@Configuration
@ComponentScan("com.test.bean")
public class MainConfiguration {
}

@Scheduled编写定时任务,

1
2
3
4
@Scheduled(fixedRate = 2000)   //单位依然是毫秒,这里是每两秒钟打印一次
public void task(){
System.out.println("我是定时任务!"+new Date());
}
  • fixedDelay:在上一次定时任务执行完之后,间隔多久继续执行
  • fixedRate:无论上一次定时任务有没有执行完成,两次任务之间的时间间隔
  • cron:如果嫌上面两个不够灵活,你还可以使用cron表达式来指定任务计划

监听器

Bean继承ApplicationListener,类型指定为对应的Event事件,当某个事件触发时会收到通知;ContextRefreshedEventSpring容器初始化完成时会触发一次

1
2
3
4
5
6
7
@Component
public class TestListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println(event.getApplicationContext()); //可以直接通过事件获取到事件相关的东西
}
}

SpringEL表达式

暂时跳过

SpringMVC

配置文件

maven导入

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.0.9</version>
</dependency>

注册配置

1
2
3
4
5
@Configuration
public class MainConfiguration {}

@Configuration
public class WebConfiguration {}

Tomcat会在类路径中查找实现ServletContainerInitializer 接口的类,如果发现的话,就用它来配置Servlet容器,Spring提供了这个接口的实现类 SpringServletContainerInitializer , 通过@HandlesTypes(WebApplicationInitializer.class)设置,这个类反过来会查找实现WebApplicationInitializer 的类,并将配置的任务交给他们来完成,因此直接实现接口即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{MainConfiguration.class}; //基本的Spring配置类,一般用于业务层配置
}

@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfiguration.class}; //配置DispatcherServlet的配置类、主要用于Controller等配置
}

@Override
protected String[] getServletMappings() {
return new String[]{"/"}; //匹配路径,与上面一致
}
}

Controller控制器

thymeleaf后端渲染引擎,过时了

1
2
3
4
5
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.12.RELEASE</version>
</dependency>

@EnableWebMvc启用WebMvc

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
@ComponentScan("com.example.controller")
@Configuration
@EnableWebMvc
public class WebConfiguration {
//我们需要使用ThymeleafViewResolver作为视图解析器,并解析我们的HTML页面
@Bean
public ThymeleafViewResolver thymeleafViewResolver(@Autowired 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页面文件存放的位置
return resolver;
}

//配置模板引擎Bean
@Bean
public SpringTemplateEngine springTemplateEngine(@Autowired ITemplateResolver resolver){
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolver(resolver); //模板解析器,默认即可
return engine;
}
}

@Controller自动注册为Controller类型的Bean;getModel().put()向Model提供数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller   //直接添加注解即可
public class MainController {
@RequestMapping("/index") //直接填写访问路径
public ModelAndView index(){
return new ModelAndView("index"); //返回ModelAndView对象,这里填入了视图的名称
//返回后会经过视图解析器进行处理
}

//@RequestMapping(value = "/index")
//public ModelAndView index(){
// ModelAndView modelAndView = new ModelAndView("index");
// modelAndView.getModel().put("name", "啊这");
// return modelAndView;
//}
}

添加Model作为形参,SpringMVC自动传递实例对象

1
2
3
4
5
@RequestMapping(value = "/index")
public String index(Model model){ //这里不仅仅可以是Model,还可以是Map、ModelMap
model.addAttribute("name", "yyds");
return "index";
}

静态资源通过Tomcat提供的默认Servlet进行解析

1
2
3
4
5
6
7
8
9
10
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable(); //开启默认的Servlet
}

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/WEB-INF/static/");
//配置静态资源的访问路径
}

@RequestMapping

收到请求时,根据映射关系调用对应的请求处理方法;path、value一样的,决定当前方法处理的请求路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Mapping
public @interface RequestMapping {
String name() default "";

@AliasFor("path")
String[] value() default {};

@AliasFor("value")
String[] path() default {};

RequestMethod[] method() default {};

String[] params() default {};

String[] headers() default {};

String[] consumes() default {};

String[] produces() default {};
}

处理多个请求路径

1
2
3
4
@RequestMapping({"/index", "/test"})
public ModelAndView index(){
return new ModelAndView("index");
}

@RequestMapping表示所有请求映射时,添加一个路径前缀;访问/yyds/index或是/yyds/test才可以得到此页面

1
2
3
4
5
6
7
8
9
@Controller
@RequestMapping("/yyds")
public class MainController {

@RequestMapping({"/index", "/test"})
public ModelAndView index(){
return new ModelAndView("index");
}
}

路径支持通配符

  • ?:表示任意一个字符,比如@RequestMapping(“/index/x?”)可以匹配/index/xa、/index/xb
  • *:表示任意0-n个字符,比如@RequestMapping(“/index/*”)可以匹配/index/hi、/index/yyds
  • **:表示当前目录或基于当前目录的多级目录,比如@RequestMapping(“/index/**”)可以匹配/index、/index/xxx

method属性表示请求方法的类型,用来限定请求方式;@PostMappingPost请求类型的请求映射,还有@GetMapping指定为GET请求方式

1
2
3
4
5
6
7
8
9
10
@RequestMapping(value = "/index", method = RequestMethod.POST)
public ModelAndView index(){
return new ModelAndView("index");
}

//效果一样
@PostMapping(value = "/index")
public ModelAndView index(){
return new ModelAndView("index");
}

params属性指定携带哪些参数;!不允许携带,!=不允许为test

1
2
3
4
5
6
@RequestMapping(value = "/index", params = {"username", "password"})
params = {"!username", "password"}
params = {"username!=test", "password=123"}
public ModelAndView index(){
return new ModelAndView("index");
}

header属性请求头需要携带什么;其他属性,consumes处理请求的提交内容类型,produces返回的内容类型

1
2
3
4
@RequestMapping(value = "/index", headers = "!Connection")
public ModelAndView index(){
return new ModelAndView("index");
}

@RequestParam

@RequestParam获取请求中的参数,value会传递给形参,required是否必需传参,defaultValue参数丢失时的默认参数

1
2
3
4
5
@RequestMapping(value = "/index")
public ModelAndView index(@RequestParam(value = "username", required = false, defaultValue = "我是默认值") String username){
System.out.println("接受到请求参数:"+username);
return new ModelAndView("index");
}

请求参数传递给一个实体类;必须携带set方法或是构造方法中包含所有参数,请求参数会自动根据类中的字段名称进行匹配

1
2
3
4
5
6
7
8
9
10
11
@Data
public class User {
String username;
String password;
}

@RequestMapping(value = "/index")
public ModelAndView index(User user){
System.out.println("获取到cookie值为:"+user);
return new ModelAndView("index");
}

@RequestHeader与@RequestParam用法一致,不过它是用于获取请求头参数的

@CookieValue

获取请求携带的Cookie信息

1
2
3
4
5
6
7
@RequestMapping(value = "/index")
public ModelAndView index(HttpServletResponse response,
@CookieValue(value = "test", required = false) String test){
System.out.println("获取到cookie值为:"+test);
response.addCookie(new Cookie("test", "幽蓝"));
return new ModelAndView("index");
}

@SessionAttribute

获取请求携带的Session信息

1
2
3
4
5
6
7
@RequestMapping(value = "/index")
public ModelAndView index(@SessionAttribute(value = "test", required = false) String test,
HttpSession session){
session.setAttribute("test", "xxxx");
System.out.println(test);
return new ModelAndView("index");
}

重定向和请求转发

redirect:实现重定向,forward:转发给其他请求映射

1
2
3
4
5
6
7
8
9
@RequestMapping("/index")
public String index(){
return "redirect:home";
}

@RequestMapping("/index")
public String index(){
return "forward:home";
}

Bean的Web作用域

Bean分别会以单例和多例模式进行创建,而在SpringMVC中,它的作用域被继续细分:

  • request:每次HTTP请求,使用request作用域定义的Bean都将产生一个新实例,请求结束后Bean也消失
  • session:每一个会话,使用session作用域定义的Bean都将产生一个新实例,会话过期后Bean也消失

@RequestScope@SessionScope表示此Bean的Web作用域

RestFul风格

@PathVariable占位符位置的所有内容都会被作为请求参数

1
2
3
4
5
@RequestMapping("/index/{str}")
public String index(@PathVariable("str") String text){
System.out.println(text);
return "index";
}

按照不同功能划分:

编写四个请求映射

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
@Controller
public class MainController {

@RequestMapping(value = "/index/{id}", method = RequestMethod.GET)
public String get(@PathVariable("id") String text){
System.out.println("获取用户:"+text);
return "index";
}

@RequestMapping(value = "/index", method = RequestMethod.POST)
public String post(String username){
System.out.println("添加用户:"+username);
return "index";
}

@RequestMapping(value = "/index/{id}", method = RequestMethod.DELETE)
public String delete(@PathVariable("id") String text){
System.out.println("删除用户:"+text);
return "index";
}

@RequestMapping(value = "/index", method = RequestMethod.PUT)
public String put(String username){
System.out.println("修改用户:"+username);
return "index";
}
}

Interceptor拦截器

实现HandlerInterceptor接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MainInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("我是处理之前!");
return true; //只有返回true才会继续,否则直接结束
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("我是处理之后!");
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("我是完成之后!");
}
}

配置类注册

1
2
3
4
5
6
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MainInterceptor())
.addPathPatterns("/**") //添加拦截器的匹配路径,只要匹配一律拦截
.excludePathPatterns("/home"); //拦截器不进行拦截的路径
}

异常处理

自定义异常处理控制器,出现指定异常转接给控制器

1
2
3
4
5
6
7
8
9
@ControllerAdvice
public class ErrorController {
@ExceptionHandler(Exception.class)
public String error(Exception e, Model model){ //可以直接添加形参来获取异常
e.printStackTrace();
model.addAttribute("e", e);
return "500";
}
}

th:text是thymeleaf语法,用来显示错误信息

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
500 - 服务器出现了一个内部错误QAQ
<div th:text="${e}"></div>
</body>
</html>

JSON数据格式

前端转换方式

JSON字符串转换JS对象

1
2
3
let obj = JSON.parse('{"studentList": [{"name": "幽蓝", "age": 9}, {"name": "椰奶", "age": 18}], "count": 2}')
//将JSON格式字符串转换为JS对象
obj.studentList[0].name //直接访问第一个学生的名称

JS对象转换JSON字符串

1
JSON.stringify(obj)

后端转换方式

常用JSON解析框架:Jackson和FastJSON

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>

JSONObject存放数据

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping(value = "/index")
public String index(){
JSONObject object = new JSONObject();
object.put("name", "幽蓝");
object.put("age", 9);
System.out.println(object.toJSONString()); //以JSON格式输出JSONObject字符串
return "index";
}

//结果
//{"name": "幽蓝", "age": 9}

JSONArray嵌套JSONObject

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping(value = "/index")
public String index(){
JSONObject object = new JSONObject();
object.put("name", "幽蓝");
object.put("age", 9);
JSONArray array = new JSONArray();
array.add(object);
System.out.println(array.toJSONString());
return "index";
}

//结果
//[{"name": "幽蓝", "age": 9}]

实体类转换为JSON格式;produces = "application/json"表示服务器端返回了一个JSON格式的数据;@ResponseBody表示方法返回;@RestController表示返回字符串数据

1
2
3
4
5
6
7
8
9
@RequestMapping(value = "/index", produces = "application/json")
@ResponseBody
//@RestController
public String data(){
Student student = new Student();
student.setName("幽蓝");
student.setAge(9);
return JSON.toJSONString(student);
}

AJAX请求

html导入jquery框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
你好,
<span id="username"></span>
您的年龄是:
<span id="age"></span>
<button onclick="updateData()">点我更新页面数据</button>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script th:src="@{/static/test.js}"></script>
</body>
</html>

点击按钮,更新用户名和年龄

1
2
3
4
5
6
7
8
function updateData() {
//美元符.的方式来使用Ajax请求,这里使用的是get方式,第一个参数为请求的地址(注意需要带上Web应用程序名称),第二个参数为成功获取到数据的方法,data就是返回的数据内容
$.get("/mvc/data", function (data) { //获取成功执行的方法
window.alert('接受到异步请求数据:'+JSON.stringify(data)) //弹窗展示数据
$("#username").text(data.name) //这里使用了JQuery提供的选择器,直接选择id为username的元素,更新数据
$("#age").text(data.age)
})
}

js对象转换为JSON字符串的形式进行传输

1
2
3
4
5
6
7
8
9
10
11
function submitData() {
$.ajax({ //最基本的请求方式,需要自己设定一些参数
type: 'POST', //设定请求方法
url: "/mvc/submit", //请求地址
data: JSON.stringify({name: "测试", age: 18}), //转换为JSON字符串进行发送
success: function (data) {
window.alert(JSON.stringify(data))
},
contentType: "application/json" //请求头Content-Type一定要设定为JSON格式
})
}

@RequestBody读取JSON格式数据

1
2
3
4
5
6
@RequestMapping("/submit")
@ResponseBody
public String submit(@RequestBody JSONObject object){
System.out.println("接收到前端数据:"+object);
return "{\"success\": true}";
}

文件上传和下载

maven导入,接收用户上传的文件

1
2
3
4
5
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>

配置Resolver

1
2
3
4
5
6
7
@Bean("multipartResolver")   //注意这里Bean的名称是固定的,必须是multipartResolver
public CommonsMultipartResolver commonsMultipartResolver(){
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
resolver.setMaxUploadSize(1024 * 1024 * 10); //最大10MB大小
resolver.setDefaultEncoding("UTF-8"); //默认编码格式
return resolver;
}

上传功能

1
2
3
4
5
6
7
8
@RequestMapping(value = "/upload", method = RequestMethod.POST)
@ResponseBody
public String upload(@RequestParam CommonsMultipartFile file) throws IOException {
File fileObj = new File("test.html");
file.transferTo(fileObj);
System.out.println("用户上传的文件已保存到:"+fileObj.getAbsolutePath());
return "文件上传成功!";
}

上传页面

1
2
3
4
5
6
<div>
<form action="upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit">
</form>
</div>

下载功能

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping(value = "/download", method = RequestMethod.GET)
@ResponseBody
public void download(HttpServletResponse response){
response.setContentType("multipart/form-data");
try(OutputStream stream = response.getOutputStream();
InputStream inputStream = new FileInputStream("test.html")){
IOUtils.copy(inputStream, stream);
}catch (IOException e){
e.printStackTrace();
}
}

下载页面

1
<a href="download" download="test.html">下载最新资源</a>

SpringSecurity

项目需准备各种配置以及前端页面,为了省事只记录语法格式,最好实战了解

用户认证

配置用户验证

1
2
3
4
5
6
7
8
9
10
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); //这里使用SpringSecurity提供的BCryptPasswordEncoder
auth
.inMemoryAuthentication() //直接验证方式
.passwordEncoder(encoder) //密码加密器
.withUser("test") //用户名
.password(encoder.encode("123456")) //这里需要填写加密后的密码
.roles("user"); //用户的角色
}

数据库认证

创建一个Service实现,实现UserDetailsService,返回数据库中的用户名、密码等信息的UserDetails,SpringSecurity会自动进行比对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class UserAuthService implements UserDetailsService {
@Resource
UserMapper mapper;

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
String password = mapper.getPasswordByUsername(s); //从数据库根据用户名获取密码
if(password == null)
throw new UsernameNotFoundException("登录失败,用户名或密码错误!");
return User //这里需要返回UserDetails,SpringSecurity会根据给定的信息进行比对
.withUsername(s)
.password(password) //直接从数据库取的密码
.roles("user") //用户角色
.build();
}
}

数据库交互

1
2
3
4
5
@Mapper
public interface UserMapper {
@Select("select password from users where username = #{username}")
String getPasswordByUsername(String username);
}

自定义登录界面

配置拦截规则,用户未登录时,哪些路径可以访问,哪些路径不可以访问

1
2
3
4
5
6
7
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() //首先需要配置哪些请求会被拦截,哪些请求必须具有什么角色才能访问
.antMatchers("/static/**").permitAll() //静态资源,使用permitAll来运行任何人访问(注意一定要放在前面)
.antMatchers("/**").hasRole("user") //所有请求必须登陆并且是user角色才可以访问(不包含上面的静态资源)
}

如果不可以访问,那么会被自动重定向到登陆页面

1
2
3
4
5
.formLogin()       //配置Form表单登陆
.loginPage("/login") //登陆页面地址(GET)
.loginProcessingUrl("/doLogin") //form表单提交地址(POST)
.defaultSuccessUrl("/index") //登陆成功后跳转的页面,也可以通过Handler实现高度自定义
.permitAll() //登陆页面允许所有人访问

登出操作

1
2
3
4
.and()
.logout()
.logoutUrl("/logout") //退出登陆的请求地址
.logoutSuccessUrl("/login"); //退出后重定向的地址

学习过程,关闭CSRF防护

1
2
.and()
.csrf().disable();

授权方式

SpringSecurity两种授权方式:

  • 基于权限的授权:只要拥有某权限的用户,就可以访问某个路径
  • 基于角色的授权:根据用户属于哪个角色来决定是否可以访问某个路径

基于角色的授权

数据库添加用户字段,user是普通用户,admin是管理员用户

1
2
3
4
5
http
.authorizeRequests()
.antMatchers("/static/**").permitAll()
.antMatchers("/index").hasAnyRole("user", "admin") //index页面可以由user或admin访问
.anyRequest().hasRole("admin") //除了上面以外的所有内容,只能是admin访问

基于权限的授权

与角色类似,需要以hasAnyAuthority或hasAuthority进行判断

1
.anyRequest().hasAnyAuthority("page:index")

注解判断权限

Mvc的配置类上添加,这里只针对Controller进行过滤

1
2
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@PreAuthorize执行之前判断权限,如果没有对应的权限或是对应的角色,将无法访问页面

1
2
3
4
5
@PreAuthorize("hasRole('user')")   //判断是否为user角色,只有此角色才可以访问
@RequestMapping("/index")
public String index(){
return "index";
}

@PostAuthorize方法执行之后再进行拦截

1
2
3
4
5
6
@PostAuthorize("hasRole('user')")
@RequestMapping("/index")
public String index(){
System.out.println("执行了");
return "index";
}

Spring管理的Bean也同样起作用;注意Service是由根容器进行注册,在Security配置类上需添加@EnableGlobalMethodSecurity注解才生效

1
2
3
4
5
6
7
@Service
public class UserService {
@PreAuthorize("hasAnyRole('user')")
public void test(){
System.out.println("成功执行");
}
}

@PreFilter@PostFilter对集合类型的参数或返回值进行过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
@PreFilter("filterObject.equals('yyds')")   //filterObject代表集合中每个元素,只要满足条件的元素才会留下
public void test(List<String> list){
System.out.println("成功执行"+list);
}

@RequestMapping("/index")
public String index(){
List<String> list = new LinkedList<>();
list.add("hello");
list.add("yyds");
service.test(list);
return "index";
}

多个集合时,需要使用filterTarget指定

1
2
3
4
@PreFilter(value = "filterObject.equals('yyds')", filterTarget = "list2")
public void test(List<String> list, List<String> list2){
System.out.println("成功执行"+list);
}

记住我

已登录的浏览器分配一个携带Token的Cookie,默认14天,携带此Cookie访问服务器将无需登录

1
2
3
4
5
6
.and()
.rememberMe() //开启记住我功能
.rememberMeParameter("remember") //登陆请求表单中需要携带的参数,如果携带,那么本次登陆会被记住
.tokenRepository(new InMemoryTokenRepositoryImpl()) //这里使用的是直接在内存中保存的TokenRepository实现
//TokenRepository有很多种实现,InMemoryTokenRepositoryImpl直接基于Map实现的,缺点就是占内存、服务器重启后记住我功能将失效
//后面我们还会讲解如何使用数据库来持久化保存Token信息

Token信息存放到数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Resource
PersistentTokenRepository repository;

@Bean
public PersistentTokenRepository jdbcRepository(@Autowired DataSource dataSource){
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl(); //使用基于JDBC的实现
repository.setDataSource(dataSource); //配置数据源
repository.setCreateTableOnStartup(true); //启动时自动创建用于存储Token的表(建议第一次启动之后删除该行)
return repository;
}

.and()
.rememberMe()
.rememberMeParameter("remember")
.tokenRepository(repository)
.tokenValiditySeconds(60 * 60 * 24 * 7) //Token的有效时间(秒)默认为14天

SecurityContext

SecurityContext对象,获取当前用户的名称和授权信息

1
2
3
4
5
6
7
8
9
@RequestMapping("/index")
public String index(){
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
User user = (User) authentication.getPrincipal();
System.out.println(user.getUsername());
System.out.println(user.getAuthorities());
return "index";
}