IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Spring:IOC与AOP -> 正文阅读

[Java知识库]Spring:IOC与AOP

目录

一、Spring简介

1、Spring

2.spring优势

二、IOC

1.概念和原理

?2.自定义IOC容器

1.需求

2.实现

3.Spring相关的API

3.1 图解?编辑

?3.2.BeanFactory 接口

4、Spring相关配置文件和操作

4.1 Bean标签基本配置

4.2 Bean标签配置范围

4.3 Bean生命周期配置

4.4 Bean实例化三种方式

4.5 无参构造方法实例化

4.6 工厂静态方法实例化

4.7 工厂普通方法实例化

4.8 Bean依赖注入概述

4.9 有参构造方法注入(一个是userdao? 另一个时 str?? 配置如下)

4.10 set方法注入

4.11 命名空间注入

4.12 set 普通数据类型的注入

4.13 集合 数组 map properties 的注入

4.14? 配置文件模块化

4.15 知识小结

5 Dbutils整合

5.1 xml的形式

5.2 注解的形式 (无配置文件)

6 Spring整合Junit? 代码 和5 的一样

6.1 为什么用 Spring测试

6.2 依赖

6.3 测试

三、AOP

1 AOP概念

2? AOP底层原理(JDK动态代理和CGLIB动态代理)

核心配置文件??? jdbc.properties 同上

数据库连接工具类 和 事务工具类

事务工具类

?2.1 JDK动态代理

2.2 CGLIB动态代理

测试? 转账中间添加了int i=5/0? 事务捕捉到异常 数据库未发生改变

service实现层代码

dao实现层代码

3.AOP术语

4 编写通知类 配置核心文件(两种方法 一种是直接写poingtcut? 另一种 是先定义 再用pointcut-ref引入 )

5 .AOP 自动代理

6 注解配置AOP详解

6.1 切点表达式

6.2通知类型

6.3 纯注解配置

6.4 AOP优化转账案例

6.5注解配置实现

6.6 注意点



一、Spring简介

1、Spring

Spring是分层的 Java SE/EE应用 full-stack(全栈式) 轻量级开源框架。

提供了表现层 SpringMVC和持久层 Spring JDBC Template以及 业务层 事务管理等众多的企业级应用
技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的Java EE 企业应用开源框
架。
两大核心:以 IOC(Inverse Of Control:控制反转 将创建对象过程交给Spring进行管理)和 AOP(Aspect OrientedProgramming:面向切面编程)为内核。

2.spring优势

1)方便解耦,简化开发
?? ?Spring就是一个容器,可以将所有对象创建和关系维护交给Spring管理
?? ?什么是耦合度?
?? ??? ?对象之间的关系,通常说当一个模块(对象)更改时也需要更改其他模块(对象),这就是耦合,
?? ??? ?耦合度过高会使代码的维护成本增加。要尽量解耦
2)AOP编程的支持
?? ?Spring提供面向切面编程,方便实现程序进行权限拦截,运行监控等功能。
3)声明式事务的支持
?? ?通过配置完成事务的管理,无需手动编程
4)方便测试,降低JavaEE API的使用
?? ?Spring对Junit4支持,可以使用注解测试
5)方便集成各种优秀框架
?? ?不排除各种优秀的开源框架,内部提供了对各种优秀框架的直接支持

二、IOC

1.概念和原理

1、什么是 IOC (Inverse Of Control)
(1)控制反转,把对象创建和对象之间的调用过程,交给 Spring 进行管理(比如对象的创建和销毁)
控制:在java中指的是对象的控制权限(创建、销毁)
反转:指的是对象控制权由原来 由开发者在类中手动控制 反转到 由Spring容器控制
(2)使用 IOC 目的:为了耦合度降低
(3)做入门案例就是 IOC 实现
2、IOC 底层原理
(1)xml 解析、工厂模式、反射
3、画图讲解 IOC 底层原理

?2.自定义IOC容器

1.需求

1. 创建java项目,导入自定义IOC相关坐标
2. 编写Dao接口和实现类
3. 编写Service接口和实现类
4. 编写测试代码

2.实现

依赖

  <dependencies>
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>

编写Dao接口和实现类

public interface UserDao {
    public void save();
}
public class UserDaoImpl implements UserDao {
    @Override
    public void save() {
        System.out.println("保存成功");
    }
}

Service接口和实现类

public interface UserService {
    public  void save();
}
public class UserServiceImpl implements UserService {
    private UserDao userDao;
    public void save()  {
        userDao = (UserDao) BeanFactory.getBean("userDao");
        userDao.save();
    }

}

beans.xml 文件

<beans>
<!--    bean 表示一个需要创建的对象
        id: 表示该对象的名字
        class:表示该对象的类型
