SSM学习笔记(Spring篇Day03)

一、Spring 整合 MyBatis

1. 代码实现
(1) 创建Spring的主配置类
java 复制代码
//配置类注解
@Configuration
//包扫描,主要扫描的是项目中的AccountServiceImpl类
@ComponentScan("com.itheima")
public class SpringConfig {
}
(2) 创建数据源的配置类

配置类中完成数据源的创建

java 复制代码
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.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }
}
(3) 主配置类中读properties并引入数据源配置类
java 复制代码
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import(JdbcConfig.class)
public class SpringConfig {
}
(4) 创建Mybatis配置类并配置SqlSessionFactory
java 复制代码
public class MybatisConfig {
    //定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        //设置模型类的别名扫描
        ssfb.setTypeAliasesPackage("com.itheima.domain");
        //设置数据源
        ssfb.setDataSource(dataSource);
        return ssfb;
    }
    //定义bean,返回MapperScannerConfigurer对象
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.itheima.dao");
        return msc;
    }
}

说明:

① 使用SqlSessionFactoryBean封装SqlSessionFactory需要的环境信息

图里的代码,其实就是把 XML 配置里的关键部分 "翻译" 成了 Java 方法调用:

原生 MyBatis XML 配置 Spring Java 配置 (SqlSessionFactoryBean) 作用
<typeAliases> setTypeAliasesPackage("com.itheima.domain") 为实体类设置别名,简化 SQL 映射文件中的类名书写
<dataSource> setDataSource(dataSource) 注入数据源,MyBatis 用它来建立数据库连接

② 为什么 dataSource 参数不用自己 new?

这是 Spring 的依赖注入(DI)特性在起作用:

  • 你的项目里肯定已经配置了一个 Druid 数据源(DruidDataSource),并且它已经被注册成了一个 Spring Bean。
  • 当 Spring 初始化 sqlSessionFactory() 这个方法时,发现它需要一个 DataSource 类型的参数,就会自动在容器里寻找一个符合类型的 Bean(也就是你的 Druid 数据源),并自动注入进来。

③ 使用MapperScannerConfigurer加载Dao接口,创建代理对象保存到IoC容器中

(5) 主配置类中引入Mybatis配置类
java 复制代码
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}
2. 执行流程
(1) 执行new AnnotationConfigApplicationContext(SpringConfig.class)时,Spring 开始启动:
  • @ComponentScan("com.itheima"):扫描指定包下所有带@Service/@Repository等注解的类,准备创建 Bean;
  • @PropertySource("classpath:jdbc.properties"):加载数据库配置文件,读取jdbc.driver/url等配置项;
  • @Import(...):导入数据源配置类(JdbcConfig)和 MyBatis 配置类(MybatisConfig),触发这两个类的解析。

二、Spring 整合 Junit

1. 代码实现
(1) 编写测试类

在test \ java下创建一个AccountServiceTest

java 复制代码
//设置类运行器
@RunWith(SpringJUnit4ClassRunner.class)
//设置Spring环境对应的配置类
@ContextConfiguration(classes = {SpringConfiguration.class}) //加载配置类
//@ContextConfiguration(locations={"classpath:applicationContext.xml"})//加载配置文件
public class AccountServiceTest {
    //支持自动装配注入bean
    @Autowired
    private AccountService accountService;
    @Test
    public void testFindById(){
        System.out.println(accountService.findById(1));

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

核心逻辑:

@RunWith(SpringJUnit4ClassRunner.class)

  • 作用:指定用 Spring 提供的专用类运行器来执行测试。
  • 为什么需要它
    • 普通的 JUnit 运行器只会执行你的测试方法,不会启动 Spring 容器。
    • 这个特殊的运行器会先启动 Spring 容器 ,然后再运行你的测试方法,这样 Spring 的所有功能(如@Autowired)才能生效。

@ContextConfiguration(classes = {SpringConfiguration.class})

  • 作用:告诉 Spring 容器,应该加载哪些配置来创建 Bean。
  • classes:如果你的配置是用 Java 类写的(比如@Configuration注解的类),就用这个属性,指定配置类。
  • locations:如果你的配置是用 XML 文件写的(比如applicationContext.xml),就用这个属性,指定 XML 文件路径。
2. 知识点
(1) @RunWith
名称 @RunWith
类型 测试类注解
位置 测试类定义上方
作用 设置JUnit运行器
属性 value(默认):运行所使用的运行期
(2) @ContextConfiguration
名称 @ContextConfiguration
类型 测试类注解
位置 测试类定义上方
作用 设置JUnit加载的Spring核心配置
属性 classes:核心配置类,可以使用数组的格式设定加载多个配置类 locations:配置文件,可以使用数组的格式设定加载多个配置文件名称

三、AOP 核心概念

1. 连接点

程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等。在SpringAOP中,理解为方法的执行,可以直接理解成所有的方法

2. 切入点

匹配连接点的式子,在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法。

连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。

3. 通知

在切入点处执行的操作,也就是共性功能。在SpringAOP中,功能最终以方法的形式呈现

4. 通知类

定义通知的类

5. 切面

描述通知与切入点的对应关系。

四、AOP 入门案例

1. 代码实现
(1) 定义通知类和通知
java 复制代码
public class MyAdvice {
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}
(2) 定义切入点
java 复制代码
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}
  • 切入点定义依托一个**不具有实际意义的方法进行,**即无参数、无返回值、方法体无实际逻辑。
(3) 制作切面
java 复制代码
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置

说明:@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行

(4) 将通知类配给容器并标识其为切面类
java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}
(5) 开启注解格式AOP功能
java 复制代码
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}
2. 知识点
(1) @EnableAspectJAutoProxy
名称 @EnableAspectJAutoProxy
类型 配置类注解
位置 配置类定义上方
作用 开启注解格式AOP功能
(2) @Aspect
名称 @Aspect
类型 类注解
位置 切面类定义上方
作用 设置当前类为AOP切面类
(3) @Pointcut
名称 @Pointcut
类型 方法注解
位置 切入点方法定义上方
作用 设置切入点方法
属性 value(默认):切入点表达式

