image-20251117154216907

spring

核心概念

image-20251117154611993

image-20251117155539816

  • 目标:充分解耦
    • 使用IoC容器管理bean(IoC)
    • 在IoC容器内将有依赖关系的bean进行关系绑定(DI)
  • 最终效果
    • 使用对象时不仅可以直接从IoC容器中获取,并且获取到的bean已经绑定了所有的依赖关系

IoC入门案例思路分析

  1. 管理什么?(Service与Dao)
  2. 如何将被管理的对象告知IoC容器?(配置)
  3. 被管理的对象交给IoC容器,如何获取到IoC容器?(接口)
  4. IoC容器得到后,如何从容器中获取bean?(接口方法)
  5. 使用Spring导入哪些坐标?(pom.xm1)

先导入spring依赖,然后新建image-20251117160116095

这个就是spring的配置文件,取名applicationContext.xml

1
2
3
4
5
6
<!--1.导入spring的坐标spring-context,对应版本是5.2.10.RELEASE-->

<!--2.配置bean,id随便写,class对应要管理的对象-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>

<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl"/>
1
2
3
4
5
6
7
public class App2 {
public static void main(string[]args){
//3.获取ToC容器
Applicationcontext ctx = new classPathXmlApplicationcontext( configLocation: "applicationcontext.xml"),
//4.获取bean
BqokDao bookDao =(BookDao)ctx.getBean( s:"bookDao");
bookDao.save();

DI入门案例思路分析

  1. 基于IoC管理bean
  2. Service中使用new形式创建的Dao对象是否保留?(否)
  3. Service中需要的Dao对象如何进入到Service中?(提供方法)
  4. Service与Dao间的关系如何描述?(配置)
1
2
3
4
5
6
7
8
9
10
11
12
13
public class BookServiceImpl implements Bookservice {
//5.删除业务层中使用new的方式创建的dao对象
//private BookDao bookDao = hew BookDaoImpl();
private BookDao bookDao
public void save(){
System.out.println("book service save ...");
bookDao.save();
}
//6.提供对应的set方法
public void setBookDao(BookDao bookDao){
this.bookDao = bookDao;
}
}
1
2
3
4
5
6
7
<bean id="bookService" class="com,itheima.service.impl.BookServiceImpl">
<!--7.既置server与dao的关系-->
<!--property标签表小配置当前bean的属性
name届性表小配置哪一个具体的属性
ref属性表小参照哪-个bean-->
<property name="bookDao" ref="bookDao"/>
</bean>

bean

bean基础配置

image-20251117193854510

以下是新的bean配置文件,可以通过name增加别名,用逗号、分号和空格分隔,但还是推荐id起名

1
2
3
4
<bean id="bookservice" name="service service2 bookEbi" class="com.itheima.service.impl.BookseryiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>
<bean id="bookDao" name="dao" class="com.itheima.dao.impl.BookDaoImpl"/>

如果命名出现问题,就会出现NoSuchBeanDefinitionException Create breakpoint : No bean named ‘service4’ available的报错

image-20251117195050515

bean作用范围:默认创建出来的是同一个对象

image-20251117195227957

  • 为什么bean默认为单例 ?如果不是单例就会执行一次造一次对象,但其实可以用同一个对象(也就是可复用)
  • 适合交给容器进行管理的bean
    • 表现层对象
    • 业务层对象
    • 数据层对象
    • 工具对象
  • 不适合交给容器进行管理的bean
    • 封装实体的域对象

bean实例化

查看异常的方法是从下往上看,越往上越详细

方式有三种

一:构造方法(常用)

  • 提供可访问的构造方法
1
2
3
4
5
6
7
8
public class BookDaoImpl implements BookDao {
public BookDaoImpl(){
System.out.printIn("book constructor is running...");
}
public void save(){
System.out.println("book dao save ...");
}
}
  • 配置
1
2
3
<bean
id="bookDao"
class="com.itheima.dao.impl.BookDaoImpl"/>
  • 无参构造方法如果不存在,将抛出异常BeancreationException

二:使用静态工厂实例化bean(以前常用)

这里class造出来的是工厂对象,但我们需要的不是这个工厂,所以用后面那个配置工厂里面真正用来造对象的方法名

1
<bean id="orderDao" class="com.itheima.factory.OrderDaoFactory" Factory-method="getOrderDao"/>
1
2
3
4
//静态工厂
public class OrderDaoFactory {
public static orderDao getOrderDao(){
return new OrderDaoImpl();

三:使用实例工厂实例化bean(了解)

1
2
3
<!--先配出工厂,再把工厂id放进下面bean里,实例方法写method-->
<bean id="userFactory" class="com.itheima.factory.UserDaoFactory"/>
<bean id="userDao" factory-method="getUserDao" factory-bean="userFactory"/>
1
2
3
4
5
public class AppForInstanceUser {
public static void main(string[] args){
Applicationcontext ctx = new classPathXmlApplicationContext("applicationcontext.xml");
UserDao userDao=(UserDao)ctx.getBean("userDao");
userDao.save();

四:使用FactoryBean实例化bean(重点)

1
<bean id="userDao" class="com.itheima.factory.UserDaoFactoryBean"/>
1
2
3
4
5
6
7
8
9
10
11
12
//利用泛型来获取对象
public class UserDaoFactoryBean implements FactoryBean<UserDao> {
//代替原始实例工厂中创建对象的方法,造出来的是UserDao
public UserDao getobject()throws Exception {
return new UserDaoImpl();
public class<?>getobjectType(){
return UserDao.class;
}
//如果是true就是单例(默认,输出内存地址一样),false变成非单例
public boolean issingleton(){
return false;
}

bean生命周期

  • 生命周期:从创建到消亡的完整过程
  • bean生命周期:bean从创建到销毁的整体过程
  • bean生命周期控制:在bean创建后到销毁前做一些事情

管理生命周期通过java代码有以下两种,但其实可以交给Tomcat管理,所以是白学

1
<bean id="bookDao" class="com.itheima.dao.imp1.BookDaoImp1" init-method="init" dastroy-method="destory"/>
1
2
3
4
5
6
7
8
9
10
11
public class AppForLifecycle {
public static void main(string[]args ){
//Applicationcontext没有close这个方法,但子类有,就改成它的子类
//Applicationcontext ctx = new ClassPathXmlApplicationContext( configLocation: "applicationcontext.xm1")
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext( configLocation: "applicationcontext.xm1")
BookDao bookDao=(BookDao)ctx.getBean(s:"bookDao");
bookDao.save();
//这个方法是直接关闭容器,比较暴力,位置改动会影响结果
ctx.close();
//注册关闭钩子,容器在启动之后如果虚拟机要关闭就要先把容器关闭,位置改动不影响结果
ctx.registerShutdownHook();

也可以通过实现接口的方式管理

1
2
3
4
5
6
7
8
9
10
public class BookServiceImpl implements BookService, InitializingBean ,DisposableBean {
//然后实现里面的方法就行了
public void destroy() throws Exception {
System.out.println("destroy");
}
//这个方法的意思是属性设置之后
public void afterPropertiesSet() throws Exception {
System.out.println("afterPropertiesset");
}

bean在整个初始化过程中经历的阶段:

  • 初始化容器
  1. 创建对象(内存分配)
  2. 执行构造方法
  3. 执行属性注入(set操作 )
  4. 执行bean初始化方法
  • 使用bean
  1. 执行业务操作
  • 关闭/销毁容器
  1. 执行bean销毁方法

bean销毁时机

  • 容器关闭前触发bean的销毁

  • 关闭容器方式

    • 手工关闭容器:

      ConfigurableApplicationcontext接close()操作

    • 注册关闭钩子,在虚拟机退出前先关闭容器再退出虚拟机:

      ConfigurableApplicationcontext接registerShutdownHook()操作

依赖注入方式

  • 思考:向一个类中传递数据的方式有几种 ?
    • 普通方法(set方法)
    • 构造方法
  • 思考:依赖注入描述了在容器中建立bean与bean之间依赖关系的过程,如果bean运行需要的是数字或字符串呢 ?
    • 引用类型
    • 简单类型(基本数据类型与string
  • 依赖注入方式
    • setter注入
      • 简单类型
      • 引用类型
    • 构造器注入
      • 简单类型
      • 引用类型

setter引用类型

在bean中定义引用类型属性并提供可访问的set方法

1
2
3
4
5
public class BookServiceImpl implements BookService{
private BookDao bookDao;
public void setBookDao(BookDao bookDao){
this.bookDao = bookDao;
}

配置中使用property标签ref属性注入引用类型对象

1
2
3
4
5
<!--ref后面的是下面的bean名称,用作引用bean,如果是基本类型用value,name是java里的-->
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>

setter简单类型

在bean中定义引用类型(基本数据类型也可以)属性并提供可访问的set方法

1
2
3
4
5
6
7
8
9
public class BookDaoImpl implements BookDao
private int connectionNum;
private String databaseName;
public void setConnectionNum(int connectionNum){
this.connectionNum = connectionNum;
}
public void setDatabaseName(string databaseName)
this.databaseName =databaseName;
}

配置中使用property标签value属性注入简单类型数据

1
2
3
4
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl".>
<property name="databaseName" value="mysql"/>
<property name="connectionNum" value="10"/>
</bean>

构造器注入-简单类型(了解)

1
2
3
4
5
6
7
public class BookDaoImpl implements BookDao {
private string databaseName;
private int connectionNum;
public BookDaoImpl(string database,int connectionNum){
this.databaseName =database;
this.connectionNum = connectionNum;
}

这里的property位置换成了constructor-arg,一般了解这个就行

1
2
3
4
5
6
7
8
9
10
<!--标准书写-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl”>
<constructor-arg name="connectionNumvalue=”10"/>
<constructor-arg name="databaseName" value="mysqL"/>
</bean>
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<constructor-arg name="userDao" ref="userDao"/>
<constructor-arg name="bookDao" ref="bookDao"/>
</bean>

把构造方法名改成了直接写类型,但是如果有重复类型就会出问题

1
2
3
4
5
<!--解决形参名称的问题,与形参名不合-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<constructor-arg type="int” value="10"/>
<constructor-arg type="java.lang.String" value="mysql"/>
</bean>

解决参数类型重复问题,使用下标位置解决参数匹配

1
2
3
4
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<constructor-arg index="0" value="10"/>
<constructor-arg index="1" value="mysql"/>
</bean>

依赖注入方式选择:

  1. 强制依赖(bean运行必须要的东西)使用构造器进行,使用setter注入有概率不进行注入导致null对象出现
  2. 可选依赖使用setter注入进行,灵活性强
  3. Spring框架倡导使用构造器,第三方框架内部大多数采用构造器注入的形式进行数据初始化,相对严谨
  4. 如果有必要可以两者同时使用,使用构造器注入完成强制依赖的注入,使用setter注入完成可选依赖的注入
  5. 实际开发过程中还要根据实际情况分析,如果受控对象没有提供setter方法就必须使用构造器注入
  6. 自己开发的模块推荐使用setter注入

自动装配

  • IoC容器根据bean所依赖的资源在容器中自动查找并注入到bean中的过程称为自动装配
  • 自动装配方式
    • 按类型(常用)
    • 按名称
    • 按构造方法
    • 不启用自动装配

这里用按类型配置

1
2
3
4
5
public class BookserviceImpl implements BookService{
private BookDao bookDao;
//这个set方法不可以省略
public void setBookDao(BookDao bookDao){
this.bookDao = bookDao;
1
2
3
4
<!--这里的dao也需要是唯一的,如果有多个就可以使用byName-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<!--autowire里有五种,按需使用-->
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl" autowire="byType"/>

依赖自动装配特征:

  • 自动装配用于引用类型依赖注入,不能对简单类型进行操作(简单类型比如数字这种,就不需要注入bean了)
  • 使用按类型装配时(byType)必须保障容器中相同类型的bean唯一,推荐使用
  • 使用按名称装配时(byName)必须保障容器中具有指定名称的bean,因变量名与配置耦合,不推荐使用
  • 自动装配优先级低于setter注入与构造器注入,同时出现时自动装配配置失效

集合注入

就学个格式

1
2
3
4
5
6
7
8
public class BookDaoImpl implements BookDao {
private int[] array;
private List<string> list;
private Set<string> set;
private Map<string,string> map;
private Properties properties;

public void setArray(int[]array){this.array = array; }
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="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<property name="array">
<array>
<value>xxx</value>
</array>
</property>
<property name="list">
<list>
<value>xxx</value>
</list>
</property>
<property name="set">
<!--重复的值会自动过滤-->
<set>
<value>xxx</value>
</set>
</property>
<property name="map">
<!--重复的值会自动过滤-->
<map>
<entry key="x" value="x"/>
</map>
</property>
<property name="properties">
<!--重复的值会自动过滤-->
<props>
<prop key="x">x</prop>
</props>
</property>

案例:数据源对象管理

管理第三方的bean

思考用构造方法还是set方法注入,这个的话得看这个类是怎么写的,这里是找到了set相关的方法

1
2
3
4
5
6
7
<!--管理DruidDataSource对象-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverclassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://1ocalhost:3306/spring_db"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>

随便输出一下

1
2
3
4
5
6
public class App {
public static void main(string[l args){
Applicationcontext ctx = new classPathXmlApplicationcontext( configLocation: "applicationcontext.xml");
DataSource dataSource =(DataSource)ctx.getBean( s:"dataSource");
System.out.println(dataSource);
}

加载properties文件

第一步开启context命名空间,下面的context就是新加的命名空间

第二步使用context空问加载properties文件

第三步使用属性占位待${}读取properties 文件中的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--第一步开启context命名空间,下面的context就是新加的命名空间-->
<beans xmIns="http://www.springframework.org/schema/beans'
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
下面这个xmlns:就是开了一个新的namespace命名空间叫做context
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">
<!--2.使用context空问加载properties文件-->
这个叫属性占位符,location是指定要加载的文件
<context:property-placeholder location="jdbc.properties"/>
<!--3.使用属性占位待${}读取properties 文件中的属性-->
<bean class="com.alibaba.druid.pool.DruidDataSpurce">
<property name="driverclassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>

环境变量问题:

​ 有个需要注意的是如果value里的占位符改成username会出问题,这是因为系统中有一个环境变量叫username,会和这里要读取的username冲突,系统的环境变量比这里的优先级高

1
2
3
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<property name="name" value="${username}"/>
</bean>

解决方法是在上面的context里加个参数就不会加载环境变量了

1
2
<context:property-placeholder location="jdbc.properties"
system-properties-mode="NEVER"/>

如何加载多个配置文件:

  • 方式一(不理想):在location的里面用逗号分隔添加多个
1
2
<context:property-placeholder location="jdbc.properties,jdbc2.properties"
system-properties-mode="NEVER"/>
  • 方式二(规范格式):名称用*代替,加载所有配置文件,在前面加上类路径,但是只能读取当前工程里的配置文件
1
2
<context:property-placeholder location="calsspath:*.properties"
system-properties-mode="NEVER"/>
  • 方式三(标准):calsspath后面再加一个星号,这样可以从依赖的jar包读,也可以从自己的工程读
1
2
<context:property-placeholder location="calsspath*:*.properties"
system-properties-mode="NEVER"/>

容器

加载配置文件有两种方法:

1.加载类路径下的配置文件(用这个)

1
Applicationcontext ctx = new classPathXmlApplicationcortext("applicationcontext.xml");

2.从文件系统下加载配置文件,用的绝对路径

1
Applicationcontext ctx = new classPathXmlApplicationcortext("D:\\xxxx\\xxx.xml");

3.加载多个配置文件

1
Applicationcontext ctx = new classPathXmlApplicationcontext("bean1.xml", "bean2.xml");

获取bean:

1.利用bean名称和强制转换

1
BookDao bookDao=(BookDao)ctx.getBean( "bookDao");

2.前面获取bean的名称后面获取bean的类型,以上两种没有什么区别

1
BookDao bookDao = ctx.getBean("bookDao",BookDao.class);

3.按照类型获取bean,但是这种有个问题就是容器中对应的bean只能有一个,如果有多个bean就会不唯一

1
BookDao bookDao = ctx.getBean(BookDao.class);

image-20251122183336605

这个是最早期的容器初始化方案(已过时)

1
2
3
4
5
6
7
public class AppForBeanFactory{
public static void main(string[] args){
//获取资源文件
Resource resources = new classPathResource("applicationcontext.xml"),
BeanFactory bf=new XmlBeanFactory(resources);
BookDao bookDao=bf.getBean(BookDao.class);
bookDao.save();

BeanFactory和applicationcontext的区别:BeanFactory创建完毕后,所有的bean是延迟加载,applicationcontext是立即加载,如果applicationcontext也想延迟加载可以在配置文件里添加lazy-init

1
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl" lazy-init='true"/>

容器相关

  • BeanFactory是IoC容器的顶层接口,初始化BeanFactory对象时,加载的bean延迟加载
  • Applicationcontext接口是Spring容器的核心接口,初始化时bean立即加载
  • Applicationcontext接口提供基础的bean操作相关方法,通过其他接口扩展其功能
  • Applicationcontext接口常用初始化类
    • ClassPathXmlApplicationContext
    • FileSystemXmlApplicationContext(几乎不用)

image-20251122205935752

image-20251122211918710

注解开发

示例:这里的**@Component**是组件的意思,代表的是xml文件中的,后面就是bean的名字,这个名字可以默认不写,默认的话就是空的。加上这个注解,在xml中的标签就可以不用写了

1
2
3
4
5
6
7
@Component("bookDao")
public class BookDaoImpl implements BookDao {
publis void save(){
System.out.println("book dao save ...");

有bean名用BookDao)ctx.getBean(s:"bookDao");
无bean名用ctx.getBean(BookService.class);

但使用了这个注解也需要spring知道这个,就需要在xml里配置,context:component-scan用于扫描,base-package是指定的位置

1
2
<context:component-scan base-package="com.itheima.dao.impl"/>
com.itheima也可以,这样扫描的就是itheima下所有的

注解开发bean

  • Spring提供@Component注解的三个衍生注解
    • @Controller:用于表现层bean定义
    • @service:用于业务层bean定义
    • @Repository:用于数据层bean定义

纯注解开发

  • Spring3.0升级了纯注解开发模式,使用Java类替代配置文件,开启了Spring快速开发赛道

也就是不写配置文件了,换个形式,新建一个config包下的SpringConfig类来代替配置文件,只需要用**@Configuration**就可以代表这个是配置类了,代表的是下图这些

image-20251122215255200

然后用@ComponentScan代替,但是要带上包的路径

1
<context:component-scan base-package="com.itheima"/>

替换完是这样的

1
2
3
@Configuration
@ComponentScan("com.itheima")
public class springconfig {}

但用注解的话启动程序就也要跟着改,因为原本是需要配置文件来启动的,新建一个AppForAnnotation,new的接口不一样了,剩下的都一样

1
2
3
4
5
6
7
8
public class AppForAnnotation {
public static void main(string[] args){
//换了个接口new,意思是注解配置容器,括号里是配置类
ApplicationContext ctx = new AnnotationConfigApplicationcontext(springconfig.class)
BookDao bookDao = (BookDao)ctx.getBean("bookDao");
System.out.println(bookDao);
BookService bookService = ctx.getBean(BookService.class);
System.out.println(bookService);
  • 读取Spring核心配置文件初始化容器对象切换为读取Java配置类初始化容器对象

    1
    2
    3
    4
    //加载配置文件初始化容器
    ApplicationContext ctx= new ClassPathXmlApplicationContext("applicationcontext.xml");
    //加载配置类初始化容器
    ApplicationContext ctx= new AnnotationConfigApplicationContext(SpringConfig.class);
  • @Configuration注解用于设定当前类为配置类

  • @Componentscan注解用于设定扫描路径,此注解只能添加一次,多个数据请用数组格式

1
@Componentscan({"com.itheima.service","com.itheima.dao"})

bean管理和生命周期

注解@Scope(“singleton”)就可以让bean变成单例,prototype是非单例

1
2
3
4
5
6
7
8
9
10
11
12
13
@Repository
@scope("singleton")
public class BookDaoImpl implements BookDao {
public void save(){
System.out.println("book dao save ...");
}
@PostConstruct//这个意思是构造方法之后运行
public void init(){
System.out.println("init ...");
}
@PreDestroy//这个是彻底销毁前运行的方法
public void destroy(){
System.out.println("destroy ...");

上面的两个注解弹幕说java8之后就不用了,如果执行后没有销毁,就是需要在运行方法那里使用关闭容器的方式来做,ctx.close();

依赖注入

比如说下面这个程序是存在dao没有注入成功的问题,需要@Autowired这个注解来注入,这个是按类型装配

1
2
3
4
5
6
7
8
9
10
11
@Service
public class BookserviceImpl implements Bookservice {
@Autowired
private BookDao bookDao;

public void setBookDao(BookDao bookDao){
this.bookDao = bookDao;
}
public void save(){
System.out.println("book service save ...");
bookDao.save();

然后发现这个@Autowired注解放在哪里都不影响结果,所以这个set方法不要了也没问题,这个是使用反射里边的暴力反射给它直接加值了

如果是多个dao那就用按名称装配,之前的dao类里的@Repository注解后面加上名字比如(“bookDao”),这样用@Autowired注入bookDao时就按照上面的名字去寻找了。但这种有多个相同的bean推荐使用@Qualifier来指定名称,想注入谁就写谁,并且@Autowired不能删除。

1
2
3
@Autowired
@Qualifier("bookDao2")//找dao类里有@Repository("bookDao2")这个注解的
private BookDao bookDao;
  • 使用@Autowired注解开启自动装配模式(按类型)

  • 注意:自动装配基于反射设计创建对象并暴力反射对应属性为私有属性初始化数据,因此无需提供setter方法

  • 注意:自动装配建议使用无参构造方法创建对象(默认),如果不提供对应构造方法,请提供唯一的构造方法

  • 使用@Qualifier注解开启指定名称装配bean

  • 注意:@Qualifier注解无法单独使用,必须配合@Autowired注解使用

简单类型怎么用?@Value

1
2
3
4
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
@Value("itheima666")
private string name;//这样就把itheima666注入到name里了

如果这个value值是从外部的properties文件获取,应该怎么获取

1
name=123
1
2
3
4
5
6
7
@Configuration
@ComponentScan("com.itheima")
@PropertySource("jdbc.properties")//属性源
//多个的写法@PropertySource({"jdbc.properties","jdbc.properties"})
public class springconfig {}

上面的value后面改成("${name}")就行了

注意配置文件的名字不支持通配符*

第三方bean管理

  • 使用@Bean配置第三方bean

​ 这个springconfig里就不放那个扫描注解了,这里不能直接把配置写在人家的源代码里,所以要自己写,但这些三方配置都写这一个文件里就太多了,所以要拆出去

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class SpringConfig {
//1.定义一个方法获得要管理的对象
//2.添加@Bean,表示当前方法的返回值是一个bean
@Bean
public Datasource datasource(){
DruidDataSource ds=new DruidDatasource();
ds.setDriverclassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysq1://localhost:3306/spring_db");
ds.setUsername("root");
ds.setPassword("root");
return ds;

拆出去一般就是专门的类管理,使用独立的配置类管理第三方bean,比如jdbcConfig,然后把上面的代码拿到jdbcConfig里面,但是这样会导致springconfig识别不到,解决方法有两种

  1. 扫描式(不推荐使用),把这个类也加上@Configuration,然后在SpringConfig里加上扫包注解@ComponentScan,不推荐的原因是下面这个类也写了@Configuration,而且SpringConfig看不出到底加载了哪些配置类
1
2
3
4
5
6
7
8
@Configuration
public class JdbcConfig {

@Bean
public Datasource datasource(){
DruidDataSource ds=new DruidDatasource();
ds.setDriverclassName("com.mysql.jdbc.Driver");
~~~
  1. 导入式,使用@Import注解手动加入配置类到核心配置,此注解只能添加一次,多个数据请用数组格式
1
2
3
@Configuration
@Import(JdbcConfig.java)多个就是({xxx.java,xxx.java})
public class SpringConfig {

为第三方bean注入资源

对于简单类型的方式:使用@Value

1
2
3
4
5
public class Jdbcconfig{
@Value("com.mysql.jdbc.Driver")
private string driver;
@Value("jdbc:mysql:/localhost:3306/spring_db")
private string url;
@Bean
public Datasource dataSource(){
	DruidDatasource ds=new DruidDatasource();
	ds.setDriverclassName(driver);
	ds.setUr1(ur1);
 	
1
2
3
4
5
6
7
8
9
10
对于引用类型:引用类型注入只需要为bean定义方法设置形参即可,容器会根据类型自动装配对象

假设运行需要dao,dao类写上@Repository(不写会出现NoSuchBeanDefinitionException异常),然后SpringConfig类是下面的这样,JdbcConfig里只加上个形参就可以,这是因为自动装配

~~~java
public class JdbcConfig{
@Value("com.mysql.jdbc.Driver")
private string driver;
@Value("jdbc:mysql:/localhost:3306/spring_db")
private string url;
@Bean public Datasource dataSource(BookDao bookDao){//这里多了个形参
1
2
3
4
@Configuration
@ComponentScan("com.itheima.dao" )//扫描
@Import(JdbcConfig.class)
public class SpringConfig {

XML配置与注解配置比较

image-20251124031607539

Spring整合MyBatis

  • MyBatis程序核心对象分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class App{
public static void main(string[]args)throws IOException {
//1.创建SqlSessionFactoryBuilder对象
SqlSessionfactoryBuilder sqlSessionFactoryBuilder = new sqlsessionfactoryBuilder(),
//2.加载SqlMapconfig.xml配置文件
InputStream inputStream = Resources.getResourceAsStream("SqlMapCconfig.xml");
//3.创建sqlsessionFactory对象
//获取连接,获取实现
SqlSessionfactory sqlsessionFactory = sqlsessionfactoryBuilder.build(inputstream);
// 4.获取sqlSession
SqlSession sqlSession=sqlsessionFactory.opensession();
//5.执行SqlSession对象执行查询,获取结果User
//获取数据层接口
AccountDao accountDao =sqlsession.getMapper(AccountDao.class);
Account ac = accountDao.findById(2);
System.out.println(ac);
// 6 、释放资源
sqlsession.close();

依赖要添加两个新的

第一个包是spring里操作数据库专用的包

第二个包时spring整合mybatis用的包,spring提供接口规范,让被整合的按照标准开发

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>

创建JdbcConfig(三方bean的管理格式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//@Configuration不使用这个注解被扫描是因为可读性差,就需要手动导入
public class Jdbcconfig{
@Value("${jdbc.driver}")
private string driver;
@Value("${jdbc.url}")
private string url;
@Value("${jdbc.username}")
private string userName;
@Value("${jdbc.password}")
private string password;

@Bean
public Datasource datasource(){
DruidDatasource ds = new DruidDatasource();
ds.setDriverClassName(driver);
ds.setUr1(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}

创建SpringConfig

1
2
3
4
5
6
7
@Configuration
@Componentscan("com.itheima")
@ropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})//手动导入
public class SpringConfig {

}

写个专门做mybatis的配置类,里面写上sqlsessionfactorybean,然后在springconfig里导入。通过创建这个类来获取factory,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
public class MybatisConfig{
//使用@bean想注入引用类型直接在括号的形参加上对应参数就可以
@Bean
public SqlsessionFactoryBean sqlsessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
//代替了package name
ssfb.setTypeAliasesPackage("com.itheima.domain");
//environments就不要了
//代替了datasource。spring容器里有dataSource
ssfb.setDataSource(dataSource);
//还有个transactionManager事务处理,暂时忽略
return ssfb;
}

//还有个mapper标签,用来造dao实现类的
//这个也属于mybatis整合spring包里的,映射扫描配置器
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
//设置映射的包路径
msc.setBasePackage("com.itheima.dao")
return msc;
}

以上的只需要改包路径就可以,之前的springConfig.xml就不要了,但之前的运行方式就不能要了,新建一个app2

1
2
3
4
5
6
7
8
public class App2 {
public static void main(string[] args){
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
//这里getbean注意不需要拿dao,因为dao已经注入到service里了,所以直接用service的类
AccountService accountService = ctx.getBean(AccountService.class);
Account ac = accountService.findById(1);
System.out.println(ac);//查到数据
}

spring整合JUnit

使用spring整合结构的这种方式来运行程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//1.设定类运行器
//2.设置上下文配置,配置类用calsses
//3.要测谁直接注入就行
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest{
@Autowired
private AccountService accountService;

@Test
public void testFindById(){
System.out.println(accountservice.findById(1));

@Test
public void testFindA1l(){
System.out.println(accountService.findAll());
}

AOP

简介

  • AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构
    • 00P(0bject Oriented Programming)面向对象编程
  • 作用:在不惊动原始设计的基础上为其进行功能增强
  • Spring理念:无入侵式/无侵入式

想让蓝色部分在别的方法也拥有,就把蓝色部分抽取出来做一个单独的方法,然后原始save方法起名叫连接点;对追加功能的方法叫做切入点,这个切入点就说明了哪些方法要追加功能;让大家都有的方法是一组共性的功能,起名叫通知,但怎么就知道要在update和delete上执行这个通知呢,看来在通知和切入点之间还得有个东西把他俩绑定到一块,这样的话一个通知就对应一个切入点,这个东西叫做切面,切面描述的是这个通知的共性功能与对应的切入点的关系。然后在java中的方法需要依托通知类,不能独立存在

image-20251216184936360

了解完后整体的流程就是:首先找到程序中间的共性功能,写一个通知类,在通知类中定义一个方法,这个方法叫通知,这个方法里放的是共性功能 ,把需要执行对应通知的方法找出定义成切入点,把切入点和通知绑定就得到了切面,切面描述的是在哪个切入点执行哪些通知

  • 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
    • 在SpringAOP中,理解为方法的执行
  • 切入点(Pointcut):匹配连接点的式子
    • 在SpringAoP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
      • 一个具体方法:com.itheima.dao包下的BookDao接口中的无形参无返回值的save方法
      • 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
  • 通知(Advice):在切入点处执行的操作,也就是共性功能
    • 在springA0P中,功能最终以方法的形式呈现
  • 通知类:定义通知的类
  • 切面(Aspect):描述通知与切入点的对应关系

切入点范围小,连接点范围大,切入点一定在连接点中

AOP入门案例

设定:在接口执行前输出当前系统时间
开发模式:XML or 注解

思路分析:

  1. 导入坐标(pom.xml)
  2. 制作连接点方法(原始操作,Dao接口与实现类)
  3. 制作共性功能(通知类与通知)
  4. 定义切入点
  5. 绑定切入点与通知关系(切面 )

导入坐标的话导入spring-context,aop也会被自动导入,再导入一个aspectj

第二步是制作连接点方法

1
2
3
4
5
6
7
@Repository
public class BookDaoImpl implements BookDao {
public void save(){
System.out.println(system.currentTimeMillis());
System.out.println("book dao save ...
public void update(){
System.out.println("book dao update ..."

第三步是把共性功能抽出做新的类,里面写个方法叫method,这就是共性功能

第四步定义切入点,写个私有方法,加上@Pointcut,里面的execution是执行的意思,括号里是返回值void,类全限定名的update方法有没有参数。说明:切入点定义依托一个不具有实际意义的方法进行,即无参数,无返回值,方法体无实际逻辑

第五步,让method方法在切入点的什么位置执行呢,如果是前面就用@before,里面是方法名

第六步加上@Component让它受springbean控制,加上@Aspect让spring知道这里是aop

1
2
3
4
5
6
7
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}

@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());

但现在spring不知道是用注解开发的aop,所以要在配置类加个@EnableAspectJAutoProxy告诉spring这里面有用注解开发的aop,也就是启动了@Aspect去识别里面的东西

1
2
3
4
5
6
@Configuration
@Componentscan("com.itheima")
@EnableAspectJAutoProxy
public class springcinfig{

}

AOP工作流程

  1. Spring容器启动

  2. 读取所有切面配置中的切入点(配置在@Before的才会生效)

  3. 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点

    ​ 如果匹配失败,就创建对象

    ​ 匹配成功,创建原始对象(目标对象)代理对象,代理就表示可以用代理对象调用对应方法走增强的那些操作

  4. 获取bean执行方法

    ​ 获取bean,拿上面匹配失败创建的对象去调用方法并执行,完成操作

    ​ 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作,这个也就是aop用的是代理模式

核心概念:

  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现

没有开启aop的时候打印对象和class类:

com.itheima.dao.impl.BookDaoImpl@279fedbd
class com.itheima.dao.impl.BookDaoImpl

开启aop的时候打印对象和class类:

com.itheima.dao.impl.BookDaoImpl@4e50c791
class com.sun.proxy.$Proxy19

因为aop会对最终的对象的toString方法进行重写,直接打印对象比如System.out.println(bookDao);就会带来一些误导

AOP切入点表达式

  • 切入点:要进行增强的方法
  • 切入点表达式:要进行增强的方法的描述方式

比如现在有两个方法

1
2
3
4
5
6
7
8
9
public interface BookDao {
public void update();
}

public class BookDaoImpl implements BookDao {
public void update(){
System.out.println("book dao update ...");
}
}
  • 描述方式一:执行com.itheima.dao包下的BookDao接口中的无参数update方法
    • execution(void com.itheima.dao.BookDao.update())
  • 描述方式二:执行com.itheima.dao.impl包下的BookDaoImpl类中的无参数update方法
    • execution(void com.itheima.dao.impl.BookDaoImpl.update())

上面两种方式都可以,描述接口或者描述实现类

切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名 )

1
execution (public User com.itheima.service.UserService.findById (int))
  • 动作关键字:描述切入点的行为动作,例如execution表示执行到指定切入点,这个词几乎不动
  • 访问修饰符:public,private等,可以省略
  • 返回值
  • 包名
  • 类/接口名
  • 方法名
  • 参数
  • 异常名:方法定义中抛出指定异常,可以省略,忽略就是public

可以使用通配符描述切入点,快速描述

  • 单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

    1
    2
    //第一个*是不限制返回类型,第二个是任意包的意思,第三个是范围匹配,第四个是任意参数
    execution(public * com.itheima.*.UserService.find* (*))

    ​ 匹配com.itheima包下的任意包中的UserService类或接口中所有find开头的(必须)带有一个参数的方法

  • ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

    1
    execution(public User com..UserService.findById(..))

    意思是匹配com包下的任意包中的UserService类或接口中所有名称为findByld的任意方法

  • +:专用于匹配子类类型

    1
    execution(* *..*Service+.*(..))

    任意返回值,任意包下面以Service结尾的子类的任意方法任意参数,括号里的是*就必须有参数才行

  • 书写技巧:

    • 所有代码按照标准规范开发,否则以下技巧全部失效
    • 描述切入点通常描述接口,而不描述实现类,因为描述实现类就紧耦合了
    • 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述
    • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
    • 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
    • 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
    • 方法名书写以动词进行精准匹配,名词采用*匹配,例如getByld书写成getBy*,selectAl书写成selectAll
    • 参数规则较为复杂,根据业务方法灵活调整
    • 通常不使用异常作为匹配规则

AOP通知类型

  • AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
  • AOP通知共分为5种类型
    • 前置通知
    • 后置通知
    • 环绕通知(重点)
    • 返回后通知(了解 )
    • 抛出异常后通知(了解)

演示:准备以下代码

1
2
3
4
public interface BookDao {
public void update();
puplic iht select();
}
1
2
3
4
5
6
7
8
9
10
11
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}

@Before("pt()")//前置通知
public void before(){ system.out.println("before advice ...");}

@After("pt()")//后置通知
public void after(){ system.out.println("after advice ...");}

环绕通知这个直接输出的话会输出
around before advice …
around after advice …,原方法没有执行,所以必须要加个东西表示对原始操作的调用,方法是在around的括号里加上ProceedingJoinPoint参数,取名pjp,然后用这个调用proceed,这个就是代表对原始操作的调用,这里有红线因为原始操作无法预期是否有异常,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.itheima.dao.BookDao.update())")
private void pt(){}

@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2(){}

@Around("pt()")//环绕通知
public void around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("around before advice ...");
//表示对原始操作的调用
pjp.proceed();
System.out.println("around after advice ...");
}

但如果新弄一个select的,运行时会抛出异常AopInvocationException,后面的英文意思是空的返回值从这个advance中出来了,它不匹配原始操作调用的返回值类型,原始操作返回的是int,所以要让最后能有个返回值给抛出去,返回类型用object。原始操作其实调用的是impl方法,里面return的是100,那么这100哪去了? pjp.proceed();其实有个返回值,这个返回值就是原本的100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   @Around("pt2()")
//public void aroundSelect(ProceedingJoinPoint pjp) throws Throwable{要有返回值
public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("around before advice ...");
//表示对原始操作的调用
//pjp.proceed();
Object proceed = pjp.proceed();
//如果返回值要加点什么,就要强转类型,因为object不能做加法
//Integer ret = (Integer)pjp.proceed;
System.out.println("around after advice ...");
//return 200;
return ret;
//return ret+666;
以上是标准写法
}

下面代表原始方法执行成功后执行这个动作,了解即可

1
2
3
4
@AfterReturning("pt2()")
public void afterReturning(){
system.out.println("afterReturning advice ...");
}

下面是抛出异常后才运行

1
2
3
@AfterThorwing("pt2()")
public void afterThrowing(){
system.out.println("afterThrowing advice ...");}

@Around注意事项

  1. 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
  2. 通知中如果未使用ProceedingjoinPoint对原始方法进行调用将跳过原始方法的执行,会产生一种对原始操作进行隔离的效果,比如做权限校验
  3. 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,必须设定为Obiect类型
  4. 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
  5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象

案例:测量业务层接口万次执行效率

需求:任意业务层接口执行均可显示其执行效率(执行时长)
分析:

  1. 业务功能:业务层接口执行前后分别记录时间,求差值得到执行效率
  2. 通知类型选择前后均可以增强的类型——环绕通知

image-20251220160656006

准备以上数据

业务层

1
2
3
4
5
6
public interface Accountservice{
void save(Account account);
void delete(Integer id);
void update(Account account);
List<Account> findA11();
Account findById(Integer id);

第一步,先在springConfig里把aop注解开启

1
2
3
4
5
6
@Configuration
@Componentscan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({Jdbcconfig.class,MybatisConfig.class})
@EnableAspectJAutoProxy//开启
public class springconfig {}

然后创建aop包,新建ProjectAdvice类,我们是对业务层接口做监控所以起名也是这个,返回值不好说是什么,所以用通配,方法是任意方法,参数是什么都行。然后用循环去执行原始方法,最后打印结果,但这个并不能用,因为并不知道执行时间是哪个具体的方法,而ProceedingJoinPoint描述的是原来那个方法的执行对象,通过这个拿到执行的签名信息

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
@Component//受spring管理
@Aspect//切面类
public class ProjectAdvice{
//匹配业务层所有方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt(){}

@Around("ProjectAdvice.servicePt()")
public void runspeed(ProceedingJoinPoint pjp) throws Throwable{
//通过这个拿到执行的签名信息
Signature signature = pjp.getsignature();
//System.out.println(signature.getDeclaringType());//打印类型interface com.itheima.service.AccountService
//System.out.println(signature.getName());//打印方法名findById
//System.out.println(signature.getDeclaringTypeName());//打印类型名com.itheima.service.AccountService
//改进
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();

long start=system.currentTimeMillis();
for(int i = 0;i < 10000; i++){
pjp.proceed();
}
long end = System.currentTimeMillis();
//System.out.println("业务层接口万次执行时间:["+(end-start)+"ms");//注意要加括号,否则是字符串拼接
//return ret;但是返回哪一遍都不合理,所以直接干掉,方法返回值变成void
//改进
System.out.println("万次执行:"+ className +"."+ methodName +"---->" +(end-start)+ "ms");
}
}

补充说明:当前测试的接口执行效率仅仅是一个理论值,并不是一次完整的执行过程

AOP通知获取数据

  • 获取切入点方法的参数
    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知(每个都有)
    • ProceedJointPoint:适用于around环绕通知
  • 获取切入点方法返回值
    • 返回后通知(afterReturning)
    • 环绕通知(around)
  • 获取切入点方法运行异常信息
    • 抛出异常后通知
    • 环绕通知

image-20251221024909688

这个参数是pjp的父

1
2
3
4
5
6
@Before("pt()")
public void before(JoinPoint jp){
object[] args = jp.getArgs();
System.out.println(Arrays.tostring(args));
System.out.println("before advice ...");
}

然后在bookdao添加参数

1
2
3
public interface BookDao {
public String findName(int id,String password);
}
1
2
3
4
5
6
@Repository
public class BookDaoImpl implements BookDao {
public string findName(int id,string password){
System.out.println(“id:"+id);
return "itcast";
}

启动程序

1
2
3
4
5
6
7
8
9
10
11
12
public class App {
public static void main(string[] args){
ApplicationContext ctx= new AnnotationConfigApplicationcontext(Springconfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
String name = bookDao.findName(id:100,password: "itheima");
System.out.println(name):
}
}
然后输出:[100, itheima]
before advice ...
id:100
itcast

参数的调用:

其他的都一样这样用,对于around,父接口能调用的方法子接口也可以用,所以直接pjp.getArgs调用对应参数

在调用原始方法时,对于proceed这个操作除了空参外还可以传个object数组,也就是可以把上面的args传进去

1
2
3
4
5
6
7
8
9
10
11
@Around(“pt()")
public object around(ProceedingJoinPoint pjp)throws Throwable {
object[] args = pjp.getArgs();
System.out.println(Arrays.tostring(args));
//object ret = pjp.proceed();
object ret = pjp.proceed(args);
return ret;
}
输出[100,itheima]
id:100
itcast

但如果在获取之后调用之前把args的值更改了,比如args[0] = 666;最后输出的结果也会被更改,所以如果传过来的参数有问题,我们就可以先处理一下,这里可以保证程序的健壮性,不需要每个程序都写,一个aop就可以搞定

返回值的调用

一个是around一个是afterReturning,around的返回值就是ret

而afterReturning要拿返回值,可以先定义一个用来接受返回值的形参,但如果定义了后就要告诉@AfterReturning用ret这个变量准备接受返回值。点进@AfterReturning可以看到提供了很多东西,其中一个是returning和value,returning的值要和形参名对应

1
2
3
4
@AfterReturning(value = "pt()", returning = "ret")
public void afterReturning(Object ret){
System.out.println("afterReturning advice ..."+ret);
}

当JoinPoint和Object同时存在,JoinPoint必须在第一个位置,其他的也是一样,如果不是这样就会报IllegalArgumentException异常

1
public void afterReturning(JoinPoint jp,Object ret){

异常对象的调用:

两种写法,第一种是

1
2
3
4
5
6
7
8
9
10
11
public object around(ProceedingJoinPoint pjp) {
object[] args = pjp.getArgs();
System.out.println(Arrays.tostring(args));
object ret = null;
try{
ret = pjp.proceed(args);
} catch (Throwable throwable){
throwable.printStackTrace();
}
return ret;
}

第二种

1
2
3
4
@AfterThrowing(value = "pt()",throwing ="t")
public void afterThrowing(Throwable t){
System.out.println("afteiThrowing advice ..."+t)
}

案例:百度网盘密码数据兼容处理

image-20251224024522509

分析:
①:在业务方法执行之前对所有的输入参数进行格式处理——trim(0)
②:使用处理后的参数调用原始方法–环绕通知中存在对原始方法的调用

准备代码

1
2
3
public interface ResourcesService{
public boolean openURL(string url ,string password);
}
1
2
3
4
5
6
7
8
@Service
public class ResourcesServiceImpl implements ResourcesService {
@Autowired
private ResourcesDao resourcesDao;

public boolean openURL(string url, string password){
return resourcesDao.readResources(url,password);
}
1
2
public interface ResourcesDao{
boolean readResources(String url,String password);
1
2
3
4
5
6
@Repository
public class ResourcesDaoImpl implements ResourcesDao{
public boolean readResources(String url,String password){
//模拟校验
return password.equals("root");
}
1
2
3
4
5
6
7
public class App {
public static void main(string[l args){
ApplicationContext ctx = new AnnotationConfigApplicationcontext(SpringConfig.class);
ResourcesService resourcesService = ctx.getBean(ResourcesService.class):
boolean flag = resourcesService.openURL( url: "http://pan.baidu.com/haha", password: "root"),
System.out.println(flag);
}

现在简单运行一下,返回的是true,如果把root后面加个空格就会返回false,我们想要的效果是加上空格也能返回true,也就是帮我把这个空格去掉

首先开启aop注解

1
2
3
4
@Configuration
@Componentscan("com.itheima")
@EnableAspectJAutoProxy
public class springconfig {

新建aop处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DataAdvice {
@Pointcut("execution(boolean com.itheima.service.*service.*(*,*))")
private void servicePt(){}

@Around("DataAdvice.servicePt()")
public object trimstr(ProceedingJoinPoint pjp)throws Throwable {
object[l args = pjp.getArgs();
//怎么去掉呢,有的人想用循环trim去掉空格,但args是个对象不能用trim
/*for(object obj:args){
obj.trim//没有这个调用方法
}*/
//这里用for循环不用foreach
for(int i = 0; i < args.length; i++){//通过i拿到索引值
//判断参数是不是字符串
if(args[i].getClass().equals(String.class)){
args[i] = args[i].toString().trim();//如果是字符串类就一定可以转换成字符串,然后trim,改完再放回去
}
}
//把改完的参数放进原始操作里然后返回
Obiect ret = pip.proceed(args),
return ret;
}

如果想测试做没做对就可以在输出程序app里添加

1
System.out.println(password.length());

然后把root改成1234空格空格,打印出来的长度为4就是对的

AOP总结

  • 概念:AOP(Aspect 0riented Programming)面向切面编程,一种编程范式
  • 作用:在不惊动原始设计的基础上为方法进行功能增强
  • 核心概念
    • 代理(Proxy):SpringAoP的核心本质是采用代理模式实现的
    • 连接点(JoinPoint):在SpringA0P中,理解为任意方法的执行
    • 切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述
    • 通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
    • 切面(Aspect):描述通知与切入点的对应关系
    • 目标对象(Target):被代理的原始对象成为目标对象

切入点表达式书写技巧

  1. 标准规范开发
  2. 查询操作的返回值建议使用*匹配
  3. 减少使用..的形式描述包
  4. 对接口进行描述,使用*表示模块名,例如userservice的匹配描述为*Service
  5. 方法名书写保留动词,例如get,使用表示名词,例如getById匹配描述为getBy
  6. 参数根据实际情况灵活调整

环绕通知可以模拟出其他四种通知,比如只在调用原始操作前做事情,这就是前置通知,用try catch写完后在finally写的东西就是后置通知,而返回后通知是在你的try catch finally中try大括号结束之前,原始调用方法之后的那些可以模拟返回后通知,catch模拟抛出异常后通知

环绕通知(重点 )

  • 环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用
  • 环绕通知可以隔离原始方法的调用执行
  • 环绕通知返回值设置为0bject类型
  • 环绕通知中可以对原始方法调用过程中出现的异常进行处理

spring事务简介

案例:银行账户转账

  • 事务作用:在数据层保障一系列的数据库操作同成功同失败
  • Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败

spring提供了一个接口,叫做平台事务管理器,大概意思是提交的时候都提交,回滚的时候都回滚

1
2
3
4
public interface PlatformTransactionManager{
void commit(TransactionStatus status)throws TransactionException;
void rollback(TransactionStatus status)throws TransactionException;
}

实现类

1
2
public class DatasourceTransactionManager{
}

但是这个内部使用的是jdbc的事务,如果用的是jdbc的技术就可以用这个,而mybatis内部就是用的jdbc

案例:模拟银行账户间转账业务
需求:实现任意两个账户间转账操作
需求微缩:A账户减钱,B账户加钱

分析
①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
②:业务层提供转账操作(transfer),调用减钱与加钱的操作
③:提供2个账号和操作金额执行转账操作
④:基于Sprinq整合MyBatis环境搭建上述操作

转账接口:

1
2
3
4
5
6
7
8
9
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* param in 转入方
* @param money 金额
*/
@Transactional//在需要添加事务的方法上加这个注解
public void transfer(String out,String in ,Double money);

数据层接口:

1
2
3
4
5
6
public interface AccountDao
@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name")String name, @Param("money")Double money);

@Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name")String name, @Param("money") Double money);

业务层实现类:

1
2
3
4
5
6
7
8
9
@Service
public class AccountServiceImpl implements AccountService{
@Autowired
private AccountDao accountDao;

public void transfer(String out,String in ,Double money){
accountDao.outMoney(out,money);
accountDao.inMoney(in,money);
}

测试

1
2
3
4
5
6
7
8
9
10
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest{
@Autowired
private AccountService accountService;

@Test
public void testTransfer() throws IOException {
accountService.transfer("Tom","Jerry",100D);
}

结果分析
①:程序正常执行时,账户金额A减B加,没有问题
②:程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败

第一步:要在接口上添加事务注解

第二部,到jdbc配置中配一个事务管理器

1
2
3
4
5
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);//dataSource是bean注入的
return transactionManager;

第三步要让spring知道你是用注解形式开启事务管理,到spring的配置文件中配置

1
2
3
4
5
6
@Configuration
@Componentscan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({Jdbcconfig.class,Mybatisconfig.class})
@EnableTransactionManagement//这个
public class springconfig {

注意事项:Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合
注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务

spring事务角色

上一节的案例里业务层方法中使用了这个操作

1
2
3
4
5
@Transactional
public void transfer(string out,string in ,Double money){
accountDao.outMoney(out,money);
accountDao.inMoney(in,money);
}

而这两个操作分别对应独立的数据层操作

1
2
3
4
public interface AccountDao {
@Update("update tbl_account set money = money -#{money} where name = #{name}"
void outMoney(@Param("name") String name, @Param("money") Double money);
}
1
2
3
public interface AccountDao {
@Update("update tbl_account set money = money + #{money} where name = #{name}"
void inMoney(@Param("name")String name, @Param("money")Double money);

上面两个属于增删改操作所以都要开启事务,分别叫t1t2,这两个是不同的事务,假如第二个出现了异常,第一个不会去回滚。当业务层加上开启事务注解后就会有事务T,目前是三个事务,谁也不挨谁,于是spring在这里做了一件事,既然数据层都在业务层的控制范围内,这两个事务都改成和他一样的事务。讲spring开启的这个事务叫做事务管理员,将加入事务管理员的这些成员叫做事务的协调员

  • 事务角色
    • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
    • 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法

还有个需要注意的是在写程序的时候jdbc和mybatis里都注入了一个datasource,正因为使用了同一个datasource,所以才能够进行统一管理

事务相关配置

属性 作用 示例
readOnly 设置是否为只读事务 readonly=true 只读事务
timeout 设置事务超时时间 timeout =-1(永不超时)
rollbackFor 设置事务回滚异常(class) rollbackFor ={NullPointException.class}
rollbackForClassName 设置事务回滚异常(string) 同上格式为字符串
noRollbackFor 设置事务不回滚异常(class) noRollbackFor
={NullPointException.class}
noRollbackForClassName 设置事务不回滚异常(string) 同上格式为字符串
propagation 设置事务传播行为 …..

点进事务按ctrl+f12可以查看类的方法

1
2
3
public interface AccountService{
@Transactinnal(readOnly = true,timeout = -1)//代表只读和永不超时
public void transfer(String out,String in ,Double money)throws IoException;

重点是rollbackFor遇到某一类异常回滚事务

一般遇到这两种会进行回滚,第一种是error系列的错误,比如内存溢出,第二种是运行时异常,而IOException这种属于编译时异常。而你希望在IOException异常发生后进行回滚就可以这样写

1
@Transactional(rollbackFor ={I0Exception.class)

案例:转账业务追加日志
需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
需求微缩:A账户减钱,B账户加钱,数据库记录日志

分析:

  1. 基于转账操作案例添加日志模块,实现数据库中记录日志
  2. 业务层转账操作(transfer),调用减钱、加钱与记录日志功能

实现效果预期

​ 无论转账操作是否成功,均进行转账操作的日志留痕

log业务层

1
2
3
4
public interface LogService {
@Transactional
void log(string out, string in, Double money);
}

实现类

1
2
3
4
5
6
7
8
@Service
public class LogserviceImpl implements LogService{
@Autowired
private LogDao logDao;

public void log(String out,String in,Double money ){
logDao.log( info:"转账操作由"+out+"到"+in+",金额:"+money);//拼接成字符串
}

数据层

1
2
3
public interface LogDao {
@Insert("insert into tbl log(info,createDate)values(#{info},now())")
void log(String info);//把传过来的信息作为info信息传到insert里

实现类,回过头看要求是无论转账操作是否成功,均进行转账操作的日志留痕,怎样保证代码一定会执行,可以用try finally方法,把有可能出问题的代码放上面,finally放一定执行的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class AccountServiceImpl implements Accountservice {
@Autowired
private AccountDao accountDao;

@Autowired
private LogService logService;

/*public void transfer(string out,string in ,Double money){
logservice.log(out,in,money);//这一句话并不一定保证能运行
accountDao.outMoney(out,money);
accountDao.inMoney(in,money);
}*/
public void transfer(String out,String in ,Double money){
try{
accountDao.outMoney(out,money);
int i = 1/0;//模拟异常
accountDao.inMoney(in,money);
}finally {
logService.log(out,in,money);
}

然后在accountService和LogService都开启事务,去数据库查看运行效果,发现没有问题,然后在程序里模拟异常,然后再查看数据库,这时候金额没有发生变化,但日志也没有被记录

存在的问题
日志的记录与转账操作隶属同一个事务,同成功同失败

实现效果预期改进
无论转账操作是否成功,日志必须保留

这里学习到一个事务传播行为:事务协调员对事务管理员所携带事务的处理态度,协调员是加入呢还是不加入还是搞一个新的。

这里是搞一个新的事务,点开LogService的事务注解,里面有个方法是propagation,里面有一系列的默认值,用法是如下

1
@Transactinnal(propagation = Propagation.REQUIRES_NEW)

image-20251226033800696

SpringMVC

SpringMVC简介

SpringMVC概述

  • SpringMVc技术与Servlet技术功能等同,均属于web层开发技术
  • 优点
    • 使用简单,开发便捷(相比于Servlet)
    • 灵活性强

以下是用servlet的保存代码(仅演示,除此之外还有其他三个不同功能的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Webservlet("/user/save")
public class UserSaveServlet extends HttpServlet{
@Override
protected void doGet(HttpservletRequest reg, HttpservletResponse resp) throws ServletException, IoException {
//1.接收请求参数
String name = reg.getParameter( name:"name");
System.out.println("servlet save name ==>" + name);
//2.产生响应
resp.setcontentType("text/json;charset=utf-8");
PrintWriter pw=resp.getWriter();
pw.write( s:"{'module':'servlet save'}");
}
@override
protected void doPost(HttpservletRequest reg, HttpservletResponse resp) throws ServletException, IOException {
this.doGet(reg,resp);
}

用mvc做就变成只有一个类,里面集成了四个方法

1
2
3
4
5
6
7
@Controller
public class Usercontroller{
@RequestMapping("/save")
@ResponseBody
public String save(String name){
System.out.println("springmvc save name ==>" + name);
return "{'module':'springmvc save'}";

入门案例

①:使用SpringMVc技术需要先导入SpringMVc坐标与Servlet坐标,也就是导入maven

②:创建SpringMVC控制器类(等同于servlet功能),用@Component是定义bean,但在mvc中开发表现层的bean要用@Controller这个注解

③:初始化SpringMVC环境(同Spring环境),设定SpringMVC加载对应的bean

④:初始化Servlet容器,加载SpringMVC环境,并设置SpringMVC技术处理的请求。在mvc中提供了一个专用的开发web容器的配置类,只需要自己定义一个类然后继承就行了

然后新建项目,在pom.xml配置,导入servlet包说会与tomcat有冲突,之前的web里学的是启动项参数(大概),这里是加了个标签provided,意思是在编译时可用运行时不可用,test相反

1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>

然后新建controller包UserController类,返回json就是String类型

1
2
3
4
5
6
7
8
9
10
11
12
//2.定义controller
//2.1使用@controller定义bean
@Controller
public class UserController {
//2.2设置当前操作的访问路径,对应下面第五步,/save对应save方法
@RequestMapping("/save")
//2.3设置当前操作的返回值类型
@ResponseBody
public String save(){
System.out.println("user save...”);
return "{'module':'springmvc'}";
}

再新建一个spring配置类

1
2
3
4
//3.创建springmvc的配置文件,加载controller对应的bean
@Configuration
@ComponentScan("com.itheima.controller")//看下面的第四步
public class SpringMvcConfig {}

再做一个tomcat容器启动的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//4.定义一个servlet容器启动的配置类,在里面加载spring的配置
public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer {

//加载mvc配置
@Override
protected WebApplicationContext createServletApplicationContext(){//看下面工作流程的第二步
AnnotationConfigWebApplicationcontext ctx = new AnnotationConfigWebApplicationContext();//注意这个是应用于web的,是初始化一个空容器
//注册配置,看下面的第三步
ctx.register(SpringMvcConfig.class);
return ctx;
}
//设置哪些发过来的请求归属springMVc处理,对应下面的第六步
protected String[] getServletMappings(){
return new string[]{"/"};//一个斜杠就是所有请求
}
//加载spring容器配置
protected WebApplicationContext createRootApplicationContext(){
//把上面mvc的直接复制古来然后把括号里的mvc删掉就是spring的配置了
AnnotationConfigWebApplicationcontext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(SpringConfig.class);
return ctx;
}

名称:@Controller

  • 类型:类注解
  • 位置:SpringMvc控制器类定义上方
  • 作用:设定SpringMVc的核心控制器bean

名称:@RequestMapping

  • 类型:方法注解
  • 位置:SpringMVc控制器方法定义上方
  • 作用:设置当前控制器方法请求访问路径
  • 相关属性:value:请求访问路径

名称:@ResponseBody

  • 类型:方法注解

  • 位置:SpringMVc控制器方法定义上方

  • 作用:设置当前控制器方法响应内容为当前返回值,无需解析

  • AbstractDispatcherServletInitializer类是SpringMVC提供的快速初始化Web3.8容器的抽象类

  • AbstractDispatcherServletInitializer提供三个接口方法供用户实现

    • createServletApplicationContext()方法,创建Servlet容器时,加载SpringMVC对应的bean并放入
      WebApplicationContext对象范围中,而webApplicationContext的作用范围为ServletContext范围,即整个web容器范围
    • getservletMappings()方法,设定SpringMVC对应的请求映射路径,设置为/表示拦截所有请求,任意请求都将转入到SpringMVc进行处理
    • createRootApplicationcontext()方法,如果创建Servlet容器时需要加载非SpringMVC对应的bean,使用当前方法进行,使用方式同createservletApplicationcontext()

入门案例工作流程分析

启动服务器初始化过程:

  1. 服务器启动,执行ServletContainersInitConfig类,初始化web容器

  2. 执行createServletApplicationContext方法,创建了WebApplicationcontext对象,这个对象就是mvc最终体现的对象,有了这个对象就被加载到了web容器中,但被加载到什么位置呢,就是ServletContext,最大范围的这个

  3. 加载SpringMvcConfig

  4. 执行@Componentscan加载对应的bean

  5. 加载Usercontroller,每个@RequestMapping的名称对应一个具体的方法

  6. 执行getservletMappings方法,定义所有的请求都通过SpringMVC

    []getRootconfigclasses(){
    return new class[]{SpringConfig.class};
    }
    protected class<?>[]getServletconfigclasses(){
    return new Class[]{SpringMvcConfig.class};
    }
    protected string[l getservletMappings(){
    return new string[]{“/“};
    }

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

## 网页调试与发送网页HTTP请求的插件

老师教的是postman,但我用的是apifox,所以看个原理就行

主要作用是模拟浏览器发送请求,比如发送get请求在地址栏写一下就行,但post请求就得写个表单,ajax就得写个js文件,但使用了这个工具,上面的工作就不用做了

先新建工作空间,这里面的操作可以备份和记录,然后新建请求,选择请求的类型比如get,然后输入地址点击发送就可以

## 请求与响应

### 请求的映射路径

准备数据

~~~java
@Controller
@RequestMapping("/user")
public class Usercontroller {

@RequestMapping("/save")
//@RequestMapping("/user/save")
@ResponseBody
public String save(){
System.out.println("user save..");
return "{'module':'user save'}";
}

@RequestMapping("/delete")
//@RequestMapping("/user/delete")
@ResponseBody
public String delete(){
System.out.println("user delete...");
return "{'module':'user delete'}";
}
}
1
2
3
4
5
6
7
8
9
@Controller
public class Bookcontroller {
//@RequestMapping("/save")
@RequestMapping("/book/save")
@ResponseBody
public String save(){
System.out.println("book save ...");
return "{'module':'book save'}";
}

这里面有两个save,然后启动一下发现报错

com.itheima.controller.UserController#save()
to{/save}: There is already “bookController’ bean method

意思是/save已经被bookController的bean方法用过了

1.团队多人开发,每人设置不同的请求路径,冲突问题如何解决——设置模块名作为请求路径前缀,比如@RequestMapping(“/user/save”)

也可以把@RequestMapping(“/user”)放在整个类上面定义整个模块的请求路径前缀

请求方式

get方式:

准备数据ServletContainersInitConfig简单配置,SpringMvcConfig

1
2
3
@Configuration
@Componentscan("com.itheima.controller")
public class springMvcconfig {

控制器类,如果请求的路径是http://localhost/commonParam?name=itcast,这个name参数怎么接收呢,在括号里直接添加String来接收就行,如果是多个参数就在后面接着添加就行

1
2
3
4
5
6
7
8
9
@Controller
public class UserController {
//普通参数
@RequestMapping("/commonParam")
@ResponseBody
public String commonParam(String name){
System.out.println("普通参数传递 name ==>"+ name);
return "{'module':'common param'}";
}

Post方式:

在这里不管是post还是get请求,对于后台代码是没有变化的不区分的,和之前的Servlet是不一样的。请求地址:http://localhost/commonParam,然后在老师演示的工具里不能再点params了,因为post请求的参数属于请求体内,所以要点body这项

image-20251228003332321

如果是发表单数据应该用这个选项,和左边的区别在于这个不仅能发表单还能发文件,代码还是使用上面的,设置以下参数

image-20251228003851644

如果把英文值改成中文,就会出现乱码现象,之前的处理方式是在web服务器设置一个过滤器,这里也是一样,只不过要在ServletContainersInitConfig里配置,用override覆盖方法。可以看到这里是要一个Filter数组,那么就可以直接new一个filter数组然后把过滤器对象放进去。但是过滤器在哪,springmvc有现成的字符过滤器,设定好字符集后直接把filter放进去就行

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ServletContainersInitConfig extends AbstractAnnotationConfigpispatcherservletInitializer{
protected Class<?>[] getRootConfigClasses(){ return new Class[0];}
protected Class<?>[] getServletConfigClasses() { return new Class[]{SpringMvcConfig.class}; }
protected String[] getServletMappings(){ return new String[]{"/"}; }

//乱码处理
@Override
protected Filter[] getservletFilters(){
CharacterEncodingFilter filter = new characterEncodingFilter();
filter.setEncoding("UTF-8");
return new Filter[]{filter);
//return new Filter[]{filter,filter1,filter2);设置多个过滤器
}

这个characterEncodingFilter在哪里?图下坐标包含了这个

image-20251228154530141

请求参数

**普通参数:**url地址传参,地址参数名与形参变量名相同,定义形参即可接收参数

之前提到过一个问题就是get请求里的name与代码里的名字不一样,还能接收吗,答案是不能,解决方式是添加@RequestParam,也就是把请求参数中的name给到username

1
2
3
@RequestMapping("/commonParamDifferentName")
@ResponseBody
public string commonParamDifferentName(@RequestParam("name")String userName , int age){

**pojo参数:**传参的目的是通过大量的数据组成一个model,下面这个user里面有两个属性name和age,然后发送http://localhost/pojoParam?name=itcast&age=15,还是能被接收到,也就是前面的属性名和后面实体类的属性名一样就可以自动把属性塞进去

1
2
3
4
5
//P0J0参数
@RequestMapping("/pojoParam")
@ResponseBody
public String pojoParam(User user){

1
2
3
4
5
public class User{
private string name;
private int age;

private Address address;//后面加上了get和tostring方法,这里省略

然后还有个实体类里面定义了省市,然后封装成一个对象,然后加到上面

1
2
3
public class Address{
private String province;
private String city;

然后要给实体类里某个对象传属性值应该怎么做,以下这样做就可以

image-20251229170705867

**数组参数:**比如你的个人爱好之类的,是很多个数据,比如复选框,可以用以下接收

1
2
3
4
//数组参数
@RequestMapping("/arrayParam")
@ResponseBody
public String arrayParam(String[] likes){

image-20251229173509907

但是java编程一般用集合比较多

集合参数:

还是用上面的数据发送看看有什么结果,发现报错了,提示NoSuchMethodException没有这个方法,后面还有java.util.List.(),只要出现代表的就是构造方法,list是个接口本身就不会有构造方法。他现在是在尝试造一个list类型的对象,为什么要造这个对象呢,因为这是引用类型,上面的那些也都是引用类型,所以都是准备造个对象然后setlike属性,但我们要的是作为集合的参数放进去。

用@RequestParam告诉他吧那些东西作为参数扔进去,只要是集合就不按照造对象的方法来了

1
2
3
4
5
//集合参数
@RequestMapping("/listParam")
@ResponseBody
//public String listParam(List<String> likes){
/public String listParam(@RequestParam List<String> likes){
  • 名称:@RequestParam

  • 类型:形参注解

  • 位置:SpringMVc控制器方法形参定义前面

  • 作用:绑定请求参数与处理器方法形参间的关系

  • 参数:

    • required:是否为必传参数
    • defaultValue:参数默认值

json数据

先在pom导入json,然后发送数据,在body里选择raw然后选择json

image-20251230024829629

接收代码,但目前mvc不知道外面过来的json要转化成List,需要开启一个功能键,让它帮忙做这种转化

1
2
3
4
//集合参数:json格式
@RequestMapping("/listParamForJson")
@ResponseBody
public String listParamForJson(@RequestBody List<String> likes)

在mvc配置中添加

1
2
3
4
@Configuration
@ComponentScan("com.itheima.controller")
@EnableWebMvc//开启这个
public class springMvcconfig {

然后运行还是报错无法造对象,然后这次信息是在请求体里,也就是requestbody中,@RequestParam就不好用了,用@RequestBody就可以了

如果是传给pojo的,在body里变成这样,然后加上@RequestBody就行

image-20251230033804617

集合也是一样

  • 名称:@EnableWebMvc

  • 类型:配置类注解

  • 位置:SpringMvc配置类定义上方

  • 作用:开启SpringMvc多项辅助功能

  • 名称:@RequestBody

  • 类型:形参注解

  • 位置:SpringMVC控制器方法形参定义前面

  • 作用:将请求中请求体所包含的数据传递给请求参数,此注解一个处理器方法只能使用一次

@RequestBody与@RequestParam区别

  • 区别
    • @RequestParam用于接收url地址传参,表单传参【application/x-www-form-urlencoded】
    • @RequestBody用于接收json数据【application/json】,在postman软件里发送的请求头中找到content-type,这里指定的是application/json,老师说以前手写发ajax强求时这项要自己手写,现在自动加上了,如果是post的话这一项就变成了application/x-www-form-urlencoded
  • 应用
    • 后期开发中,发送json格式数据为主,@RequestBody应用较广
    • 如果发送非ison格式数据,选用@RequestParam接收请求参数

日期类型参数传递

如果是标准日期格式比如2012/02/02是可以直接由字符串转换成日期格式,但如果不是标准格式就需要用@DateTimeFormat后面带上格式就行

1
2
3
4
5
@RequestMapping(Ov"/dataParam"
@ResponseBody
public string dataParam(Date date,
@DateTimeFormat(pattern="yyyy-MM-dd")Date date1,
@DateTimeFormat(pattern="yyyy/MM/dd HH:mm:ss")Date date2){

这个注解内部是怎么工作的呢?里面用到了类型转换器converter,它是个接口,打开时注意包的位置在springframework里,这个接口就定义了一个方法T convert(S source);,这个接口的作用是将你传递的参数与我们要收集的参数进行转换,有非常多的实现类来帮助转换

响应

相应页面,注意这里没有加@RequsetBody

1
2
3
4
5
6
7
8
@Controller
public class UserController {
//响应页面/跳转页面
@RequestMapping("/toJumpPage")
public String toJumpPage(){
System.out.println("跳转页面");
return "page.jsp";
}
1
2
3
4
5
<html>
<body>
<h2>Hejlo Spring Mvc!</h2>
</body>
</html>

然后访问路径就可以看到page.jsp的内容被输出了,在mvc中想要响应页面只需要把页面名称写在return里然后返回字符串就可以了

如果只想返回文本呢,如果没加@ResponseBody会报错No mapping for GET/response text,它在找一个叫response text的页面,而response text只是个文本,所以要加上@ResponseBody就行了

1
2
3
4
5
6
//响应文本数据
@RequestMapping("/toText")
@ResponseBody
public String toText(){
System.out.println("返回纯文本数据");
return "response text";

然后是响应json数据,如果想响应pojo对象,只需要把方法的返回值设成pojo对象然后再return这个对象就行了

1
2
3
4
5
6
7
8
9
//响应P0J0对象
@RequestMapping("/toJsonP0J0")
public User to]sonPo]0(){
System.out.println("返回json对象数据");
User user = new User();
user.setName("itcast");
user.setAge(15);
return user;
}

但这样运行会报错,因为mvc对User返回值的要求默认是要个字符串,会把return user认成字符串”user”,所以还是要加@ResponseBody,而转换的这个操作是pom里导入的jackson帮我们做的,集合的获取也是这样

  • 名称:@ResponseBody
  • 类型:方法注解
  • 位置:SpringMVc控制器方法定义上方
  • 作用:设置当前控制器方法响应内容为当前返回值,无需解析
  • 作用:设置当前控制器返回值作为响应体

实现这个转换的过程是一个叫HttpMessageConverter的接口,专门转换http消息的,打开类型层次图找到MappingJackson2HttpMessageConverter就是最终的实现类了

REST风格

这两个链接和上面的作用完全一样

上述行为是约定方式,约定不是规范,可以打破,所以称REST风格,而不是REST规范
描述模块的名称通常使用复数,也就是加s的格式描述,表示此类资源,而非单个资源,例如:users、books、accounts.

  • 根据REST风格对资源进行访问称为RESTful,开发使用这个风格开发就叫RESTful

案例

根据之前的代码进行更改

1
2
3
4
5
6
7
@Controller
public class UserController {
@RequestMapping("/save")
@ResponseBody
public String save(@RequestBody User user){
System.out.println("user save..."+ user);
return "{'module':'user save'}";

然后更改,先不传参,第一链接那里要写users,第二步指定请求行为,点进RequestMapping可以看到有个叫method的,这个可以设定对应的请求方法

1
2
3
4
5
6
7
@Controller
public class UserController {
@RequestMapping(Value ="/users",method = RequestMethod.POST)
@ResponseBody
public String save(){
System.out.println("user save...");
return "{'module':'user save'}";

再改个delete,在请求路径上包含了一个值1,要想让id参数对应请求路径就要用到@PathVariable,也就是路径变量的意思,只有这个注解不够,还要告诉来自路径中的哪里,所以要加/{id}进行匹配

1
2
3
4
5
6
@RequestMapping(value = "/users/{id},method = RequestMethod.DELETE)
@ResponseBody
public string delete(@PathVariable Integer id){
”+ id);
System.out.println("user delete...
return "{'module':'user delete'}";

@RequestBody、@RequestParam、@PathVariable

  • 区别
    • @RequestParam用于接收url地址传参或表单传参
    • @RequestBody用于接收json数据
    • @PathVariable用于接收路径参数,使用{参数名称}描述路径参数
  • 应用
    • 后期开发中,发送请求参数超过1个时,以json格式为主,@RequestBody应用较广
    • 如果发送非json格式数据,选用@RequestParam接收请求参数
    • 采用RESTfu1进行开发,当参数数量较少时,例如1个,可以采用@Pathvariable接收请求路径变量,通常用于传递id值

restful快速开发

value=”/books”和@ResponseBody这种重复代码写起来很繁琐能不能简化呢。可以用@ReqstMapping(“/books”)代替前面重复的value那些,但是有参数的还是要留着{id}这种。

然后@ResponseBody可以拿到外面了,而springmvc给合成了一个@RestController,里面包含了这两个注解。

@RequestMapping(value = “/{id}”,method = RequestMethod.DELETE)这个就简化成了DeleteMapping(“/{id}”)

1
2
3
4
5
6
7
8
9
10
//@Controller
//@ResponseBody
@RestController
@ReqstMapping("/books”)
public class BookController {
//@RequestMapping(value = "/{id}",method = RequestMethod.DELETE)
//DeleteMapping("/{id}")
public string delete(@PathVariable Integer id){
System.out.println("book delete..."+ id);
return "{'module':'book delete'}";

案例:基于RESTful页面数据交互

能正常展示数据,并且点击新建按钮输入信息后能提交到后台

先把后台的controller做出来然后再使用postman测通,然后让页面能运行,然后发送ajax提交能访问后台表现层接口

准备数据

1
2
3
4
@Configuration
@Componentscan("com.itheima.controller")
@EnableWebMvc
public class SpringMvcConfig {
1
2
3
4
5
6
7
8
9
10
11
12
public class servletcontainersInitconfig extends AbstractAnnotationConfigDispatcherservletInitializer{
protected Class<?>[] getRootconfigclasses(){ return new class[0]; }
protected Class<?>[] getServletConfigclasses(){ return new class[]{springMvcConfig.class}; }
protected String[] getservletMappings(){ return new String[]{"/"}; }

//乱码处理
@Override
protected Filter[] getservletFilters(){
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
return new Filter[]{filter};
}

domain包下Book类

1
2
3
4
5
6
public class Book{
private Integer id;
private String type;
private String name;
private String description;
下面是tostring和setget方法

创建BookController

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
@RestController
@RequestMapping("/books")
public class BookController {

@PostMapping
public String save(@RequestBody Book book){//要记得加@RequestBody接收json数据
System.out.println("book save ==>"+ book);
return "{'module':'book save success'}";
}

@GetMapping
public List<Book> getAll(){
List<Book>bookList = new ArrayList<Book>();
Book book1 = new Book();
book1.setType("计算机");
book1.setName("springMvc入门教程");
book1.setDescription("小试牛刀");
bookList.add(book1);//添加到集合里

Book book2 = new Book();
book2.setType("计算机");
book2.setName("springMVc实战教程");
book2.setDescription("一代宗师");
bookList.add(book2);
return bookList:

然后postman发送和接收一下测试测试

然后把html的那些东西复制到webapp文件夹里,运行一下服务器,然后访问html网址,结果404,后台输出提示[WARNING] No mapping for GET /pages/books.html,是被mvc拦截了,它认为应该有个配置叫books.html,所以应该把这个放行交给tomcat处理,问题出在下面,”/“是把所有请求都经过mvc,应该把静态资源不过mvc

1
2
public class servletcontainersInitconfig extends AbstractAnnotationConfigDispatcherservletInitializer{
protected String[] getservletMappings(){ return new String[]{"/"}; }

方案一:做一个功能类,在config里新建一个SpringmvcSupport

1
2
3
4
5
6
@Configuration
public class SpringMvcSupport extends WebMvcconfigurationSupport {//继承这个类
@override
protected void addResourceHandlers(ResourceHandlerRegistry registry){//查看继承类有什么关于source的方法,找到了这个
//当访问/pages/**的时候,走/pages目录下的内容
registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");//这里的意思是如果发送的是/pages/**请求就走/pages/下的内容

写完上面的代码记得必须要有个动作就是让mvc启动时加载这里,加上@Configuration然后在Springmvc配置类里改成

1
@ComponentScan({"com.itheima.controller","com.itheima.config")

以后要放行什么就在SpringmvcSupport继续添加就行

这里是vue代码

1
2
3
4
5
6
7
8
9
10
//主页列表查询
getA11(){
axios.get("/books").then((res)=>{
this.dataList = res,data;
});
},
//添加
saveBook(){
axios.post("/books",this.formData).then((res)=>{
});//这里是post要提交数据,所以路径后面要加上this.formData

SSM整合

SSM整合流程

  1. 创建工程

  2. SSM整合

    • Spring

      • SpringConfig
    • MyBatis

      • MybatisConfig

      • Jdbcconfig

      • jdbc.properties

    • SpringMVC

      • ServletConfig

      • SpringMvcConfig

  3. 功能模块

    • 表与实体类
    • dao(接口+自动代理)
    • service(接口+实现类
      • 业务层接口测试(整合Junit)
    • controller
      • 表现层接口测试(PostMan)

先导入pom,里面有Spring、mybatis、mybatisSpring整合、mysql、junit、Servlet、jackson

再创建包config、controller、dao、domain、Service、impl

1
2
3
4
5
@Configuration//配置类
@ComponentScan({"com.itheima.service"})//扫描包
@PropertySource("jdbc.properties")//加载jdbc配置文件(里面是连接jdbc的信息)
@Import({JdbcConfig.class,MyBatisConfig.class})//加载配置类
public class springconfig {
1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm db
jdbc.username=root
jdbc.password=root
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Jdbcconfig{
@Value("${jdbc.driver}")
private String driver;,
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;

@bean
public DataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverclassName(driver);
datasource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyBatisconfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){//这里的dataSource是Spring容器根据类型自动装配
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setTypeAliasesPackage("com.itheima.domain");//类型别名扫描包
return factoryBean;
}

//映射扫描(Dao)
@Bean
public MapperscannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.itheima.dao");//扫描包
return msc;
1
2
3
4
5
6
7
8
9
10
11
12
public class servletcontainersInitconfig extends AbstractAnnotationConfigDispatcherservletInitializer{
protected Class<?>[] getRootconfigclasses(){ return new class[0]; }
protected Class<?>[] getServletConfigclasses(){ return new class[]{springMvcConfig.class}; }
protected String[] getservletMappings(){ return new String[]{"/"}; }

//乱码处理
@Override
protected Filter[] getservletFilters(){
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
return new Filter[]{filter};
}
1
2
3
4
@Configuration
@Componentscan("com.itheima.controller")
@EnableWebMvc
public class SpringMvcConfig {

这里老师提到mvc容器能访问Spring容器,反过来不行,这里是父子容器概念

功能模块

创建pojo对象

1
2
3
4
5
6
public class Book {
private Integer id;
private String type;
private String name;
private String description;
//后面是setget tostring

Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface BookDao {
//用mybatis自动代理创建实现类
//@Insert("insert into tbl_book values(null,#{type},#{name},#{description})”)这种写法括号里的字段对应book中的属性
@Insert("insert into tbl_book (type,name,description)values(#{type},#{name},#{description})"//这种写法前面的type是数据库中的字段,后面的是book中的属性,没有了id的null
public void save(Book book);

@Update("update tbl_book set type = #{type}, name = #{name}, description = #{idescription} where id = #{id}")
public void update(Book book);
@Delete("delete from tbl_book where id = #{id}")
public void delete(Integer id);
@Select("select * from tbl_book where id = #{id}")
public Book getById(Integer id);//查单个
@Select("select * from tbl_book")
public List<Book> getA11();//查所有

这样的接口层一般是不行的,最好是见名知意,返回值用boolean用来判断返回成功或失败

1
2
3
4
5
6
7
8
9
10
11
public interface Bookservice{
/**
* 保存
@param book
@return
**/
public boolean save(Book book);
public boolean update(Book book);
public boolean delete(Integer id);
public Book getById(Integer id);
public List<Book> getA11();

然后实现类,要用到dao接口了,然后注入dao

1
2
3
4
5
6
7
8
9
10
@Service
public class BookServiceImpl implements BookService {

@Autowired//注入dao
private BookDao bookDao;

public boolean save(Book book){
bookDao.save(book);
return true;//统一写true
}
public Book getById(Integer id){
	return bookDao.getById(id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
但做ssm整合的时候如果要注入这个的东西,在整个系统中不存在,也就是Spring中没有配bookdao的bean,我们用的自动代理,所以没有对应的bean给它自动装配,所以idea在这里把bookDao做个检查然后报错,但这个报错不影响运行,在弹出的下列选项里进入到idea做语法检查的设置,有一个Autowiring for bean class是自动装配的时候对bean的类型做检测,把它勾掉或者更改提示类型为警告

![image-20260101163637629](./images/黑马程序员SSM/image-20260101163637629.png)

![image-20260101163817235](./images/黑马程序员SSM/image-20260101163817235.png)

然后是控制层

~~~java
@RestController
@RequestMapping("/books")
public class Bookcontroller{
@Autowired
private BookService bookService;

@PostMapping
public boolean save(@RequestBody Book book){//从前端提交的json数据获取所以要加RequestBody
return bookService.save(book);
}
@DeleteMapping("/{id}")
public boolean delete(@PathVariable Integer id){
	return bookService.delete(id);
}
@GetMapping
public List<Book> getA11(){
	return bookService.getA11();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

### 接口测试

~~~java
@RunWith(SpringJUnit4ClassRunner.class)//Spring整合测试类
@ContextConfiguration(classes = SpringConfig.class)//加载配置类
public class BookServiceTest{

@Autowired
private Bookservice bookservice;

@Test
public void testGetById(){
Book book = bookService.getById(1);
System.out.println(book);
}//这里老师说正常测试要做断言测试,不知道是什么

然后是开启事务,先在SpringConfig里加上@EnableTransactionManagement,然后写事务管理器,要用到datasource,在jdbcconfig里添加代码

1
2
3
4
5
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){//平台事务管理器
DataSourceTransactionManager ds= new DataSourceTransactionManager();
ds.setDataSource(dataSource)://这里的datasource一定要从形参里拿
return ds;

然后在业务层加上@Transactional

表现层数据封装

前端接收数据格式太多,增删改返回true,查单条一个json,查全部一个集合json,所以现在要整合到一个里面

  • 前端接收数据格式-创建结果模型类,封装数据到data属性中

image-20260101194434877

但现在分不出返回的true是增还是删,所以再加一个code用来区分功能

image-20260101195035410

又有个问题,如果查询不存在的数据,后台会返回data:null,code:20041,但这个代表成功还是失败?所以我们做个规定,结尾是1就是成功,0就是失败。所以就变成了这样,如果是20041就从data取数据

1
2
"code":20040,
"data":null

然后又有问题,20040是没取到,应该给用户看什么呢,于是封装特殊消息到message(msg)属性中,变成

1
2
3
"code":20040,
"data":null
"msg":"数据查询失败,请重试!"

设置统一数据返回结果类

1
2
3
4
public class Result {
private Object data;
private Integer code;
private String msg;

注意:Result类中的字段并不是固定的,可以根据需要自行增减
提供若干个构造方法,方便操作

实现

其他数据继承上面的ssm整合,然后因为这是前后端沟通,要展示给前端的所以是表现层用,放到controller包里。因为是前后端沟通的所以不需要写tostring,只需要写setget,然后要提供构造方法不然就是new一个对象然后不停往里面set。构造方法分别是三个都要,data、code和无参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Result {
private Object data;
private Integer code;
private String msg;

public Result(){}
public Result(Integer code,Object data){
this.data = data;
this.code = code;
}
public Result(Integer code, Object data, String msg){
this.data = data;
this.code = code;
this.msg = msg;
}

然后再创建一个Code类

1
2
3
4
5
6
7
8
9
public class code {
public static final Integer SAVE_OK = 20011;
public static final Integer DELETE_OK = 20021;
public static final Integer UPDATE OK = 2031;
public static final Integer GET OK = 20041;
public static final Integer SAVE ERR = 20010;
public static final Integer DELETE ERR = 20020;
public static final Integer UPDATE_ERR = 20030;
public static final Integer GET ERR = 20040;

然后BookController里return的结果就变了,全都返回Result

1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping
public Result save(@RequestBody Book book){
boolean flag = bookservice.save(book);
return new.Result(flag ? Code.SAVE_OK:Code.SAVE_ERR,flag);
}

@GetMapping("/{id}")
public Result getById(@PathVariable Integer id){
Book book = bookService.getById(id);
Integer code = book != null ? Code.GET_OK : Code.GET_ERR;//如果book不为空就返回ok,否则err
String msg = book != null ? "":"数据查询失败,请重试!";
return new Result(code,book,msg);

异常处理器

  • 出现异常现象的常见位置与常见诱因如下
    • 框架内部抛出的异常:因使用不合规导致
    • 数据层抛出的异常:因外部服务器故障导致(例如:服务器访问超时)
    • 业务层抛出的异常:因业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引异常等)
    • 表现层抛出的异常:因数据收集、校验等规则导致(例如:不匹配的数据类型间导致异常)
    • 工具类抛出的异常:因工具类书写不严谨不够健壮导致(例如:必要释放的连接长期未释放等)

思考:

  1. 各个层级均出现异常,异常处理代码书写在哪一层

    -所有的异常均往上抛出到表现层进行处理

  2. 表现层处理异常,每个方法中单独书写,代码书写量巨大且意义不强,如何解决

    -AOP思想,但不需要自己写,mvc提供了快捷的处理方案

  • 异常处理器,要按规范来写
    • 集中的、统一的处理项目中出现的异常

首先在表现层新建一个ProjectExceptionAdvice类,然后加上注解用来声明这个类是做异常处理的,注意要在mvc配置类里加上扫描包路径

1
2
3
4
5
6
7
8
@RestControllerAdvice//声明这个类是做异常处理的
public class ProjectExceptionAdvice {
@ExceptionHandler(Exception.class)//拦截什么异常
public Result doException(Exception ex){//通过Exception形参接收异常
//这里面想怎么处理异常都行
System.out.println("嘿嘿,异常你哪里跑!");
return new Result(666, null, "嘿嘿,异常你哪里跑!");
}
  • 名称:@RestControllerAdvice

  • 类型:类注解

  • 位置:Rest风格开发的控制器增强类定义上方

  • 作用:为Rest风格开发的控制器类做增强

  • 说明:

    • 此注解自带@ResponseBody注解与@component注解,具备对应的功能
  • 名称:@ExceptionHandler

  • 类型:方法注解

  • 位置:专用于异常处理的控制器方法上方

  • 作用:设置指定异常的处理方案,功能等同于控制器方法,出现异常后终止原始控制器执行,并转入当前方法执行

  • 说明

    • 此类方法可以根据处理的异常不同,制作多个方法分别处理对应的异常

项目异常处理方案

项目异常分类

  • 业务异常(BusinessException)
    • 不规范的用户行为操作产生的异常,比如访问路径被更改
    • 规范的用户行为产生的异常,比如年龄输入嘿嘿
  • 系统异常(SystemException)
    • 项目运行过程中可预计且无法避免的异常比如停电
  • 其他异常(Exception)
    • 编程人员未预期到的异常,比如系统找不到指定的文件

项目异常处理方案

  • 业务异常(BusinessException)

    • 发送对应消息传递给用户,提醒规范操作
  • 系统异常(SystemException)

    • 发送固定消息传递给用户,安抚用户
    • 发送特定消息给运维人员,提醒维护
    • 记录日志
  • 其他异常(Exception)

    • 发送固定消息传递给用户,安抚用户
    • 发送特定消息给编程人员,提醒维护(纳入预期范围内)
    • 记录日志

创建一个exception包,然后创建一个系统异常类,然后加一个code用以区分错误,然后实现构造方法,可以用一个写一个也可以全写上,这里是只用了两个,然后再加上code的getset方法就行

1
2
3
4
5
6
7
8
9
10
11
public class SystemException extends RuntimeException{
private Integer code;
public SystemException(Integer code,string message){
super(message);
this.code = code;
}

public SystemException(Integer code, String message, Throwable cause){
super(message,cause);
this.code = code;
}

然后在可能出现异常的地方去进行一下处理,土办法是try catch,再自己new一个异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Book getById(Integer id){
//这也是模拟异常
if(id == 1){
throw new BusinessException(Code.BUSINESS_ERR,"请不要xxxx");
}

//将可能出现的异常进行包装,转换成自定义异常
try{
int i = 1/0;
//}catch(ArithmeticException e){这样太麻烦了,直接换成Exception比较方便
}catch(Exception e){
throw new SystemException(Code.SYSTEM TIMEOUT_ERR.,"服务器访问超时,请重试!",e);
}
return bookDao.getById(id);
}

然后就是异常处理器,code和消息都可以从ex里获得,其他异常就按照上面的异常处理器里写的就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestControllerAdvice
public class ProjectExceptionAdvice {
//系统异常
@ExceptionHandler(SystemException.class)//拦截上面的异常
public Result doSystemException(SystemException ex){
//记录日志
//发送消息给运维
//发送邮件给开发人员,ex对象发送给开发人员
return new Result(ex.getCode(),data:null,ex.getMessage());
}

//业务异常
@ExceptionHandler(BusinessException.class)
public Result doBusinessException(BusinessException ex){
return new Result(ex.getCode(),data:null,ex.getMessage());

前后端协议联调

添加功能

复制前端文件过来,然后现在没有配置的话页面的请求一定会被mvc拦截,所以要建个配置类给他放行,将任意请求都指向到哪个location

1
2
3
4
5
6
7
8
9
@Configuration
public class SpringMvcSupport extends WebMvcconfigurationsupport
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry){
registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
registry.addResourceHandler("/css/**").addResourceLocations("/css/");

registry.addResourceHandler("/js/**").addResourceLocations("/js/");
registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");

然后要在mvc中添加下面代码确保能扫描到

1
@Componentscan({"com.itheima.controller","com.itheima.config"})

前端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
methods:{
//列表
getA11(){
//发送ajax请求
axios.get("/books").then((res)=>{
this.dataList =res.data.data;
});

//添加
handleAdd(){
//发送ajax请求
axios.post("/books",this.formData).then((res)=>{
//如果操作成功,关闭弹层,显示数据
if(res.data.code == 20011){//添加成功
this.dialogFormVisible = false,
this.$message.success("添加成功");//这里的返回信息按理应该从后台拿,但是我们后天没写所以先在这里添加
}else if(res.data.code == 20010){//添加失败
this.$message.error("添加失败");
}else{//其他情况
}
this.getAll();

然后看代码,要让代码失败就要让code为20010,去bookController看到post的返回代码里如果想返回失败就得让flag值为false,看到BookServiceImpl的代码为以下,我们要让save这个功能必须把结果反馈给我们,所以要改dao

1
2
3
4
5
public boolean save(Book book){
//bookDao.save(book);
//return true;这里返回true是之前写死的
return bookDao.save(book) > 0;//也就是大于0代表成功
}

更改返回值为int,这个int就是行计数,用来判断到底是成功或失败

1
2
3
4
5
public interface BookDao{
@Insert("xxx")
//pubilc void save(Book book);
pubilc int save(Book book);
}

这里就把成功返回写好了,但怎么构建失败,在老师的数据库设计里如果输入的东西超过20个然后提交就会报错,然后提交的时候应该弹个框提示错误的但没有反应,在前端post那里添加代码console.log(res.data);查看打印数据,发现走的是sql异常,被异常拦截器拦截了,code是5999,就直接让它这个异常输出到前端就行。然后this.getAll();这句不管成功还是失败都要输出,一般用finally来写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  //添加
handleAdd(){
//发送ajax请求
axios.post("/books",this.formData).then((res)=>{
console.log(res.data);
if(res.data.code == 20011){
this.dialogFormVisible = false,
this.$message.success("添加成功");
}else if(res.data.code == 20010){
this.$message.error("添加失败");
}else{
this.this.$message.error(res.data.msg);//直接让它这个异常输出到前端就行
}
//this.getAll();
}).finally(()=>{
this.getAll();
});

然后还有个问题是弹窗输入完信息后再次打开信息没有被清空,要让每次打开弹窗都是空的就加入以下代码

1
2
3
4
5
6
7
8
//弹出添加窗口
handlecreate(){
this.dialogFormVisible =true;
this.resetForm();
//重置表单
resetForm(){
this.formData ={};
}。

修改功能

修改功能弹出的框里应该是有数据的,所以要先查询数据再展示,打印row看看里面的数据,选择id为查询条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//弹出编辑窗口
handleUpdate(row){
//console.log(row);//row.id 查询条件
//查询数据,根据id查询
axios.get("/books/"+row.id).then((res)=>{
if(res.data.code == 20041){
//console.log(res.data.data);//查看报错问题
//展示弹层加载数据
this.formData = res.data.data;
this.dialogFormVisible4Edit = true;//显示这个组件
}else{
this.$message.error(res.data.msg);
}
});

显示组件的时候发现报错,打印数据看看,发现输出了个null,结果是之前的Serviceimpl里有模拟的异常导致的,注释掉就行了

编辑功能

和添加功能一模一样,把里面的代码匹配一下,然后axios.post改为put

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//编辑
handleEdit(){
//发送ajax请求
axios.put("/books",this.formData).then((res)=>{
//如果操作成功,关闭弹层,显示数据
if(res.data.code == 20031){
this.dialogFormVisible4Edit =false;
this.$message.success("修改成功");
}else if(res.data.code == 20030){
this.$message.error("修改失败");
}else{
this.$message.error(res.data.msg);
}).finally(()=>{
this.getA11();
});
},

删除功能

删除要先提示是否删除,then是确定后干什么,catch是取消后干什么。然后根据id删除,删除完在finally那里记得刷新页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
handleDelete(row){
//1.弹出提示框,第一个参数是内容,第二个是标题,第三个是关于框的一些设定
this.$confirm*("此操作永久删除当前数据,是否继续?"."提示",{
type:'info'//会有个提示的图标
}).then(()=>{
//2.做删除业务
axios.delete("/books/"+row.id).then((res)=>{
if(res.data.code == 20021){
this.$message.success("删除成功");
}else{
this.$message.error("删除失败");
}
}).catch(()=>{
//3.取消删除
this.$message.info("取消删除操作");
}).finally(()=>{
this.getA11();
});
}

拦截器

拦截器概念

浏览器发送请求会发送到tomcat服务器,拦截这个请求后会划分成静态和动态资源,静态的一般直接返回,动态的就是经过过滤器,过完过滤器后就会交给处理器工作,也就是mvc,然后先看访问的是什么请求,这个地方就是中央控制器,中央控制器根据你的访问将它分发到具体的控制器方法中,让它执行操作,操作完会返回一个数据,比如json。

image-20260105033140661

然后我们想要在每个Controller访问之前做一件事,比如说权限控制,有的人觉得在controller后也需要一些事,所以就这样产生了拦截器

  • 拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMvc中动态拦截控制器方法的执行
  • 作用
    • 在指定的方法调用前后执行预先设定后的的代码
    • 阻止原始方法的执行

拦截器与过滤器区别:

  • 归属不同:Filter属于Servlet技术,Interceptor属于SpringMVc技术
  • 拦截内容不同:Filter对所有访问进行增强,Interceptor仅针对SpringMvVc的访问进行增强

弹幕:springmvc核心就是一个servlet做请求分发,filter是在进入servlet之前,interceptor是在进入servlet之后

入门案例

准备的类和之前的差不多,过滤器放在表现层里,在controller新建interceptor包,这个单词要记住,然后新建ProjectInterceptor。继承的这个方法后发现不需要实现方法,这是因为接口里面都有默认方法,然后把这三个方法都覆盖掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component//受mvc管理
public class ProjectInterceptor implements HandlerInterceptor {
@override
public boolean preHandle(HttpservletRequest request, HttpservletResponse response, object handler) throws Exception{
System.out.println("preHandle...");
return true;//如果改成false就会只执行到preHandle...然后中止往后的所有操作
}

@0verride
public void postHandle(HttpservletRequest reguest, HttpservletResponse response, obiect handler, ModelAndview modelAndView) throws Exception{
System.out.println("postHandle...");
}

@0verride
public void afterCompletion(HttoservletReguest reguest, HttoservletResponse response, obiect handler, Exception ex) throws Exception{
System.out.println("aftercompletion...");
}

记得让mvc扫这个包

然后在config新建一个SpringMvcSupport,这个是过滤访问静态资源的,之前有写过。拦截器和过滤的写法区别差不多,只不过注册的东西不一样。下面括号里的拦截器可以通过自动注入来加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class SpringMvcSupport extends WebMvcConfigurationsupport
@Autowired
private ProjectInterceptor projectIntereptor;

@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry){
registry.addResourceHandler( "/pages/**").addResourceLocations("/pages/");
}//访问page路径时指定访问到什么目录下的文件

@override
protected void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(projectIntereptor).addPathPatterns("/books","/books/*");//添加指定拦截器,添加被拦截路径,第二个是以books开头的全拦截,想添加其他的可以继续添加
}

记得也要让mvc加载这个包

然后前端发送请求去调试,返回了以下信息

1
2
3
4
preHandle...这是在什么什么之前
book save...Book{书名='springmvc实战',价格=66.8}
postHandle..这是在什么什么之后
afterCompletion...在完成之后

不过SpringMvcSupport可以简化开发,在mvcConfig里可以继承这个方法,里面可以实现的方法就有之前SpringMvcSupport的两个addResourceHandlers和addInterceptors,开头的protected换成了public

1
2
3
4
5
6
7
8
9
@Configuration
@ComponentScan({"com.itheima.controller"})
@EnablewebMvc
public class springMvcconfig implements WebMvcConfigurer {
@Autowired
private ProjectInterceptor projectInterceptor;
@override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(projectInterceptor).addPathPatterns("/books","/books/*");

但是这种简化开发的侵入式较强

拦截参数

上面的参数都是用来干什么的呢,就是现在的程序执行以后可以通过这些参数取数据。对于HandlerMethod这个类里面封装了现在执行的方法,用的是反射,然后通过强转然后调用getMethod来获取方法之类的

1
2
3
4
5
6
7
8
9
10
11
@Component//受mvc管理
public class ProjectInterceptor implements HandlerInterceptor {
@override
public boolean preHandle(HttpservletRequest request, HttpservletResponse response, object handler) throws Exception{
String contentType = request.getHeader("Content-Type");//获取数据。会输出application/json
//System.out.println(handler.getClass());输出了class org.springframework.web.method.HandlerMethod,
HandlerMethod hm = (HandlerMethod)handler;
hm.getMethod().~~~调用想获取的反射
System.out.println("preHandle..."+ contentType);
return true;
}

modelAndView封装了mvc进行页面跳转的相关数据。下面那个ex可以拿到原始程序执行过程中的异常,但我们有异常统一处理机制所以也没有这个需求了

1
2
3
4
5
6
7
8
9
@0verride
public void postHandle(HttpservletRequest reguest, HttpservletResponse response, obiect handler, ModelAndview modelAndView) throws Exception{
System.out.println("postHandle...");
}

@0verride
public void afterCompletion(HttoservletReguest reguest, HttoservletResponse response, obiect handler, Exception ex) throws Exception{
System.out.println("aftercompletion...");
}

preHandle

  • 参数
    • request:请求对象
    • response:响应对象
    • handler:被调用的处理器对象,本质上是一个方法对象,对反射技术中的Method对象进行了再包装
  • 返回值
    • 返回值为false,被拦截的处理器将不执行

postHandle

  • modelAndview:如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整

afterCompletion

  • ex:如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理

多个拦截器执行顺序

  • 当配置多个拦截器时,形成拦截器链
  • 拦截器链的运行顺序参照拦截器添加顺序为准
  • 当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行
  • 当拦截器运行中断,仅运行配置在前面的拦截器的afterCompletion操作

全成功就是运行到post1后,再把所有的after执行完,如果3是false,就会中止拦截器运行,直接跳到after2和1运行完,以此类推,如果1就是false,那么后面的都不运行

image-20260106160327882

拦截器链的运行顺序
preHandle:与配置顺序相同,必定运行
postHandle:与配置顺序相反,可能不运行
aftercompletion:与配置顺序相反,可能不运行

Maven进阶

分模块开发与设计

白学警告

将原始模块按照功能拆分成若干个子模块,方便模块间的相互调用,接口共享

image-20260106161601387

右击新建个maven模块,取名叫maven_03_pojo,在这个里面有pom.xml,然后再java里建个con.itheima.domain,把maven_02_ssm里的domain里的Book剪切到03的里面,这里就要思考如果我们要在02里调用03的book应该怎么弄

可以打开02的pom.xml添加以下这行,这行是03的pom.xml里复制过来的,然后在右侧的maven管理里也能看到多了个这个的依赖

1
2
3
4
5
6
7
<dependencies>
<!--依赖domain运行-->
<dependency>
<groupId>com.itheima</groupId>
<artifactId>maven 03 pojo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

maven想要真正运行最终要执行的是生命周期里的操作,点击compile,发现提示了error:Could not resolve dependencies for project com.itheima:maven 02 ssm:war:1.8-SNAPsHoT: Could not find artifact com.itheima:maven 03_pojo:jar:1.0-SNAPSHOT ->[Help 1],引用03的时候找不到

我们去本地仓库找,maven-repository-com-itheima这个路径找不到对应的文件夹,但是其他的仓库是能找到的,也就是本地仓库没有这个资源坐标,要保证可移植性肯定是需要仓库也有的,所以需要打包进去,点击maven指令里的install,将当前的模块安装到本地仓库中,安装的时候注意是选择安装03

依赖管理

依赖指当前项目运行所需的jar,一个项目可以设置多个依赖

打开右侧的maven管理,前面有箭头的代表当前的依赖又依赖了别的东西,比如02里面有03和04,而04又依赖了03,那么把02里面的03注释掉,依然可以正常使用,这说明依赖具有传递性

  • 直接依赖:在当前项目中通过依赖配置建立的依赖关系
  • 间接依赖:被资源的资源如果依赖其他资源,当前项目间接依赖其他资源

依赖传递冲突问题

  • 路径优先:当依赖中出现相同的资源时,层级越深,优先级越低,层级越浅,优先级越高
  • 声明优先:当资源在相同层级被依赖时,配置顺序靠前的覆盖配置顺序靠后的
  • 特殊优先:当同级配置了相同资源的不同版本,后配置的覆盖先配置的,以最后配置的为准

image-20260107190516992

这个按钮显示了依赖的关系和深度

可选依赖和排除依赖

可选依赖指对外隐藏当前所依赖的资源–不透明

如果不想让别人知道我的依赖里用过什么东西,想给隐藏起来,就可以加一个标签

1
2
3
4
5
6
7
<dependency>
<groupId>com.itheima</groupId>
<artifactId>maven 03 pojo</artifactId)
<version>1.0-SNAPSHOT</version>
<!--可选依赖是隐藏当前工程所依赖的资源,隐藏后对应资源将不具有依赖传递性-->
<optional>true</optional>加这个
</dependency>

排除依赖是指在当前引用的坐标中,将它的依赖从你的依赖关系中去除,是主动断开依赖的资源

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>com.itheima</groupId>
<artifactId>maven 04 dao</artifactId>
<version>1.0-SNAPSHOT</version>
<!--排除依赖是隐藏当前资源对应的依赖关系-->
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>

继承与聚合

聚合

  • 聚合:将多个模块组织成一个整体,同时进行项目构建的过程称为聚合
  • 聚合工程:通常是一个不具有业务功能的“空”工程(有且仅有一个pom文件)
  • 作用:使用聚合工程可以将多个工程编组,通过对聚合工程进行构建,实现对所包含的模块进行同步构建
    • 当工程中某个模块发生更新(变更)时,必须保障工程中与已更新模块关联的模块同步更新,此时可以使用聚合工程来解决批量模块同步构建的问题

新建一个模块,里面只有一个pom.xml,之前的打包方式都是jar之类的,这里为pom的打包方式

管理的话用module标签,路径是以pom.xml开始,往上找一级,标签里的顺序不会影响效果,因为他们是按照依赖关系构建,比如a依赖b,就会先构建b,先把没有依赖的构建

1
2
3
4
5
6
7
8
9
10
11
<groupId>com.itheima</groupId>
<artifactId>maven 01_parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<!--设置管理的模块名称-->
<modules>
<module>../maven_02_ssm</module>
<module>../maven_03_pojo</module>
<module>../maven_04_dao</module>
</modules>

继承:

  • 概念:继承描述的是两个工程间的关系,与java中的继承相似,子工程可以继承父工程中的配置信息,常见于依赖关系的继承
  • 作用:
    • 简化配置
    • 减少版本冲突

这里我们让1为父工程,其他为子工程,继承关系要在子类中描述,现在在2的pom.xml里加入以下代码,relativePath是用来快速定位父工程路径的

1
2
3
4
5
6
7
<!--配置当前工程继承自parent工程-->
<parent>
<groupId>com.itheima</groupId>
<artifactId>maven 0l parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../maven 01 parent/pom.xml</relativePath>
</parent>

然后把子工程中的坐标都放到父工程里统一管理

如果2和3都要用到junit,而4不用,那可以在1里定义依赖管理,这里的不会让子工程都会继承,而是提供可选依赖资源

1
2
3
4
5
6
7
8
9
10
11
<!--定义依赖管理-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>

子工程就还是写dependency,但是不要加版本,因为它用的是父工程里的版本。

1
2
3
4
5
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope)
</dependency>

子工程中使用父工程中的可选依赖时,仅需要提供群组id和项目id,无需提供版本,版本由父工程统一提供,避免版本冲突。子工程中还可以定义父工程中没有定义的依赖关系

聚合与继承的区别

  • 作用
    • 聚合用于快速构建项目
    • 继承用于快速配置
  • 相同点:
    • 聚合与继承的pom.xm1文件打包方式均为pom,可以将两种关系制作到同一个pom文件中
    • 聚合与继承均属于设计型模块,并无实际的模块内容
  • 不同点:
    • 聚合是在当前模块中配置关系,聚合可以感知到参与聚合的块有哪些
    • 继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己

属性

前面的管理版本号还是有点麻烦,在java中学过用变量来定义版本号,maven的属性就是干这个的

1
2
3
4
5
6
7
8
9
10
11
12
<dependencys>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencys>

<!--定义属性-->
<properties>
<spring.version>5.2.10.RELEASE</spring.version>
</properties>

既然有了属性,能不能把jdbc.properties里的数据也做统一管理呢,也可以,同样是在下面标签里加上原本的数据库地址,但是只能在这个pom文件里使用,不能在配置文件里使用,如果想扩大范围可以使用build标签配置资源文件,加上路径和开启过滤规则,不过这个方法并不常用

1
2
3
4
5
6
7
8
9
10
11
12
<properties>
<jdbc.url>jdbc:mysql://127.0.0.1:3306/ssm db</jdbc,url>
</properties>

<build>
<resources>
<resource>
<directory>../maven 02 ssm/src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>

如何查看效果,只需要把02这个工程打包成war包,看看里面的配置有没有加载上值就行,找到本地仓库maven_repository_com_itheima把里面的东西清空,然后在maven点install,如果打包提示必须要web.xml,就需要新建一个,因为war包最起码要有一个web.xml。然后查看war文件里webinf里classes找到配置文件,看有没有生效就行

但是这样不规范,因为这里只配置了02,要是0304都配就要写好多,但这里面只支持写一个directory,所以就要改变代码,${project.basedir}代表了当前项目所在的目录,而后面的020304会继承这个,就不会局限在哪一个工程了

1
<directory>${project.basedir}/src/main/resources</directory>

这里是刚才的web.xml根本性处理方案,是如果没有这个文件,你不要给我报错。而只有02这个工程有这个插件,所以要在这里的pom.xml写,这下面的plugin是一个插件,里面写maven打war包的那个插件,坐标从maven>repository>org>apachemaven >plugins这个路径里找,org.apache.maven.plugins这个就是。后面要写对应的配置,这个标签的意思是如果遇到了没有web.xml是否报错,写false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.1</version>
<configuration>
<port>80</port>
<path>/</path>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.3</version>
<configuration>
<failonMissingWebXml>false<fail0nMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>

上面用到的${project.basedir}是maven内置的系统属性

  1. image-20260113184231237

版本管理

  • 工程版本:
    • SNAPSHOT(快照版本)
      • 项目开发过程中临时输出的版本,称为快照版本
      • 快照版本会随着开发的进展不断更新
    • RELEASE(发布版本)
      • 项目开发到进入阶段里程碑后,向团队外部发布较为稳定的版本,这种版本所对应的构件文件是稳定的,即便进行功能的后续开发,也不会改变当前发布版本内容,这种版本称为发布版本
  • 发布版本
    • alpha版
    • beta版
    • 纯数字版

多环境配置与应用

maven提供配置多种环境的设定,帮助开发者使用过程中快速切换环境

在01父工程里配置多环境开发

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
<!--配置多环境-->
<profiles>
<!--开发环境-->
<profile>
<id>env_depk/id>
<properties>
<jdbc.url>jdbc:mysq1://127.0.0.1:3306/ssm_db</jdbc.url>
</properties>
<!--设定是否为默认启动环境-->
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<!--生产环境-->
<profile>
<id>env_pro/id>
<properties>
<jdbc.url>jdbc:mysq1://127.2.2.1:3306/ssm_db</jdbc.url>
</properties>
</profile>
<!--测试环境-->
<profile>
<id>env_depk/id>
<properties>
<jdbc.url>jdbc:mysq1://127.0.0.1:3306/ssm_db</jdbc.url>
</properties>
</profile>
</profiles>

上面设置默认启动环境太麻烦了,也可以换种方式,点击这里,输入执行指令mvn install -P env_test,-p代表指定环境,运行就可以了

image-20260113190505918

跳过测试

点一下这个按钮,test就变成了灰色的

image-20260113191213505

但是这个会全部跳过,能不能指定跳过哪些呢。在build里写,而测试是一个插件,所以要在插件标签里写配置,里面是跳过测试。然后比如某个测试用例不参与,其他的都参与,就要把

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<build>
<resources>
<!--设置资源目录,并设置能够解析$}-->
<resource>
<directory>${project.basedir}/src/main/resources</directory>
<filtering>true</filtering)
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<skipTests>true</skipTests>
<!--排除掉不参与测试的内容-->
<excludes>
<exclude>**/BookServiceTest.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

用指令就是mvn package -D skip

私服

下载完去bin目录运行cmd,然后执行nexus.exe /run nexus

访问地址是localhost:8081,用户名admin,密码在他提供的路径里找到admin.password里

  • 修改基础配置信息
    • 安装路径下etc目录中nexus-default.properties文件保存有nexus基础配置信息,例如默认访问端口
  • 修改服务器运行配置信息
    • 安装路径下bin目录中nexus.vmoptions文件保存有nexus服务器启动对应的配置信息,例如默认占用内存空间

image-20260114153514985

现在idea打包到本地仓库,然后本地仓库上传到私服,但这样的话需要本地仓库配置访问权限,也就是访问私服的用户名和密码,还要告诉上传的位置,下载也是同理

找到maven的conf里的setting.xml,找到servers标签,复制一个server标签然后修改

1
2
3
4
5
6
<!--配置访间字符的权限-->
<server>
<id>私服中的服务器id名称</id>
<username>admin</username>
<password>admin</password>
</server>

在仓库里找到这个选项,点击创建仓库,然后选maven2(hosted),然后写上名字itheima-snapshot,下面的version policy要选同样的snapshot,其他的不动直接创建

image-20260114154330189

然后还缺一个配置,就是当前项目要发布到哪一个私服仓库中,在01pom文件里配置,一个是正式版,一个是快照版

1
2
3
4
5
6
7
8
9
10
11
<!--配置当前工程保存在私服中的具体位置-->
<distributionManagement>
<repository>
<id>itheima-release</id>
<url>http://localhost:8081/repository/itheima-release/</ur1>
</repository>
<snapshotRepository>
<id>itheima-snapshot</id>
<url>http://localhost:8081/repository/itheima-snapshot/</url>
</snapshotRepository>
</distributionManagement>

然后在maven选项中选择deploy,这个是上传到私服,本地的是用install,上传完后查看仓库,发现release里没有东西但snapshot有,这是因为在01pom文件里的版本号叫snapshot,要改成release

1
2
3
4
<groupId>com.itheima</groupId>
<artifactId>maven 01 parent</artifactId>
<version>1.0-RELEASE</version>
<packaging>pom</packaging>

下面这个地方可以换取阿里的坐标用来下载

image-20260114155647714

SpringBoot

SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化Spring应用的初始搭建以及开发过程

入门案例

创建时选择image-20260114182813129然后选择好jdk版本,按照下图设置,name随便写,描述随便写,包名只要前面的

“+id);
return “hello , spring boot!”;
}

1
2
3
4
5
6
7
8

点开Application查看代码,意思就是运行这个东西,然后把Application传进来,再加个参数就行了

~~~java
@springBootApplication
public class Application {
public static void main(string[] args){
SpringApplication.run(Application.class,args);

让后点开pom,把里面的name标签和description删掉,往上有个parent标签,这个是boot的核心继承,下面那个是前面点web所产生的东西

1
2
3
4
5
6
7
8
9
10
11
12
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/><!-- lookup parent from repository -->
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

基于idea开发SpringBoot程序需要确保联网且能够加载到程序框架结构

Springboot项目快速启动

如果前端调试程序需要链接后端,但不在同一台电脑上,后端可以使用maven的package进行打包,这个包在target里,把打好的jar包发给前端就行,运行方式是在这个文件目录启动cmd,然后输入java -jar 包名.jar就行了,而且需要的其他jar包也会一起打包好

jar支持命令行启动需要依赖maven插件支持,请确认打包时是否具有SpringBoot对应的maven插件

Springboot概述

SpringBoot是由Pivota1团队提供的全新框架,其设计目的是用来简化Spring应用的初始搭建以及开发过程

在上面的parent标签里,点击spring-boot-starter-parent进去可以看到这个又继承了spring-boot-dependencies,这里程序就已经准备好了要用的资源的版本管理,只需要写上需要的依赖不加版本就可以

下面这个是帮我们一次性写了若干依赖,意思是起步依赖,点进去都可以看到

1
2
3
4
5
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
  • starter
    • SpringBoot中常见项目名称,定义了当前项目使用的所有项目坐标,以达到减少依赖配置的目的
  • parent
    • 所有SpringBoot项目要继承的项目,定义了若干个坐标版本号(依赖管理,而非依赖),以达到减少依赖冲突的目的
    • spring-boot-starter-parent(2.5.0)与 spring-boot-starter-parent(2.4.6)共计57处坐标版本不同
  • 实际开发
    • 使用任意坐标时,仅书写GAV中的G和A,V由SpringBoot提供
    • 如发生坐标错误,再指定version(要小心版本冲突)

上面这些都是辅助功能

springboot启动方式

  • 启动方式

    1
    2
    3
    4
    @SpringBootApplication
    public class Springboot010uickstartApplicationf
    public static void main(string[] args){
    SpringApplication.run(Springboot01QuickstartApplication.class, args);
  • SpringBoot在创建项目时,采用jar的打包方式

  • SpringBoot的引导类是项目的入口,运行main方法就可以启动项目

这里我们想换成jetty服务器,不用tomcat,在pom里的org.springframework.boot里面是有tomcat的,并且是源码不能随意更改不能用可选依赖,所以我们这里用到之前学过的排除依赖,然后再加上jetty的依赖就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

Jetty比Tomcat更轻量级,可扩展性更强(相较于Tomcat),谷歌应用引擎(GAE)已经全面切换为Jetty

基础配置

修改服务器端口

  • SpringBoot提供了多种属性配置方式
    在resources里有个application.properties,在里面添加

    1
    server.port=80
  • 在这个目录下新建一个application.yml

    1
    2
    server:
    port:81
  • 新建一个application.yaml

    1
    2
    server:
    port:82

如果三个文件都有的情况下,properties为主启动文件,然后是yml最后是yaml

SpringBoot核心配置文件名为application
SpringBoot内置属性过多,且所有属性集中在一起修改,在使用时,通过提示键+关键字修改属性

yaml

  • YAML(YAML Ain’t Markup Language),一种数据序列化格式
  • 优点:
    • 容易阅读
    • 容易与脚本语言交互
    • 以数据为核心,重数据轻格式
  • YAML文件扩展舎
    • .ym1(主流)
    • .yam1

yaml语法规则

  • 大小写敏感
  • 属性层级关系使用多行描述,每行结尾使用冒号结束
  • 使用缩进表示层级关系,同层级左侧对齐,只允许使用空格(不允许使用Tab键)
  • 属性值前面添加空格(属性名与属性值之间使用冒号+空格作为分隔)
  • 表示注释
  • 核心规则:数据前面要加空格与冒号隔开

yaml数组数据

数组数据在数据书写位置的下方使用减号作为数据开始符号,每行书写一个数据,减号与数据间空格分隔

1
2
3
4
5
6
enterprise:
tel:4006184000
subject:
- Java
- 前端
- 大数据

yaml数据读取

可以通过下面的方式读取上面yml文件的数据

1
2
3
4
5
6
7
8
9
10
@RestController
@RequestMapping("/books")
public class Bookcontroller {
@Value("${enterprise.tel}")
private Integer tel;

//数组
@Value("${enterprise.subject[0]}")
private Integer subject;

也可以使用加载环境信息全部获取

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping(©v"/books")
public class Bookcontroller{
@Autowired
private Environment environment;

@GetMapping("/{id}")
public String getById(@PathVariable Integer id){
System.out.println(environment.getProperty("lesson"));

第三种方式是比较常用的,先定义一个实体类,内容和yml里的东西一一对应,要想加载这里的信息,得先让Spring控制这里,添加@Component把他加入bean,第二步要告诉从配置中读取属性,@ConfigurationProperties,里面写上前缀

1
2
3
4
5
@Component
@ConfigurationProperties(prefix = "enterprise")
public class Enterprise{
private String tel;
private String[] subject;

然后再放到上面的控制类里

1
2
3
4
5
6
@Autowired
private Enterprise enterprise,

@GetMapping("/{id}")
public String getById(@PathVariable Integer id){
System.out.println(enterprise);

自定义对象封装数据警告解决方案

把这个加在依赖里就可以

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

多环境开发配置

分隔环境在yml里用三个横线分隔,区分什么是开发测试的话用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#设置启用的环境
spring:
profiles:
active: test
---
#开发
spring:
profiles: dev
server:
port:80
---
#生产
spring:
profiles: pro
server:
port: 81
---
#测试
spring:
profiles: test
server:
port: 82

现在推荐的写法:用哪个都行

1
2
3
4
5
6
7
8
9
10
11
12
#设置启用的环境
spring:
profiles:
active: dev
---
#开发
spring:
config:
activate:
on-profile: dev
server:
port:80

properties的写法:

1
2
#设置启用的环境
spring.profiles.active=dev

dev在哪写呢?新建一个application-dev.properties,里面写上server.port=80,这样会读取

多环境命令行启动参数设置

前端人员在启动打好的jar包时可以通过命令来更改环境,但是在打包前一定要先执行clean,有中文注释的时候需要打开idea设置,输入encoding,把所有的编码改成utf-8

启动时先在jar包所在的文件夹里启动cmd,然后输入java -jar springboot.jar –spring.profiles.active=test,也可以临时修改参数,比如

java -jar springboot.jar –spring.profiles.active=test –server.port=88

image-20260116001823456

多环境开发兼容问题

开发的时候jar最终要在服务器上运行,而maven是用来最终打包,所以要以maven的profile为主,boot为辅

这里准备了数据,在pom里有设置多环境开发,yml里设置的是,这时候运行打好的jar包,发现最终运行的端口是80,说明yml里的dev在生效,properties里想要生效的也没有告诉yml,怎么让yml知道呢,在pom里把每个环境都设上<properties.active>,把yml里active后面改成${profile.avtive}。

上面配置的pom属性只能在pom文件里用,不会干预配置文件,想要干预配置文件就要加个下面的插件,配置里的useDefaultDelimiters设置为true,然后再设置一下字符集

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
<plugins>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.2.0</version>

<configuraton>
<encoding>UTF-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
</configuraton>
<configuration>
<useDefaultDelimiters>true</useDefaultDelimiters>
</configuration>
</plugin>

<profiles>
<!--开发环境-->
<profile>
<id>dev</id>
<properties>
<properties>dev</properties>
</properties>
</profile>
<!--生产环境-->
<profile>
<id>pro</id>
<properties>
<properties>pro</properties>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<!--测试环境-->
<profile>
<id>test</id>
<properties>
<properties>test</properties>
</properties>
</profile>
</profiles>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#设置启用的环境
spring:
profiles:
active: ${profile.avtive}
---
#开发
spring:
profiles:dev
server:
port:80
---
#生产
spring:
profiles: pro
server:
port: 81
---
#测试
spring:
profiles: test
server:
port: 82

配置文件分类

SpringBoot中4级配置文件
1级:file :config/application.yml【最高】
2级:file :application.yml
3级:classpath:config/application.yml
4级:classpath:application.yml【最低】
作用:
1级与2级留做系统打包后设置通用属性
3级与4级用于系统开发阶段设置通用属性

整合第三方技术

image-20260116011651976

上面两个类其实都是统一的名字,如果都一样的话是不是就不用写了,boot给做了整合。随便弄了个Service,然后到测试类去做测试,下面的方法是不重要的可以随意修改,@SpringBootTest里面整合了运行器,在这里没有写需要加载的配置文件,但是有没有加载?加载了,在Springboote7TestApplication这个类(运行的那个)就起到了加载配置类的作用,他会把他所在包及其子包全部扫描一遍

1
2
3
4
5
6
7
8
@SpringBootTest
class springboot07TestApplicationTests{
@Autowired
private BookService bookService;
@Test
//void contextLoads(){//这个是创建时自带的,可以随意修改
public void save(){//改成了这样
bookService.save();

如果就是换地方了,可以通过指定参数把类名指定上就行了

@SpringBootTest(class = Springboot07TestApplication.class)

基于SpringBoot整合mybatis

创建一个新模块,把mybatis framework依赖加上,还有mysql的

然后准备下面代码

1
2
3
4
5
public class Book{//还有setget方法
private Integer id;
private string name;
private string type;
private string description;
1
2
3
public interface BookDao {
@Select("select *from tbl_book where id = #{id}")
public Book getById(Integer id);

还有个Application.yml,serverTimezone=UTC是设置时区,这个是boot2.4.2以前的会报错,后面的就不用加了

1
2
3
4
5
6
spring:
datasource:
driver-class-name:com.mysql.cj.jdbc.Driver
url:jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username:root
password: root

然后到测试类,

1
2
3
4
5
6
7
8
@SpringBootTest
class springboote8MybatisApplicationTests{
@Autowired
private BookDao bookDao;
@Test
void testGetById(){
Book book = bookDao.getById(1);
System.out.println(book);

运行时提示错误的创建了一个bean,依赖的bookDao加载失败了,因为这样配BookDao这是一个接口,那我们的对象是什么?是通过自动代理得到的实现类,而Spring怎么知道这个类要做自动代理呢,之前是@MapperScanner然后里面是包名,这里只需要写@Mapper

如果需要其他的数据库源,就先加载对应的依赖,然后在上的yml最后面加上

1
type: com.alibaba.druid.pool.DruidDatasource

image-20260116015221587

案例:基于boot的ssm整合案例

之前的config包全部删掉,domain不需要变化,dao里需要加一个@Mapper,作用是让boot扫描到这里并且生成自动代理,Service包不需要动,controller也不需要,然后在resources包里把Application.properties后缀改成yml,然后把jdbc.properties里的东西放到yml里,测试类就是上面那些

image-20260116183738833

mybatis plus

MyBatisPlus简介

MyBatisPlus(简称MP)是基于MyBatis框架基础上开发的增强型工具,旨在简化开发、提高效率

  • SpringBoot整合MyBatis开发过程(复习)
    • 创建SpringBoot工程
    • 勾选配置使用的技术(mybatis、mysql)
    • 设置dataSource相关属性(JDBC参数)
    • 定义数据层接口映射配置

新建一个模块选择mysql,如果没有mybatisplus就手动加,再加一个druid,然后在yml里配置数据库,新建一个UserDao的接口,继承BaseMapper就可以获得所有需要的查询方法

1
2
3
4
5
6
@Mapper
public interface UserDao {
public User getById(Long id);
//以前是这么写,如果用plus就里面什么也不写,然后继承BaseMapper加一个泛型,写实体类名
@Mapper
public interface UserDao extends BaseMapper<User>{}
1
2
3
4
5
6
7
8
@SpringBootTest
class Mybatispluse1QuickstartApplicationTests{
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
List<User>userList = userDao.selectList(null);
System.out.println(userList);

MyBatisPlus特性:

  • 无侵入:只做增强不做改变,不会对现有工程产生影响
  • 强大的 CRUD 操作:内置通用 Mapper,少量配置即可实现单表CRUD 操作
  • 支持 Lambda:编写查询条件无需担心字段写错
  • 支持主键自动生成
  • 内置分页插件

标准数据层开发

image-20260117025540821

新增,但是id是按照它的规则生成的

1
2
3
4
5
6
7
8
9
@Test
void testSave(){
User user = new User();
user.setName("黑马程序员");
user.setPassword("itheima"),
user.setAge(12);
user.setTel("4006184080")
userDao.insert(user);
}

按id删除

1
2
3
@Test
void testDelete(){
userDao.deleteById(1401856123725713409L);

改,这里以前要做个判断,哪些字段为空就不更改,不为空的才更改,现在他直接自动判断

1
2
3
4
5
6
@Test
void testUpdate(){
User user = new User();
user.setId(1L);
user.setName("Tom666");
userDao.updateById(user);

1
2
3
4
@Test
void testGetById(){
User user = userDao.selectById(2L);
System.out.println(user);

然后pojo对象可以用lombok来快速生成setget方法,也可以用@Data,这个包含以下所有除了两个构造方法的所有方法

1
2
3
4
5
6
7
8
9
10
11
12
@Setter
@Getter
@Tostring
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class User {
private Long id;
private string name;
private string password;
private Integer age;
private string tel;

分页功能

selectPage要的是什么,是一个Ipage对象,那就new一个,怎么设置页码,page后面的1代表第一页,2代表有多少条

1
2
3
4
5
6
7
8
9
@Test
void testGetByPage(){
IPage page = new Page( 1,2);
userDao.selectPage(page,queryWrapper: null);
System.out.println("当前页码值:"+page.getcurrent());
System.out.println("每页显示数:"+page.getsize());
System.out.println("一共多少页:"+page.getPages());
System.out.println("一共多少条数据:"+page.getTotal());
System.out.println("数据:"+page.getRecords());

但执行时发现他显示的是一共0页,一共0条数据,然后把数据全部查询出来,正常的查询语句应该是select *from user ????? limit 1,2.,就是要在后面加上limit进行分页,但这里不是aop,是mybatisplus的拦截器,用于配置分页功能

新建一个MyConfig

1
2
3
4
5
6
7
8
9
@Configuration
public class Mpconfig {
@Bean
public MybatisPlusInterceptor mpInterceptor(){
//1.定义Mp拦截器
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
//2.添加具体的拦截器
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mpInterceptor;

然后要被加载,一种是在Application类里写上@Import加载进去,另一种是直接加@Configuration

想看sql语句可以开启mypluls日志,要在yml配置以下,调程序用的一般不开启

1
2
3
4
#开启mp的日志(输出到控制台)
mybatis-plus:
configuration:
log-impl:org.apache,ibatis.logging.stdout.stdoutImpl

DQL编程控制

MyBatisPlus将书写复杂的SQL查询条件进行了封装,使用编程的形式完成查询条件的组合

如果要关闭mybatisplus的图标显示可以在yml设置如下

1
2
3
mybatis-plus:
global-config:
banner: false

关闭springboot的是

1
2
3
spring:
main:
banner-mode:off

条件查询的使用:queryWrapper是专门做查询封装条件的,后面的lt是对应html的&lt,也就是小于号,这样就查询出年龄小于18的,大于号用gt

1
2
3
4
5
6
7
@Test
void testGetA11(){
//按条件查询
QueryWrapper qw= new QueryWrapper();
qw.lt("age",18);
List<User>userList =userDao.selectList(qw);
System.out.println(userList);

方式二,里面的是user类里的age属性的意思,注意要指定泛型

1
2
3
4
5
//方式二:lambda格式按条件查询
QueryWrapper<User> qw= new QueryWrapper<User>();
qw.lambda().lt(user::getAge, 18);
List<User>userList =userDao.selectList(qw);
System.out.println(userList);

方式三(推荐)

1
2
3
4
5
//方式三:lambda格式按条件查询
LambdaQueryWrapper<User>lqw=new LambdaQueryWrapper<User>();
lqw.lt(User::getAge,val:10);
List<User>userList = userDao.selectList(lqw);
System.out.println(userList);

null值处理

在购物的时候会有些没选择的值,这里就成了空值,这应该怎么处理呢

新建一个query包,新建UserQuery类,这是封装User类的,继承user,然后分析user中有哪些东西可能会有上下限或者范围,比如数值型和日期型。那么这里的age2就用来描述年龄的上限,user里的age就描述年龄的下限

1
2
3
@Data
public class UserQuery extends User {
private Integer age2;

然后到test中,模拟年龄下限为空时会发生什么,结果是不成功,以前判断空是要加个if(null != uq.getAge()),但这样写要写很多个if,

1
2
3
4
5
6
7
8
9
10
11
//模拟页面传递过来的查询数据
UserQuery uq = new UserQuery();
//uq.setAge(10);
uq.setAge2(30);

//nuLL判定
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.lt(User: :getAge, uq.getAge2());
lqw.gt(User::getAge, uq.getAge());
List<User> userList = userDao.selectList(lqw);
System.out.println(userList) ;

lt的值可以传个boolean

image-20260117161710215

可以改造成下面的代码

1
2
3
4
5
6
7
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.lt(User: :getAge, uq.getAge2());
//先判定第一个参数是否为true,如果为true连按当前条件
lqw.lt(null != uq.getAge2(),User::getAge,uq.getAge2());
lqw.lt(null != uq.getAge(),User::getAge,uq.getAge());
List<User> userList = userDao.selectList(lqw);
System.out.println(userList) ;

查询投影

就是设置查询结果的样子。下面这个是查全集,然后设置只显示其中某一些字段

1
2
3
4
5
6
7
8
9
//查询投影
LambdaQueryWrapper<User>lqw= new LambdaQueryWrapper<User>();
//select这个专门设置查询的字段名,只适用于lambda格式
lqw.select(User::getId,User::getName,User: :getAge);
//这个是不用lambda格式
QueryWrapper<User> lqw = new QueryWrapper<User>();
lqw.select("id","name","age");
List<User>userList =userDao.selectList(lqw);
System.out.println(userList);

现在要求查一下count,就是统计有多少数据。单纯用lqw.select(“count(*)”);的时候最终查询是个null,因为现在包装的是放在user对象里,放不进去,不能用selectList这个方法,用selectMaps,其中String代表key,Object代表value,最后输出的是count(*)=6,但是看着很怪,起个别名叫count

1
2
3
4
5
6
7
QueryWrapper<User> lqw = new QueryWrapper<User>();
lqw.select("count(*) as count, tel");
//List<User> userList = userDao.selectList(lqw);
//分组查询
lqw.groupBy("tel");
List<Map<String, Object>> userList = userDao.selectMaps(lqw);
System.out.println(userList);

查询条件

范围匹配(>、=、between)模糊匹配(like)
空判定(null)包含性匹配(in)分组(group)排序(order)

先看等匹配:eq就是equals

1
2
3
4
5
6
7
//条件查询
LambdaQueryWrapper<User> 1qw = new LambdaQueryWrapper<User>();
//意思是如果名字为Jerry并且密码是jerry
lqw.eq(User::getName,"Jerry").eq(User::getPassword,"jerry") ;
//如果是这种只查询一种,没必要用selectList,用SelectOne就行
User loginUser = userDao.select0ne(lqw);
System.out.println(loginUser);

范围查询,lt 是不带等号的,le是带等号的,gt是不带ge是带

1
2
3
4
5
LambdaQueryWrapper<User> 1qw = new LambdaQueryWrapper<User>();//范围查询LtLe gt ge eq between
//between的使用规则是有限制的,必须是从小到大
lqw.between(User::getAge,10, 30);
List<User> userList = userDao.selectList(lqw);
System.out.println(userList) ;

模糊匹配

1
2
3
4
5
LambdaQueryWrapper<User> 1qw = new LambdaQueryWrapper<User>();//模糊匹配Like
//这里的左和右是指百分号的位置,也就是J%以J开头的名字
lqw.likeRight(User::getName,"J");
List<User> userList = userDao.selectList(lqw);
System.out.println(userList) ;

更多查询条件设置参看https://mybatis.plus/guide/wrapper.html#abstractwrapper

字段映射与表明映射

  • 名称:@TableField类型:属性注解

  • 位置:模型类属性定义上方

  • 作用:设置当前属性对应的数据库表中的字段关系,也就是重命名为数据库的字段名

  • 范例:

  • 1
    2
    3
    public class User {
    @TableField(value="pwd")
    private String password;
  • 相关属性

    • value(默认):设置数据库表字段名称

问题二:编码中添加了数据库中未定义的属性

1
2
3
//这个意思是这个字段仅存在这个类里,数据库没有,排除这个
@TableField(exist = false)
private Integer online;

名称:@TableField
类型:属性注解
位置:模型类属性定义上方
作用:设置当前属性对应的数据库表中的字段关系

相关属性
value:设置数据库表字段名称
exist:设置属性在数据库表字段中是否存在,默认为true。此属性无法与value合井使用

问题三:采用默认查询开放了更多的字段查看权限

1
2
3
public class User {
@TableField(value="pwd",select = false)
private String password;

select:设置属性是否参与查询,此属性与select()映射配置不冲突

问题四:表名与编码开发设计不同步,比如表明叫tb1_user

名称:@TableName
类型:类注解
位置:模型类定义上方
作用:设置当前类对应与数据库表关系范例:

1
2
3
@TableName("tbl_user")
public class User {
private Long id;

相关属性:value:设置数据库表名称

id生成策略

  • 不同的表应用不同的id生成策略

    • 日志:自增(1,2,3,4,.)
    • 购物订单:特殊规则(FQ23948AK3843)
    • 外卖单:关联地区日期等信息(1004202003143491)
    • 关系表:可省略id
  • 名称:@TableId

  • 类型:属性注解

  • 位置:模型类中用于表示主键的属性定义上方

  • 作用:设置当前类中主键属性的生成策略

  • 范例:

    1
    2
    3
    - public class User {
    - @TableId(type = IdType.AUTo)
    - private Long id;
  • 相关属性

    • value:设置数据库主键名称
    • type:设置主键属性的生成策略,值参照IdType枚举值,有很多种

设置为AUTO后要去数据库看一下表结构,在表的选项把自动递增的值改小一点,他会自动识别最大值

设置为INPUT后要去数据库把自增策略取消,现在要求必须传id,而id需要自己指定,比如user.serId(666L)

  • AUTO(0):使用数据库id自增策略控制id生成
  • NONE(1):不设置id生成策略
  • INPUT(2):用户手工输入id
  • ASSIGN_ID(3):雪花算法生成id(可兼容数值型与字符串型)
  • ASSIGN_UUID(4):以UUID生成算法作为id生成策略

雪花算法是生成一个串,是一个由64位二进制组成的,是一个long值

image-20260119033209429

也可以弄个全局设置,在yml里加上

1
2
3
4
mybatis-plus:
global-config:
db-config:
id-type:assign_id

多记录操作

用这个方法deleteBatchIds

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//批量删除
@Test
void testDelete(){
List<Long> list = new ArrayList<>();
list.add(1402551342481838081L);
list.add(1402553134049501186L);
list.add(1402553619611430913L);
userDao.deleteBatchIds(list);

//批量查询
List<Long> list = new ArrayList<>();
list.add(1402551342481838081L) ;
list.add(1402553134049501186L);
list.add(1402553619611430913L);
userDao.selectBatchIds();

逻辑删除

删除操作业务问题:业务数据从数据库中丢弃

逻辑删除:为数据设置是否可用状态字段,删除时设置状态字段为不可用状态,数据保留在数据库中,比如设置1就是删除了,0就是没删,但都还保留着

先在数据库添加deleted字段,默认值为0,然后到User类加个逻辑删除字段,标记当前记录是否被删除

1
2
3
//逻辑删除字段,标记当前记录是否被删除,里面的值都是默认值,要与数据库的对应
@TableLogic(value = "0" ,delval = "1")
private Integer deleted;

执行后的sql语句:

1
UPDATE tbl_user SET deleted=1 WHERE id=? AND deleted=0 

这个也有在配置中搞定,第一个是用哪个字段做通用的逻辑,第二个是没有删除的东西标记是几,第三个是被逻辑删除的字段对应的值是几

image-20260119035440820

乐观锁

业务并发现象带来的问题:秒杀

要现在数据库加个字段用来标记当前谁在操作这条数据,然后在user里加下面字段

1
2
@Version
private Integer version;

这个锁的原理是比如一个sql语句update set abc=1,如果我有锁就where version = 1,但如果别人拿到了也是1,那么效果就达不到了,所以修改的时候就变成version=version+1,这样不断的累加就会不一样

在之前的分页里mp是通过拦截器帮我们把sql语句里加上,下面第二个拦截器是之前分页的拦截器

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class MpConfig {
@Bean
public MybatisPlusInterceptor mpInterceptor() {
//1.定义Mp拦截器
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
//2.添加具体的拦截器
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
//3.添加乐观锁拦截器
mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mpInterceptor;

在使用这个的时候要记得加上version值,不提供就没有锁机制。下面这个是test类

1
user.setVersion(1);

输出的结果是

1
2
 UPDATE tb1_user SET name=?, version=? WHERE id=? AND version=? AND deleted=0
Parameters: Jock666(String), 2(Integer), 3(Long), 1(Integer)

如果不用锁,就是下面代码,这里面是有version的,可以不从前台拿到version但是要保证这里有version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//1.先通过要修改的数据id将当前数据查询出来
User user = userDao.selectById(3L);
//2.将要修改的属性逐一设置进去
user.setName("Jock888");

模拟一下两个用户同时操作
//1.先通过要修改的数据d将当前数据查询出来
User user = userDao.selectById(3L);
//version=3
User user2 = userDao.selectById(3L);
//version=3
user2.setName("Jock aaa");
userDao.updateById(user2) ;
//version=>4
user.setName("Jock bbb");
userDao.updateById(user);
//verion=3?条件还成立吗?就不成立了

也就是上面的执行完version已经是4了,下面的就不成立,所以执行失败

image-20260119235730228

代码生成器

image-20260120173126665

其实有些信息是可以读取数据库获取的

我们新建一个项目,pom里面新添两个坐标

1
2
3
4
5
6
7
8
9
<!--代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId><version>3.4.1</version>
</dependency>
<!--veLocity模板引擎,模板技术是一个独立的技术-->
<dependency>
<groupId>org.apache.velocity</groupId><artifactId>velocity-engine-core</artifactId><version>2.3</version>
</dependency>

然后新建一个类,先创建代码生成器的对象,然后执行代码生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Generator{
public static void main(string[]args){
AutoGenerator autoGenerator = new AutoGenerator();

//设置数据库配置
DataSourceConfig dataSource = new DataSourceConfig();
dataSource.setDriverName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTc");
dataSource.setUsername("root");
dataSource.setPassword("root");
autoGenerator.setDataSource(dataSource);

autoGenerator.execute();//执行代码

执行后就生成了一些文件,文件名叫conf,但是生成的路径不太对,就可以配置一些东西

1
2
3
4
5
6
7
8
9
10
//设置全局配置
GlobalConfig globalConfig = new GlobalConfig();
//这个是准备输出到什么位置,点进setoutputDir查看源码,默认是输出到d盘根目录里,所以后面的配置是输出到用户目录的
globalconfig.setoutputDir(System.getProperty("user.dir")+"/mybatisplus_04_generator/src/main/java");
globalconfig.setOpen(false);//设置生成完毕后是否打开生成代码所在的目录
globalConfig.setAuthor("黑马程序员");//设置生成的作者
globalConfig.setFileoverride(true);//后生成的要不要覆盖前面生成的文件
globalConfig.setMapperName("%sDao");//设置数据层接口名,%s为占位符(数据库里的表名),指代模块名称,比如按照我们的设置生成的文件名就叫xxx_xxxDao,如果不设置这个会按照他自己的规则生成xxxMapper
globalConfig.setIdType(IdType.ASSIGN_ID); //设置Id生成策略,会在实体类里自动加上@TableId(value = "id",type = IdType.ASSIGN_ID)
autoGenerator.setGlobalConfig(globalConfig);

然后是包名相关配置,创建出来的实体类不叫domain,叫entity,dao包叫mapper

1
2
3
4
5
6
//设置包名相关配置
PackageConfig packageInfo = new PackageConfig();
packageInfo.setParent("com.itheima”);//设置生成的包名,与代码所在位置不冲突,二者叠加组成完整路径
packageInfo.setEntity("domain");//设置实体类包名
packageInfo.setMapper("dao");//设置数据层包名
autoGenerator.setPackageInfo(packageInfo);

然后是策略配置

1
2
3
4
5
6
7
8
9
10
11
//策略设置
StrategyConfig strategyConfig = new StrategyConfig();
strategyConfig.setInclude("tbl_user");//设置当前参与生成的表名,参数为可变参数,就是只生成哪些表
strategyConfig.setTablePrefix("tbl_"); //设置数据库表的前缀名称,模块名=数据库表名-前缀名例如:User =tbl_user就变成了user
strategyConfig.setRestControllerStyle(true);//设置是否启用Rest风格
strategyConfig.setEntityLombokModel(true);//设置是否启用Lombok
strategyConfig.setVersionFieldName("version");//设置乐观锁字段名strategyConfig.setLogicDeleteFieldName("deleted");//设置逻辑删除字段名

autoGenerator.setStrategy(strategyConfig);
//2.执行生成操作
autoGenerator.execute();

运行后controller里的注解就自动加上@RestController,然后User里加上了@Data,多了个@EquealsAndHashCode(callSuper = false),这个是生成的equals和hashcode是否继承父类的equals和hashcode方法,然后下面的deleted和version都加上了注解