-->


    <bean id="userDao" class="com.qiku.dao.UserDaoImpl"></bean>

</beans>
BeanFactory 工厂类
package com.qiku.utils;

/**
 * 2022/6/23 15:05
 *
 * @author yrc
 * @version MySpring
 */


import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 根据配置文件中的信息 创建对象
 * 并将所有的对象存入一个容器之中
 */
public class BeanFactory {
//声明一个 map 充当 容器作用 ,用于包存所有的对象
public  static Map<String ,Object> ioc=new HashMap<>();

//程序启动时 自动初始化对象
    static {

    try {
        //1 读取配置文件
        InputStream resource =
                BeanFactory.class.getClassLoader().getResourceAsStream("beans.xml");
        //2 解析xml
        SAXReader saxReader = new SAXReader();
        //2.1 通过saxReader 将resource装换为document
        Document document = saxReader.read(resource);
        //3 编写xPath表达式
        String xPath="//bean";
        //4 获取所有的bean标签
        List<Element> nodes = document.selectNodes(xPath);
        System.out.println(nodes);
        //5 遍历 并创建对象 设置到map中
        for (Element e:nodes){
            String id = e.attributeValue("id");
            String className = e.attributeValue("class");
            System.out.println("id= "+id);
            System.out.println("id= "+className);
            Object obj = Class.forName(className).newInstance();
            //将 id 和obj存入ioc容器
            ioc.put(id,obj);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

//提供获取对象的方法
   public static Object getBean(String beanId){
        return ioc.get(beanId);
   }




}

测试类 以及结果

?总结:

* 其实升级后的BeanFactory就是一个简单的Spring的IOC容器所具备的功能。

* 之前我们需要一个userDao实例,需要开发者自己手动创建 new UserDao();

* 现在我们需要一个userdao实例,直接从spring的IOC容器获得,对象的创建权交给了spring控制

* 最终目标:代码解耦合 解耦合并不是没有关系了 只是耦合度降低了

3.Spring相关的API

3.1 图解

?3.2.BeanFactory 接口

1、IOC 思想基于 IOC 容器完成,IOC 容器底层就是对象工厂
2、Spring 提供 IOC 容器实现两种方式:(两个接口)
(1)BeanFactory:IOC 容器基本实现,是 Spring 内部的使用接口,不提供开发人员进行使用
特点:在第一次调用getBean()方法时,创建指定对象的实例(用的时候去创建

BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));

加载配置文件时候不会创建对象,在获取对象(使用)才去创建对象
(2)ApplicationContext:BeanFactory 接口的子接口,提供更多更强大的功能一 般由开发人
员进行使用 ( 类加载的时候去创建
特点:在spring容器启动时,加载并创建所有对象的实例
加载配置文件时候就会把在配置文件对象进行创建
3、ApplicationContext 接口有实现类

1. ClassPathXmlApplicationContext
?? ?它是从类的根路径下加载配置文件 推荐使用这种。
2. FileSystemXmlApplicationContext
?? ?它是从磁盘路径上加载配置文件,配置文件可以在磁盘的任意位置。
3. AnnotationConfigApplicationContext
?? ?当使用注解配置容器对象时,需要使用此类来创建 spring 容器。它用来读取注解。
?? ?ApplicationContext app =new ClassPathXmlApplicationContext("applicationContext.xml");

4、Spring相关配置文件和操作

4.1 Bean标签基本配置

<bean id="" class=""></bean>

* 用于配置对象交由Spring来创建。


* 基本属性:
?? ?id:Bean实例在Spring容器中的唯一标识
?? ?class:Bean的全限定名(全路径)

name: 不常用
* 默认情况下它调用的是类中的 无参构造函数,如果没有无参构造函数则不能创建成功。

4.2 Bean标签配置范围

<bean id="" class="" scope=""></bean>

4.3 Bean生命周期配置

<bean id="" class="" scope="" init-method="" destroy-method=""></bean>

* init-method:指定类中的初始化方法名称
* destroy-method:指定类中销毁方法名称

4.4 Bean实例化三种方式

??? 无参构造方法实例化
??? 工厂静态方法实例化
??? 工厂普通方法实例化

4.5 无参构造方法实例化

它会根据默认无参构造方法来创建类对象,如果bean中没有默认无参构造函数,将会创建失败

<bean id="userDao" class="com.qiku.dao.impl.UserDaoImpl"/>

4.6 工厂静态方法实例化

应用场景
依赖的jar包中有个A类,A类中有个静态方法m1,m1方法的返回值是一个B对象。如果我们频繁使用B对象,此时我们可以将B对象的创建权交给spring的IOC容器,以后我们在使用B对象时,无需调用A类中的m1方法,直接从IOC容器获得。

public class StaticFactoryBean {
?? ?public static UserDao createUserDao(){
?? ??? ?return new UserDaoImpl();
?? ?}
}

<bean id="userDao" class="com.qiku.factory.StaticFactoryBean" factory-method="createUserDao" />

4.7 工厂普通方法实例化

应用场景
依赖的jar包中有个A类,A类中有个普通方法m1,m1方法的返回值是一个B对象。如果我们频繁使用B对象,此时我们可以将B对象的创建权交给spring的IOC容器,以后我们在使用B对象时,无需调用A类中的m1方法,直接从IOC容器获得。

public class DynamicFactoryBean {
?? ?public UserDao createUserDao(){
?? ??? ?return new UserDaoImpl();
?? ?}
}

<bean id="dynamicFactoryBean" class="com.qiku.factory.DynamicFactoryBean"/>
<bean id="userDao" factory-bean="dynamicFactoryBean" factory-method="createUserDao"/>

4.8 Bean依赖注入概述

依赖注入 DI(Dependency Injection):它是 Spring 框架核心 IOC 的具体实现。

在编写程序时,通过控制反转,把对象的创建交给了 Spring,但是代码中不可能出现没有依赖的情况。IOC 解耦只是降低他们的依赖关系,但不会消除例如:业务层仍会调用持久层的方法。

那这种业务层和持久层的依赖关系,在使用 Spring 之后,就让 Spring 来维护了。简单的说,就是通过框架把持久层对象传入业务层,而不用我们自己去获取。

4.9 有参构造方法注入(一个是userdao? 另一个时 str?? 配置如下)

    public UserServiceImpl(UserDao userDao,String str) {
        System.out.println("有参构造器执行了 ");
        this.userDao = userDao;

    }
<!--  配置userDao 将其交给Spring 的容器管理&ndash;&gt;-->
    <bean id="userDao" class="com.qiku.dao.impl.UserDaoImpl"></bean>


<!--    配置UserService-->
    <bean id="userService" class="com.qiku.service.impl.UserServiceImpl">
        <!-- (1)   使用有参构造器的 方式注入 index下标0 代表第一个参数
              1 则代表 第二个参数 type 为参数类型 ref 为引用数据类型 指的是上面 的 id userDao-->
<!--        <constructor-arg index="0" type="com.qiku.dao.UserDao" ref="userDao"/>-->
<!--        第(2)种方法 name表示参数中的名字  代表之前的 index=0-->
        <constructor-arg name="userDao" type="com.qiku.dao.UserDao" ref="userDao"></constructor-arg>
        <constructor-arg name="str" type="java.lang.String" value="123"></constructor-arg>
   </bean>

4.10 set方法注入

   public void setUserDao(UserDao userDao) {
        System.out.println("set方法执行了");
        this.userDao = userDao;
    }

配置spring容器利用set方法注入

<bean id="userDao" class="com.qiku.dao.impl.UserDaoImpl"></bean>
<bean id="userService2" class="com.qiku.service.impl.UserServiceImpl">
 <property name="userDao" ref="userDao"/>
</bean>

4.11 命名空间注入

xmlns:p="http://www.springframework.org/schema/p"
  <!--        p命名空间 注入-->
<bean id="userDao" class="com.qiku.dao.impl.UserDaoImpl"></bean>
    <bean id="userService3" class="com.qiku.service.impl.UserServiceImpl"
          p:userDao-ref="userDao">
    </bean>

4.12 set 普通数据类型的注入

public class User { 
	private String username; 
	private String age; 
	public void setUsername(String username) { 
		this.username = username; 
	}
	public void setAge(String age) { 
		this.age = age; 
	} 
}
    <bean id="user4" class="com.qiku.pojo.User">

        <constructor-arg name="name" value="马孟德"></constructor-arg>
        <constructor-arg name="age"  value="35"></constructor-arg>

<!--        <property name="name" value="马孟德"></property>-->
<!--        <property name="age" value="35"></property>-->
    </bean>

4.13 集合 数组 map properties 的注入

User类? 还有set 方法

    private List<Object> list;
    private Object[] arrays;
    private Map<String,Integer> map;
   private Properties properties;

配置文件

<!--    注入集合类型-->
    <bean id="user5" class="com.qiku.pojo.User">
        <property name="list" >
            <list>
                <value>chaolihai</value>
                <value>bucuoou</value>
                <value>zhadsafds</value>
                <ref bean="userDao"></ref>
            </list>
        </property>
     </bean>
<!--    注入数组类型-->
    <bean id="user6" class="com.qiku.pojo.User">
        <property name="arrays" >
            <array>
                <value>chaolihai</value>
                <value>bucuoou</value>
                <value>zhadsafds</value>
                <ref bean="userDao"></ref>
            </array>
        </property>
    </bean>
<!--    注入map类型-->
    <bean id="user7" class="com.qiku.pojo.User">
        <property name="map" >
            <map>
               <entry key="嗨嗨嗨" value="8"></entry>
                <entry key="clearlove" value="7"></entry>
                <entry key="老阴逼" value="6"></entry>
            </map>
        </property>
    </bean>

<!--    注入Properties类型数据-->
    <bean id="user8" class="com.qiku.pojo.User" scope="prototype">
        <property name="properties">
         <props>
             <prop key="driver">com.mysql.jdbc.Driver</prop>
             <prop key="url">jdbc:mysql://localhost:3306/mydb</prop>
             <prop key="username">root</prop>
             <prop key="password">root</prop>
         </props>
        </property>
    </bean>

测试

  //集合注入
    @Test
    public void test10() {

        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = context.getBean("user5", User.class);
        System.out.println(user.getList());
        context.close();
    }
   
    @Test
    public void test11() {

        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = context.getBean("user6", User.class);
        System.out.println(user.getArrays());
        context.close();
    }
    //集合注入
    @Test
    public void test12() {

        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = context.getBean("user7", User.class);
        System.out.println(user.getMap());
        context.close();
    }
    @Test
    public void test13() {

        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = context.getBean("user8", User.class);
        System.out.println(user.getProperties());
        context.close();
    }

4.14? 配置文件模块化

实际开发中,Spring的配置内容非常多,这就导致Spring配置很繁杂且体积很大,所以,可以将部分
配置拆解到其他配置文件中,也就是所谓的配置文件模块化。

1)并列的多个配置文件

ApplicationContext act = new ClassPathXmlApplicationContext("beans1.xml","beans2.xml","...");

2)主从配置文件

<import resource="applicationContext-xxx.xml"/>

4.15 知识小结

<bean>标签:创建对象并放到spring的IOC容器
?? ?id属性:在容器中Bean实例的唯一标识,不允许重复
?? ?class属性:要实例化的Bean的全限定名
?? ?scope属性:Bean的作用范围,常用是Singleton(默认)和prototype

<constructor-arg>标签:属性注入
?? ?name属性:属性名称
?? ?value属性:注入的普通属性值
?? ?ref属性:注入的对象引用值

<property>标签:属性注入
?? ?name属性:属性名称
?? ?value属性:注入的普通属性值
?? ?ref属性:注入的对象引用值
?? ?<list>
?? ?<set>
?? ?<array>
?? ?<map>
?? ?<props>
?? ?
<import>标签:导入其他的Spring的分文件

属性中包含尖括号

属性设置空值

5 Dbutils整合

5.1 xml的形式

5.1.1 核心配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
	   http://www.springframework.org/schema/beans/spring-beans.xsd