五、AOP 配置管理

1. 切入点表达式
(1) 语法格式

execution(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)

(2) 示例
java 复制代码
execution(public User com.itheima.service.UserService.findById(int))
  • execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
  • public:访问修饰符,还可以是public,private等,可以省略
  • User:返回值,写返回值类型
  • com.itheima.service:包名,多级包使用点连接
  • UserService:类/接口名称
  • findById:方法名
  • int:参数,直接写参数的类型,多个类型用逗号隔开
  • 异常名:方法定义中抛出指定异常,可以省略
(3) 通配符

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

java 复制代码
execution(public * com.itheima.*.UserService.find*(*))

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

java 复制代码
execution(public User com..UserService.findById(..))

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

(4) 书写技巧

① 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了

② 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述

③ 通常不使用异常 作为匹配规则

接口名/类名 书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名

2. AOP 通知类型
(1) AOP通知共分为5种类型
  • 前置通知

  • 后置通知

  • 环绕通知(重点)

  • 返回后通知(了解)

  • 抛出异常后通知(了解)

(2) 通知类型的使用

① 前置通知

修改MyAdvice,在before方法上添加@Before注解

java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    //此处也可以写成 @Before("MyAdvice.pt()"),不建议
    public void before() {
        System.out.println("before advice ...");
    }
}

② 后置通知

java 复制代码
@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 ...");
    }
}

③ 环绕通知

java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    
    @Pointcut("execution(int com.itheima.dao.BookDao.select())")
    private void pt2(){}
    
    @Around("pt2()")
    public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示对原始操作的调用
        Object ret = pjp.proceed();
        System.out.println("around after advice ...");
        return ret;
    }
}

说明:

  • 为什么返回的是Object而不是int的主要原因是Object类型更通用。
  • 在环绕通知中是可以对原始方法返回值就行修改的。
(3) 环绕通知注意事项

① 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知

② 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行

③ 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型

3. 业务层接口执行效率
(1) 需求分析

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

(2) 代码实现

① 开启SpringAOP的注解功能

在Spring的主配置文件SpringConfig类中添加注解

java 复制代码
@EnableAspectJAutoProxy

② 创建AOP的通知类

  • 该类要被Spring管理,需要添加@Component

  • 要标识该类是一个AOP的切面类,需要添加@Aspect

  • 配置切入点表达式,需要添加一个方法,并添加@Pointcut

java 复制代码
@Component
@Aspect
public class ProjectAdvice {
    //配置业务层的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}
    
    public void runSpeed(){
        
    } 
}

③ 添加环绕通知

在runSpeed()方法上添加@Around

java 复制代码
@Component
@Aspect
public class ProjectAdvice {
    //配置业务层的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}
    //@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
    @Around("servicePt()")
    public Object runSpeed(ProceedingJoinPoint pjp){
        Object ret = pjp.proceed();
        return ret;
    } 
}

④ 完成核心业务,记录万次执行的时间

java 复制代码
@Component
@Aspect
public class ProjectAdvice {
    //配置业务层的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}
    //@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
    @Around("servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp){
        
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
           pjp.proceed();
        }
        long end = System.currentTimeMillis();
        System.out.println("业务层接口万次执行时间: "+(end-start)+"ms");
    } 
}

⑤ 程序优化

目前程序所面临的问题是,多个方法一起执行测试的时候,控制台都打印的是:

业务层接口万次执行时间:xxxms

我们没有办法区分到底是哪个接口的哪个方法执行的具体时间,具体如何优化?

java 复制代码
@Component
@Aspect
public class ProjectAdvice {
    //配置业务层的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}
    //@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
    @Around("servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp){
        //获取执行签名信息
        Signature signature = pjp.getSignature();
        //通过签名获取执行操作名称(接口名)
        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("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
    } 
}

六、AOP通知获取数据

1. 返回后通知获取返回值
java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
    private void pt(){}

    @AfterReturning(value = "pt()",returning = "ret")
    public void afterReturning(Object ret) {
        System.out.println("afterReturning advice ..."+ret);
    }
	//其他的略
}