       http://www.springframework.org/schema/context
	   http://www.springframework.org/schema/context/spring-context.xsd"
       >

<!--   spring 加载外部文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
<!--    将数据库连接池交给IOC容器-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>


    <!--    将QueryRunner 交给IOC 容器-->
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
    <!--利用有参构造器的方式将 datasource注入 -->
    <constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>

<!--    将AccountDao 交给IOC-->
<bean id="accountDao" class="com.qiku.dao.impl.AccountDaoImpl">
    <property name="qr" ref="queryRunner"></property>
</bean>

    <!--把AccountService交给IOC容器-->
    <bean id="accountService" class="com.qiku.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"></property>
    </bean>



</beans>

5.1.2 jdbc配置文件

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false
jdbc.username=root
jdbc.password=root

5.1.3 set方法

queryrunner在Dao层 set注入spring容器

5.2 注解的形式 (无配置文件)

jdbc 配置文件同5.1.2

5.2.1 数据库配置类

package com.qiku.config;
/* 数据源配置

* */

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;

import javax.sql.DataSource;


@PropertySource("classpath:jdbc.properties")
public class DataSourceConfig {
    //使用EL表达式 读取配置文件中的值 给属性赋值
    @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 getDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return  dataSource;
    }


}

5.2.2 Spring 核心配置类

package com.qiku.config;

import org.apache.commons.dbutils.QueryRunner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import javax.sql.DataSource;

/**@Configuration 写在类上面
 * 表示该类是一个配置类
 * @ComponentScan 用于开启注解扫描
 *
 * @Import(DataSourceConfig.class)
 * 引入其他配置类
 */
@Configuration
@ComponentScan("com.qiku")
@Import(DataSourceConfig.class)
public class SpringConfig {
//   @Bean 写在方法上 用于将第三方的类 交给IOC容器
    //默认的beanId同样是雷鸣的首字母小写 体现为方法的返回值类型
    @Bean("queryRunner")
    public QueryRunner getQueryRunner(
            @Autowired DataSource dataSource) {
        return new QueryRunner(dataSource);
    }
    

}

5.2.3 DAO层实现类

package com.qiku.dao.impl;

import com.qiku.dao.AccountDao;
import com.qiku.pojo.Account;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.sql.SQLException;
import java.util.List;

/**
 * 2022/6/24 15:22
 *
 * @author yrc
 * @version spring02_quickStart
 */
@Repository
public class AccountDaoImpl implements AccountDao {

    @Autowired
    private QueryRunner qr;

    //set 方法  spring注入
    public void setQr(QueryRunner qr) {
        this.qr = qr;
    }