注意:

(1) 参数名的问题
(2) afterReturning方法参数类型的问题

参数类型可以写成String,但是为了能匹配更多的参数类型,建议写成Object类型

(3) afterReturning方法参数的顺序问题
(4) 为什么这里必须显示写上 value = "pt( )"
  • 所有注解的核心属性都会命名为value
  • 如果给注解赋值时,只给 value 属性赋值 ,没有其他属性需要设置,就可以省略value=,直接写值;
  • 如果需要给多个属性赋值(比如同时给valuereturning赋值),就必须显式写出所有属性名,不能省略。

七、百度网盘密码数据兼容处理

1. 开启SpringAOP的注解功能
java 复制代码
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}
2. 编写通知类
java 复制代码
@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
    private void servicePt(){}
    
}
3. 添加环绕通知
java 复制代码
@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
    private void servicePt(){}
    
    @Around("DataAdvice.servicePt()")
    // @Around("servicePt()")这两种写法都对
    public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
        Object ret = pjp.proceed();
        return ret;
    }
    
}
4. 完成核心业务,处理参数中的空格
java 复制代码
@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
    private void servicePt(){}
    
    @Around("DataAdvice.servicePt()")
    // @Around("servicePt()")这两种写法都对
    public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
        //获取原始方法的参数
        Object[] args = pjp.getArgs();
        for (int i = 0; i < args.length; i++) {
            //判断参数是不是字符串
            if(args[i].getClass().equals(String.class)){
                args[i] = args[i].toString().trim();
            }
        }
        //将修改后的参数传入到原始方法的执行中
        Object ret = pjp.proceed(args);
        return ret;
    }
    
}

八、AOP事务管理

1. 需求分析

(1) 需求:实现任意两个账户间转账操作

(2) 需求微缩:A账户减钱,B账户加钱

2. 代码实现
(1) 在需要被事务管理的方法上添加注解
java 复制代码
public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    //配置当前接口方法具有事务
    public void transfer(String out,String in ,Double money) ;
}

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;
	@Transactional
    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
        int i = 1/0;
        accountDao.inMoney(in,money);
    }

}
(2) 在JdbcConfig类中配置事务管理器
java 复制代码
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.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }

    //配置事务管理器,mybatis使用的是jdbc事务
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}

注意: 事务管理器要根据使用技术进行选择,Mybatis框架使用的是JDBC事务,可以直接使用DataSourceTransactionManager

(3) 开启事务注解

在SpringConfig的配置类中开启

java 复制代码
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
3. Spring事务属性
(1) 事务配置

上面这些属性都可以在@Transactional注解的参数上进行设置。

① readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true。

② timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间。

③ rollbackFor:当出现指定异常进行事务回滚

(2) 转账业务追加日志案例

① 需求分析

  • 需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕

  • 需求微缩:A账户减钱,B账户加钱,数据库记录日志

② 代码实现

1). 添加LogDao接口

java 复制代码
public interface LogDao {
    @Insert("insert into tbl_log (info,createDate) values(#{info},now())")
    void log(String info);
}

2). 添加LogService接口与实现类

java 复制代码
public interface LogService {
    void log(String out, String in, Double money);
}
@Service
public class LogServiceImpl implements LogService {

    @Autowired
    private LogDao logDao;
	@Transactional
    public void log(String out,String in,Double money ) {
        logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
    }
}

3). 在转账的业务中添加记录日志

java 复制代码
@Service
public class LogServiceImpl implements LogService {

    @Autowired
    private LogDao logDao;
	//propagation设置事务属性:传播行为设置为当前操作需要新事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(String out,String in,Double money ) {
        logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
    }
}
(3) 关于@Transactional

@Transactional事务管理注解 ,核心作用是控制业务层的数据库操作原子性,因此只应该写在业务层(Service 层),且优先写在「实现类 / 实现类方法」上,绝对不要写在 Controller 层或 DAO 层。

(4) 事务的传播行为

看前两个就行

相关推荐
YangYang9YangYan2 小时前
2026大专大数据与会计专业学习数据分析的价值分析
大数据·学习·数据分析
weixin_448119942 小时前
Datawhale Easy-Vibe 202602 第2次笔记
笔记
2501_901147832 小时前
打家劫舍Ⅱ 延伸学习笔记
笔记·学习
weixin_448119942 小时前
Datawhale 硅基生物进化论 202602第2次笔记
笔记
Peter·Pan爱编程3 小时前
NVIDIA DKMS 驱动构建失败修复笔记
笔记·cuda
Дерек的学习记录10 小时前
C++:入门基础(下)
开发语言·数据结构·c++·学习·算法·visualstudio
半壶清水10 小时前
[软考网规考点笔记]-OSI参考模型与TCP/IP体系结构
网络·笔记·tcp/ip
前路不黑暗@11 小时前
Java项目:Java脚手架项目的公共模块的实现(二)
java·开发语言·spring boot·学习·spring cloud·maven·idea
哎呦 你干嘛~12 小时前
MODBUS_RTU485通讯主站(配置部分)
学习