    @Override
    public void save(Account account) throws SQLException {
        String sql = "insert into account(name,money) " +
                "values(?,?)";
        int update = qr.update(sql, account.getName(), account.getMoney());
    }

    @Override
    public void update(Account account) throws SQLException {
        String sql="update account set name=? ,money=? where id=?";
        int update = qr.update(sql, account.getName(), account.getMoney(), account.getId());

    }

    @Override
    public void delete(Integer id) throws SQLException {
        String sql="delete from account where id=?";
        qr.update(sql,id);

    }

    @Override
    public List<Account> findAll() throws SQLException {
        String sql="select * from account";
        List<Account> query = qr.query(sql, new BeanListHandler<Account>(Account.class));
        return  query;
    }

    @Override
    public Account findById(Integer id) throws SQLException {
        String sql="select * from account where id =?";
        Account query = qr.query(sql, new BeanHandler<Account>(Account.class),id);
        return  query;

    }
}

5.2.4 Service层实现类

package com.qiku.service.impl;

import com.qiku.dao.AccountDao;
import com.qiku.pojo.Account;
import com.qiku.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.sql.SQLException;
import java.util.List;

/**
 * 2022/6/24 15:49
 *
 * @author yrc
 * @version spring02_quickStart
 */
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
    @Override
    public void save(Account account) throws SQLException {
      accountDao.save(account);
    }

    @Override
    public void update(Account account) throws SQLException {
  accountDao.update(account);
    }

    @Override
    public void delete(Integer id) throws SQLException {
  accountDao.delete(id);
    }

    @Override
    public List<Account> findAll() throws SQLException {
        List<Account> all = accountDao.findAll();
        return all;
    }

    @Override
    public Account findById(Integer id) throws SQLException {
        Account account = accountDao.findById(id);

        return  account;
    }

}

5.2.5 测试类

package com.qiku.test;

import com.qiku.config.SpringConfig;
import com.qiku.pojo.Account;
import com.qiku.service.AccountService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.sql.SQLException;
import java.util.List;

/**
 * 2022/6/24 16:37
 *
 * @author yrc
 * @version spring02_quickStart
 */
public class AccountServiceTest {
    //加载配置类

    ApplicationContext context =
            new AnnotationConfigApplicationContext(SpringConfig.class);

// 没有配置文件了
//    private ClassPathXmlApplicationContext context
//            =new ClassPathXmlApplicationContext("applicationContext.xml");
    //从IOC中获取accountService对象
    private AccountService accountService= context.getBean("accountServiceImpl",AccountService.class);
//    测试报保存信息
    @Test
    public void testSave() throws SQLException {
        Account account=new Account();
        account.setName("lucy");
        account.setMoney(9999.99);
        accountService.save(account);
    }
    @Test
    public void testUpdate() throws SQLException {
        Account account=new Account();
        account.setId(3);
        account.setName("机甲战士");
        account.setMoney(9999.99);
        accountService.update(account);
    }
    @Test
    public void testFindAll() throws SQLException {
        Account account=new Account();

        List<Account> all = accountService.findAll();
        System.out.println(all);
    }
    @Test
    public void testFindById() throws SQLException {
        Account account=new Account();
account.setId(1);
        Account byId = accountService.findById(account.getId());
        System.out.println(byId);
    }
    @Test
    public void testDelete() throws SQLException {
        Account account=new Account();

       accountService.delete(4);

    }
}

5.2.6 数据库

6 Spring整合Junit? 代码 和5 的一样

6.1 为什么用 Spring测试

6.2 依赖

<!--此处需要注意的是,spring5 及以上版本要求 junit 的版本必须是 4.12 及以上--> 
<dependency> 
	<groupId>org.springframework</groupId> 
	<artifactId>spring-test</artifactId> 
	<version>5.1.5.RELEASE</version> 
</dependency> 
<dependency> 
	<groupId>junit</groupId> 
	<artifactId>junit</artifactId> 
	<version>4.12</version> 
	<scope>test</scope> 
</dependency>

6.3 测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringConfig.class})
public class SpringAndJunitTest {
    @Autowired
    private AccountService accountService;

    @Test
    public void testFindALL() throws SQLException {
        List<Account> all = accountService.findAll();
        System.out.println(all);

    }

    @Autowired
    private DataSource dataSource;
    @Test
    public void test02(){
        System.out.println(dataSource);
    }
}

三、AOP

1 AOP概念

(1)面向切面编程(方面),利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得
业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
(2)通俗描述:不通过修改源代码方式,在主干功能里面添加新功能

2? AOP底层原理(JDK动态代理和CGLIB动态代理)

我们可以将业务代码和事务代码进行拆分,通过动态代理的方式,对业务方法进行事务的增强。这样就不会对业务层产生影响,解决了耦合性的问题啦!

常用的动态代理技术
JDK 代理 : 基于接口的动态代理技术·:利用拦截器(必须实现invocationHandler)加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理,从而实现方法增强

CGLIB代理:基于父类的动态代理技术:动态生成一个要代理的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截技术拦截所有的父类方法的调用,顺势织入横切逻辑,对方法进行增强

核心配置文件??? jdbc.properties 同上

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
	   http://www.springframework.org/schema/beans/spring-beans.xsd

       http://www.springframework.org/schema/context
	   http://www.springframework.org/schema/context/spring-context.xsd"
       >
<context:component-scan base-package="com.qiku"></context:component-scan>
<!--   spring 加载外部文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
<!--&lt;!&ndash;    将数据库连接池交给IOC容器&ndash;&gt;-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>



    <!--    将QueryRunner 交给IOC 容器-->
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
    <!--利用有参构造器的方式将 datasource注入 -->
    <constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>

</beans>

数据库连接工具类 和 事务工具类

package com.qiku.utils;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * 由于之前的 in out 操作都是独立的食物 并且独立的连接
 * 解决方法:
 * 将获取连接的方式统一管理  以及同一个事务中尽心操作
 *
 * 链接工具类: 从数据源中获取一个链接 并实现线程的绑定
 *
 *
 */
@Component
public class ConnectionUtils {
//    创建本地线程 绑定的是数据库连接
    private ThreadLocal<Connection> threadLocal=new ThreadLocal<>();

//    从IOC容器中 获取数据源对象
    @Autowired
    private DataSource dataSource;

    //获取当前线程上的连接
    public Connection getThreadConnection() {
        //1 先从threadlocal里面获取 当前连接
        Connection connection = threadLocal.get();
        //判断connection 是否为null
        if (connection == null) {

            try { //则说明是第一次获取  则从数据源中 获取连接 并与线程绑定
                connection = dataSource.getConnection();
                threadLocal.set(connection);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return  connection;
    }

    //解除绑定
    public  void removeThreadConnection(){

        threadLocal.remove();
    }
}



事务工具类

package com.qiku.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.sql.SQLException;

/*
* 事务管理器 :开启事务  提交事务  回滚事务  释放资源
* */
@Component
public class TransactionManager {

    @Autowired
    private ConnectionUtils connectionUtils;

    //开启事务
    public void beginTransaction(){
        try {
            //开启事务 并关闭自动提交
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    public void commit(){
        try {
            //提交事务
            connectionUtils.getThreadConnection().commit();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    public void rollback(){
        try {
            //事务回滚
            connectionUtils.getThreadConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    public void release(){
        try {
            //改回自动提交
            connectionUtils.getThreadConnection().setAutoCommit(true);
            //归还连接池
            connectionUtils.getThreadConnection().close();
            //解除与线程的绑定
            connectionUtils.removeThreadConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

?2.1 JDK动态代理

2.1.1 工厂动态代理类

package com.qiku.proxy;

import com.qiku.service.AccountService;
import com.qiku.utils.TransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

@Component
public class JdkProxyFactory {
    /*
    * 动态代理类*/
    //要代理的目标对象
    @Autowired
    private AccountService accountService;
    //事务管理器
    @Autowired
    private TransactionManager transactionManager;

    //创建AccountService 的代理对象
    public AccountService getAccountServiceProxy(){
        AccountService accountServiceProxy =null;
        accountServiceProxy = (AccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
//                                       代理对象    拦截到的的方法transfer()  拦截到的方法的参数
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object result=null;
                        try{
//                            开启事务
                            transactionManager.beginTransaction();
                            //执行业务处理
                            result= method.invoke(accountService,args);
                            //提交事务
                            transactionManager.commit();
                        }catch (Exception e){
                            e.printStackTrace();
                            transactionManager.rollback(); //回滚事务
                        }finally {
                            transactionManager.release();//释放资源
                        }
                        return result;
                    }
                }
        );
        return accountServiceProxy;
    }

}

2.2 CGLIB动态代理

2.2.1 工厂动态代理类
package com.qiku.proxy;

import com.qiku.service.AccountService;
import com.qiku.utils.TransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.stereotype.Component;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

@Component
public class CGLIBProxyFactory {

    @Autowired
    private AccountService accountService;
    @Autowired
    private TransactionManager transactionManager;

    public AccountService getAccountServiceProxy(){
        AccountService accountServiceProxy =null;
        /**
         * 参数一 :目标对象的字节码文件
         * 参数二 : 动作类 实现增强功能
        *
        * */
        accountServiceProxy= (AccountService) Enhancer.create(AccountService.class,
                new MethodInterceptor() {
                    @Override
                    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        Object result=null;

                        try {
                            transactionManager.beginTransaction();
                            result=method.invoke(accountService,objects);
                            transactionManager.commit();
                        } catch (Exception e) {
                            e.printStackTrace();
                            transactionManager.rollback();
                        } finally {
                            transactionManager.release();
                        }
                        return result;
                    }
                });
        return  accountServiceProxy;

    }
}

测试? 转账中间添加了int i=5/0? 事务捕捉到异常 数据库未发生改变

service实现层代码

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;

    @Override
    public void transfer(String outName, String innerName, Double money) {
//支出
            accountDao.out(outName,money);
            int i=5/0;
//        收入
            accountDao.in(innerName,money);


    }
}

dao实现层代码

@Repository //注册到IOC容器
public class AccountDaoImpl implements AccountDao {
    //dao层保证同一个链接
    @Autowired
    private QueryRunner queryRunner;
    @Autowired
    private ConnectionUtils connectionUtils;

    @Override
    public void out(String outerName, double money) {

        try {   String sql ="UPDATE account SET money=money-? WHERE name=?";
            queryRunner.update(connectionUtils.getThreadConnection(),sql,money,outerName);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void in(String innerName, double money) {

        try {   String sql ="UPDATE account SET money=money+? WHERE name=?";
            queryRunner.update(connectionUtils.getThreadConnection(),sql,money,innerName);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

}

3.AOP术语

3.1 Spring 的 AOP 实现底层就是对上面的动态代理的代码进行了封装,封装后我们只需要对需要关注的部分进行代码编写,并通过配置的方式完成指定目标的方法增强。
在正式讲解 AOP 的操作之前,我们必须理解 AOP 的相关术语,常用的术语如下:

* Target(目标对象):代理的目标对象
* Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类
* Joinpoint(连接点):所谓连接点是指那些可以被拦截到的点,也就是可以被增强的方法。在spring中,这些点指的是方法,因为 spring只支持方法类型的连接点
* Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。真正增强的方法

(1)切入点表达式作用:知道对哪个类里面的哪个方法进行增强
(2)语法结构: execution([权限修饰符] [返回类型] [类全路径] [方法名称]([参数列表]) )?
举例 1:对 com.qiku.dao.BookDao 类里面的 add 进行增强
execution(*com.qiku.dao.BookDao.add(.. ))
举例 2:对 com.qiku.dao.BookDao 类里面的所有的方法进行增强
execution( *com.qiku.dao.BookDao.* ( .. ))
举例 3:对 com.qiku.dao 包里面所有类,类

* Advice(通知/增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知 分类:前置通知、后置通知、异常通知、最终通知、环绕通知

当前四个通知组合在一起时,执行顺序如下:
@Before -> @After -> @AfterReturning(如果有异常:@AfterThrowing)


* Aspect(切面):是切入点和通知(引介)的结合
* Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织 入,而AspectJ采用编译期织入和类装载期织入

4 编写通知类 配置核心文件(两种方法 一种是直接写poingtcut? 另一种 是先定义 再用pointcut-ref引入 )

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

  http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

<!--    将目标对象 交给 IOC容器-->
    <bean id="accountService" class="com.qiku.service.impl.AccountServiceImpl"></bean>
    <bean id="userService" class="com.qiku.service.impl.UserServiceImpl"></bean>

<!--    将通知类 交给 IOC容器-->
    <bean id="myAdvice" class="com.qiku.advic.MyAdvice"></bean>

<!--    配置植入关系  以及切面    方法一-->
    <aop:config>
<!--        引入通知类 即切面 myAdvice-->
        <aop:aspect ref="myAdvice">
<!--            配置目标类的transfer  方法执行是 使用前置 通知-->
<!--            pointcut="execution(* com.qiku.service.impl.AccountServiceImpl.*(..))"/>-->
            <aop:before method="before"

                     pointcut="execution(* com.qiku.service..*.*(..))"/>
        </aop:aspect>
    </aop:config>
<!--    方法二-->
    <aop:config>
<!--        * com.qiku.service..*.*(..)   表示  service下的所有类的所有类型的方法-->
        <aop:pointcut id="myPointcut" expression="execution(* com.qiku.service..*.*(..))"/>
        <aop:aspect ref="myAdvice">

<!--            前置通知-->
            <aop:before method="before" pointcut-ref="myPointcut"/>

<!--            后置通知-->
            <aop:after-returning method="afgterReturning" pointcut-ref="myPointcut"/>

<!--            异常通知  发生异常  后置通知不执行-->
            <aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut"/>
            <!--            最终通知  无论是否发生异常 都会执行-->
            <aop:after method="after" pointcut-ref="myPointcut"/>
            <!--            环绕通知 -->
            <aop:around method="around" pointcut-ref="myPointcut"/>
        </aop:aspect>
    </aop:config>

</beans>

5 .AOP 自动代理

<!--aop的自动代理--> 
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

6 注解配置AOP详解

6.1 切点表达式

切点表达式的抽取

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.qiku..*.*(..))")
    public void myPoint(){}

    @Before("MyAdvice.myPoint()")
    public void before() {
        System.out.println("前置通知...");
    }
}

???

6.2通知类型

通知的配置语法:@通知注解(“切点表达式")
名称?? ?标签?? ?说明
前置通知?? ?@Before?? ?用于配置前置通知。指定增强的方法在切入点方法之前执行
后置通知?? ?@AfterReturning?? ?用于配置后置通知。指定增强的方法在切入点方法之后执行
异常通知?? ?@AfterThrowing?? ?用于配置异常通知。指定增强的方法出现异常后执行
最终通知?? ?@After?? ?用于配置最终通知。无论切入点方法执行时是否有异常,都会执行
环绕通知?? ?@Around?? ?用于配置环绕通知。开发者可以手动控制增强代码在什么时候执行

注意:

当前四个通知组合在一起时,执行顺序如下:
@Before -> @After -> @AfterReturning(如果有异常:@AfterThrowing)

6.3 纯注解配置

@Configuration
@ComponentScan("com.qiku")  //容器里面注解扫描
@EnableAspectJAutoProxy //替代 <aop:aspectj-autoproxy />
public class SpringConfig {
 
}

6 .4 AOP优化转账案例

依然使用前面的转账案例,将两个代理工厂对象直接删除!改为spring的aop思想来实现
xml配置实现

1)配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    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/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--开启组件扫描-->
    <context:component-scan base-package="com.qiku"/>
    <!--加载jdbc配置文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--把数据库连接池交给IOC容器-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>
    <!--把QueryRunner交给IOC容器-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>
    <!--AOP配置-->
    <aop:config>
        <!--切点表达式-->
        <aop:pointcut id="myPointcut" expression="execution(* com.qiku.service..*.*(..))"/>
        <!-- 切面配置 -->
        <aop:aspect ref="transactionManager">
            <aop:before method="beginTransaction" pointcut-ref="myPointcut"/>
            <aop:after-returning method="commit" pointcut-ref="myPointcut"/>
            <aop:after-throwing method="rollback" pointcut-ref="myPointcut"/>
            <aop:after method="release" pointcut-ref="myPointcut"/>
        </aop:aspect>
    </aop:config>
</beans>

2)事务管理器(通知)

// 事务管理器工具类,包括:开启事务、提交事务、回滚事务、释放资源
@Component
public class TransactionManager {
    @Autowired
    ConnectionUtils connectionUtils;
    public void begin(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void commit(){
        try {
            connectionUtils.getThreadConnection().commit();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    
    public void rollback(){
        try {
            connectionUtils.getThreadConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void release(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(true);
            connectionUtils.getThreadConnection().close();
            connectionUtils.removeThreadConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

6.5注解配置实现

1)配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    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/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--开启组件扫描-->
    <context:component-scan base-package="com.qiku"/>
    <!--开启AOP注解支持-->
    <aop:aspectj-autoproxy/>
    <!--加载jdbc配置文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--把数据库连接池交给IOC容器-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <!--把QueryRunner交给IOC容器-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>
</beans>

??

2)事务管理器(通知)
?

@Component
@Aspect
public class TransactionManager {
    @Autowired
    ConnectionUtils connectionUtils;

    @Around("execution(* com.hyq.serivce..*.*(..))")
    public Object around(ProceedingJoinPoint pjp) {
        Object object = null;
        try {
            // 开启事务
            connectionUtils.getThreadConnection().setAutoCommit(false);
            // 业务逻辑
            pjp.proceed();
            // 提交事务
            connectionUtils.getThreadConnection().commit();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            // 回滚事务
            try {
                connectionUtils.getThreadConnection().rollback();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        } finally {
            try {
                connectionUtils.getThreadConnection().setAutoCommit(true);
                connectionUtils.getThreadConnection().close();
                connectionUtils.removeThreadConnection();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return object;
    }
}

6.6 注意点

1 在增强类上面添加注解 @Order(数字类型值),数字类型值越小优先级越高
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-07-04 22:41:24  更:2022-07-04 22:44:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 15:49:22-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码