文章仅供学习或复习Spring知识点为主;
资料参考于B站UP主狂神说Spring5课程;
链接: 狂神说Spring5最新完整教程IDEA版通俗易懂.
一、Spring
1.1、简介
-
Spring理念:使现有技术更实用,整合了现有的框架技术。
-
Spring官网:http://spring.io
-
Spring 是一款轻量级的控制反转和面向切面编程的框架。
-
开源、轻量级、非侵入式(API不会在业务上出现,而业务可从Spring框架中移植到其他框架而且与环境无关);
-
Spring作用是降低代码耦合度(代码之间的依赖关系);
-
降低耦合度的方式分为两类:控制反转IOC、面向切面AOP;【重点】
-
IOC:不用自己创建对象,而是交给Spring容器管理;
容器在对象初始化时主动将依赖传递给对象;
AOP:面向切面编程、是一种思想,主要将业务与系统服务(日志,事务等)进行分离开发,我们只需关注业务,而AOP会将系统服务以切面的方式动态的织入到业务中。
容器:管理对象的生命周期、依赖关系、通过配置文件,定义对象以及设置依赖关系。
-
提供日志、安全、事务等,复用性强;
-
2002年,由Rod Jahnson 推出的Spring框架雏形interface21框架。
-
2004年,Spring框架以interface21框架为基础,经过重设、发布了Spring1.0版本。
1.2、组成
Spring是一个分层架构、由七个模块组成、模块构建在核心容器之上,核心容器定义了创建,配置和管理bean的方式。
Spring模块组件
模块功能:
- Core 核心容器: 提供了基本功能、主要组件是BeanFactory(工厂模式的实现)使用了控制反转模式将程序的配置和依赖性规范与实际的应用程序代码分开;
- Context 上下文: 上下文配置文件、向Spring提供了上下文信息、其上下文包括业务服务,比如 JNDI、EJB、电子邮件、国际化、校验和调度等;
- Spring AOP : 通过配置管理特性,AOP模块将面向切面的编程功能,集成到Spring中,所以Spring会管理任何支持AOP的对象;AOP模块基于Spring程序中的对象提供了事务管理服务。使用AOP不用依赖EJB组件、就可以将声明性事务管理集成到应用中。
- Spring DAO : JDBC DAO 抽象层提供了异常层次结构、用该结构管理异常处理和不同数据库供应商抛出的错误信息。简化了错误处理并降低了编写异常代码量。
- Spring ORM : Spring有若干个ORM框架,提供了ORM对象关系工具,包括 JDO、Hibernate iBatis SQL Map、都要遵从Spring的事务和DAO异常层次结构。
- Spring Web 模块: Web上下文模块建立在应用程序上下文模块之上、为基于Web的应用程序提供上下文。所以、Spring支持与Jakarta Struts 的集成。Web模块简化了处理请求和将请求参数绑定到域对象的工作。
- Spring MVC 框架: 是一个全功能的构建Web应用的MVC实现。通过策略接口、MVC框架变成为高度可配置的,MVC容纳了视图技术、包括 JSP、Velocity、Tiles、iText、POI。
Spring目录和Jar包说明:
目录 | 说明 |
---|---|
docs | 文档 |
libs | jar包 |
schema | 约束 |
license.txt | 许可 |
notice.txt | 注意 |
readme.txt | 说明 |
spring-包名-版本号.RELEASE.jar 字节码jar包
spring-包名-版本号.RELEASE-javadoc.jar 文档
spring-包名-版本号.RELEASE-sources.jar 源码
比如:
spring-aop-4.2.1.RELEASE.jar 字节码jar包
Spring核心jar包
spring-beans Beans
spring-context context上下文信息
spring-core spring核心
expression spEL
扩展
Spring Boot 和 Spring Cloud 的关系
- Spring Boot 是Spring的一套快速配置脚手架、可基于Spring boot快速开发单微服务;
- Spring Cloud基于Spring Boot实现;
- Spring Boot 专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架;
二、IOC
2.1、概念
-
控制反转(IOC、Inversion of Control) 是一种设计思想。
-
控制反转:转移对象控制权,反转到外部容器;
-
实现控制反转的两种方式:
-
依赖查找(DL,Dependency Lookup): 提供回调接口和上下文环境,我们只需提供查找方式(JNDI系统查找);
-
依赖注入(DI,Dependency Injection): 程序运行过程中需要调用对象时,无序创建被调用的对象,而是依赖容器创建并传给程序。
-
IOC是Spring的核心,通过配置XML或注解方式,实现IOC。
-
Spring容器在初始化时先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时在从IOC容器中取出需要的对象;
在没有IOC的程序中,使用面向对象编程完全依赖硬编码的方式将程序耦合在一起,而使用IOC则大大降低了耦合性;
Spring中实现控制反转的是IOC容器,其实现方法是依赖注入(DI,Dependency Injection)。
2.2、程序耦合
- 使用 类名 对象名 = new 类名(); 的方式编写代码、如果需求要求改变
- 如果需求量非常多的话,这种方式每次都要改动代码,耦合度太高;
如何降低高耦合?
-
在需要的地方,不去实现,提供一个set,比如:
// 在业务实现类中添加set方法 public class UserServiceImpl implements UserService { private UserDao userDao; // 利用set实现 public void setUserDao(UserDao userDao) { this.userDao = userDao; } } // 这时在测试 @Test public void testUser(){ UserServiceImpl service = new UserServiceImpl(); service.setUserDao( new MysqlDaoImpl()); service.getUserInfo(); service.setUserDao( new OracleDaoImpl() ); service.getUserInfo(); }
二者区别:
- new的代码是程序控制创建的,主动权在程序;
- set是自行控制创建对象的,主动权在调用者;
- 程序不用管创建实现,只负责提供接口就行了;
- set不用管理对象的创建了,更多的需要关注业务的实现;
2.3、Hello Spring
Spring第一个程序使用的步骤:
-
创建项目、导入Spring的Jar包
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.8.RELEASE</version> </dependency>
-
编写Hello实体类
public class Hello { private String name; ...... }
-
编写Spring配置文件、建议命名为:applicationContext.xml
-
引入文件约束xsd文件,spring-beans-4.2.xsd版本为4.2的spring
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd"> </beans>
-
在配置文件中定义实例对象
<!--bean就是java对象,由Spring创建和管理 --> <bean id="hello" class="com.spring.pojo.Hello"> <property name="name" value="Spring"/> </bean>
-
测试
public class MyTest { public static void main(String[] args) { // 解析xml文件,生产管理相应的Bean对象,可以解析多个xml ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); // getBean():参数为Spring配置文件中Bean标签的id、返回值类型Object类 Hello hello = (Hello) context.getBean("hello"); System.out.println(hello.toString()); } }
-
小结
- Hello对象由Spring创建
- 对象的属性由Spring容器设置
- 这个过程叫控制反转:
- 控制:谁来控制对象创建,传统程序的对象由程序控制创建,用Spring后,对象由Spring来创建。
- 反转:程序本身不创建对象,而变成被动的接收对象
- 依赖注入:利用set方法进行注入
Bean标签
<bean id="student" name="stu s1,s2;s3" class="包名.类名">
<property name="属性名" value="值" />
</bean>
<bean/>
标签:定义一个Java实例对象,由Spring来创建和管理- id:是bean的标识符,唯一,如果没有id,name就是默认标识符
- 如果配置了id又配置了name,那么name是别名;
- name可设置多个别名,用逗号,分号,空格隔开
- 如果不设置id和name可根据**getBean(类名.class)**方法获取对象;
- class:是bean的全限定名 = 包名+类名
2.4、ApplicationContext
(1)、ApplicationContext接口容器
ApplicationContext
:用于加载Spring的配置文件;
两个实现类:
ClassPathXmlApplicationContext
:获取项目类路径下的Spring配置文件;
FileSystemXmlApplicationContext
:获取本地磁盘目录中的或位于项目根路径下的Spring配置文件(本地磁盘写全路径);
getBean()
:从容器中获取对象、通过bean标签的id名获取;
(2)、BeanFactory接口容器
- BeanFactory接口是ApplicationContext接口的父类
- 若创建BeanFactory,需要使用实现类XmlBeanFactory,加载Spring配置文件。
- 而Spring配置文件以Resouce的形式出现在该类的构造参数中,Resource是接口,两个实现类:
- 1、ClassPathResource:类路径下的资源文件
- 2、FileSystemResource:项目根路径或本地磁盘路径下的资源文件。
- 创建BeanFactory后,使用重载的getBean()方法,从容器中获取指定Bean对象。
// 获取类路径下的配置文件
BeanFactory factory = new XmlBeanFactory (
new ClassPathResource("applicationContext.xml"));
// 获取当前项目根目录下的配置文件
BeanFactory factory = new XmlBeanFactory (
new FileSystemResource("applicationContext.xml"));
(3)、两个接口的区别:
- 虽然都是加载同一个配置文件,但不是同一个容器;
- 装配时机不同;
- ApplicationContext装配时机:容器对象初始化时,自动全部装配,若使用直接从内存中取出,执行效率高,但占内存。
- BeanFactory装配时机:第一次调用getBean时才装配对象。
2.5、Bean
Spring容器根据代码创建Bean对象后在传递给代码的过程,称为Bean的装配;
2.4.1、装配
(1)、通过无参构造(默认)
通过getBean()从容器中获取指定Bean的实例,容器会先调用Bean类的无参构造,创建空值的实例对象。
-
在User实体类的无参构造中添加一句话
public User() { System.out.println("user的无参构造"); }
-
applicationContext.xml
<bean id="user" class="com.spring.pojo.User"> <property name="name" value="admin" /> </bean>
-
测试
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); User user = (User) ac.getBean("user"); System.out.println(user.toString());
(2)、动态工厂Bean
Bean中的属性:
factory-bean
:指定相应的工厂Beanfactory-method
:指定创建所用的方法配置中有两个Bean定义,工厂类和所需的目标类Bean,测试时不用获取工厂Bean,而是直接获取目标Bean对象,实现测试类与工厂类的解耦;
<bean id="factory" class="全限定类名" /> <bean id="获取的目标对象名" factory-bean="factory" factory-method="类中的方法" />
2.4.2、静态工厂Bean
静态工厂不需要定义静态工厂Bean,而需要制定工厂类,还需要factory-method制定工厂方法,静态工厂类需要添加static;
2、通过有参构造创建
xml有三种方式编写
<bean id="idName" class="包名.类名">
<!-- 方式一:根据index属性设置下标(从0开始) -->
<constructor-arg index="0" value="值" />
<!-- 方式二:根据name属性设置参数名 -->
<constructor-arg name="names" value="值" />
<!-- 方式三:根据type属性设置参数类型 -->
<constructor-arg type="java.lang.数据类型" value="值" />
</bean>
3、测试
在配置文件加载时,其中管理的对象都已经初始化了;
2.4.2、作用域
通过Spring创建一个Bean实例时,不仅可完成Bean的实例化,还可通过scope属性,为Bean指定作用域。
Bean是由IOC容器初始化,装配及管理的对象;
request、session基于web应用使用;
1、Singleton
当bean的作用域为Singleton,容器会存有一个Bean实例,并对所有对该bean的请求,只要id与该bean定义的相匹配,就会返回同一个bean实例;
Singleton是单例类型,就是在创建容器同时自动创建一个bean的对象,不管是否使用都会存在,每次获取的都是同一个对象,而不会是第二个。
<bean id="user" class="包名.类名" scope="singleton">
测试:
@Test
public void test3(){
ApplicationContext context = newClassPathXmlApplicationContext("applicationContext.xml");
User user = (User) context.getBean("user");
User user2 = (User) context.getBean("user"); System.out.println(user==user2); }
// 结果为:true
2、Prototype
当bean的作用域为Prototype,表示该bean定义对应多个对象实例。
Prototype的bean会在每次对该bean请求(将注入到另一个bean中,或以程序的方式调用容器的getBean())时都会创建一个新的bean实例。
Prototype是原型类型,在创建容器时并没有实例化,而是当获取bean时才会创建一个对象,而且每次获取到的对象都不是同一个对象。
在XML中将bean定义成prototype,配置语法:
<bean id="account" class="com.sp.Account" scope="prototype"/>
或者
<bean id="account" class="com.sp.Account" singleton="false"/>
Singleton和Prototype的应用场景:
对有状态的bean应该使用prototype作用域,而对无状态的bean则应该使用singleton作用域。
3、Request
当bean的作用域为Request,表示在一次HTTP请求中,一个bean定义对应一个实例;
即每个HTTP请求都会有各自的bean实例,它们依据某个bean定义创建而成。
该作用域仅在web的Spring ApplicationContext情形下有效。
Request配置语法:
<bean id="loginAction" class=cn.csdn.LoginAction" scope="request"/>
每次HTTP请求,Spring根据loginAction bean的定义创建一个全新的LoginAction bean对象实例,且该loginAction bean实例仅在当前HTTP request内有效,可以根据需要更改所建实例的内部状态,而其他请求中根据loginAction bean定义创建的实例,看不到这些特定于某个请求的状态变化。当处理请求结束,request作用域的bean实例将被销毁。
4、Session
当一个bean的作用域为Session,表示在一个HTTP Session中,一个bean定义对应一个实例。
Session配置语法:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
针对某个HTTP Session,Spring会根据userPreferences bean定义创建一个新的userPreferences bean实例,且该userPreferences bean仅在当前HTTP Session内有效。与request一样,可根据需要更改所创建实例的内部状态,而别的Session中根据userPreferences创建的实例,不会看到这些特定于某个Session的状态变化。当HTTP Session最终被废弃时,该HTTP Session作用域内的bean也会被废弃掉。
5、global session
每个全局的session对应一个Bean实例,仅在使用portlet集群时有效,多个Web应用共享一个sessioin、global-session与session是等同的;
Request、Session和global session作用域仅在基于web的Spring ApplicationContext情形下有效。
2.4.3、后处理器
容器中所有Bean在初始化时,都会自动执行后处理器类的两个方法,由于bean是由其它Bean自动调用执行,所以不需要id;
-
只需对Bean类中的方法进行判断,即可实现对指定的Bean的指定方法进行扩展与增强,方法返回Bean对象,就是增强过的对象。
-
自定义Bean后处理器并实现BeanPostProcessor接口。
- 该接口包含两个方法:在目标初始化完毕之前与之后执行。
- 返回值就是增强后的Bean对象。
// 在目标初始化完毕之后由容器自动调用执行 public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return null; } // 在目标初始化完毕之前由容器自动调用执行 public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { // 不能让返回值为null,否则抛出NullPointerException return null; } // 参数一:即将初始化的Bean实例,参数二:该Bean实例的id属性值,
定制Bean的生命始末
给指定Bean标签中增加属性:
init-method
:指定初始化方法的方法名
destroy-method
:指定销毁方法的方法名
注意:destroy-method的执行条件:
-
确保Bean为 singleton 单例模式
-
确保ApplicationContext容器关闭,它虽然没有close()方法,但实现类有,只需将ApplicationContext强转为实现类对象或直接创建实现类对象。
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); ((ClassPathXmlApplicationContext)ac).close();
2.4.4、生命周期
Bean实例从创建到销毁的过程:
- 调用无参构造,创建实例对象;
- 调用属性的set方法,为属性注入值;
- 若Bean实现了BeanNameAware接口,则执行接口方法setBeanName(String beanld)使Bean类可获取其在容器中的id名;
- 若Bean实现了BeanFactoryAware接口,则执行接口方法setBeanFactory (BeanFactory factory),使Bean类获取到BeanFactory对象;
- 若定义并注册了Bean后处理器 BeanPostProcessor 则执行接口方法postProcessBeforelnitialization();
- 若Bean实现了InitializingBean接口,则执行接口方法afterPropertiesSet(),该方法在Bean所有属性的set方法执行完后执行,是Bean实例化的结束。
- 若设置init-method方法,则执行;
- 若定义并注册Bean后处理器、BeanPostProcessor,则执行接口方法postProcessAfterlnitialization();
- 执行业务方法
- 若Bean实现了DisposableBean接口,则执行接口方法destroy();
- 若设置了destroy-method方法。则执行。
2.4.5、id与name属性
id和name属性的作用是相同的,如果Bean标签中有特殊字符时就需要使用name属性:
id的命名要满足XML对id属性命名规范:必须以字母开头,可包含数字,字母,下划线,连字符,括号,冒号。
name属性值可包含各种字符;
如果id和name同时设置了那么name则是别名;
三、DI
概念
- 依赖注入(Dependency Injection,DI)
- Bean在调用无参构造创建空值对象后,就要对进行Bean对象的属性初始化。
3.1、基于XML注入
3.1.1、设值注入
Set方法注入:要求被注入的值,必须有set方法,如果属性是boolean类型,没有set方法,是is
案例
1、实体类 address.java
@Setter
public class Address {
private String address;
}
实体类 student.java
@Setter
public class Student {
private String name;
private Address address;
private String[] books;
private List<String> hobbys;
private Map<String,String> card;
private Set<String> games;
private String wife;
private Properties info; // 配置类
public void show(){
System.out.println("name="+ name
+ ",address="+ address.getAddress()
+ ",books="
);
for (String book:books){
System.out.print("<<"+book+">>\t");
}
System.out.println("\n爱好:"+hobbys);
System.out.println("card:"+card);
System.out.println("games:"+games);
System.out.println("wife:"+wife);
System.out.println("info:"+info);
}
}
2、beans.xml
<bean id="address" class="com.spring.pojo.Address">
<property name="address" value="北京"/>
</bean>
<bean id="student" class="com.spring.pojo.Student">
<!-- 常量注入,通过value -->
<property name="name" value="小李子"/>
<!-- ref注入:
当Bean的属性值为另一bean的实例、
通过rel指定他们的引用关系,ref的值必须为某个Bean的id值
或使用 <ref bean="id值" /> 标签
<property>
<ref bean="address" />
</property>标签
-->
<property name="address" ref="address"/>
<!-- 数组注入 -->
<property name="books">
<array>
<value>三国演义</value>
<value>水浒传</value>
<value>西游记</value>
<value>红楼梦</value>
</array>
</property>
<!-- list注入 -->
<property name="hobbys">
<list>
<value>听歌</value>
<value>敲代码</value>
<value>看电影</value>
<value>玩游戏</value>
</list>
</property>
<!-- map注入 -->
<property name="card">
<map>
<entry key="手机号" value="1561325497"/>
<entry key="邮箱" value="2316497454@qq.com"/>
<!-- <description></description>:描述信息-->
</map>
</property>
<!--set注入-->
<property name="games">
<set>
<value>lol</value>
<value>cf</value>
</set>
</property>
<!-- null值注入 -->
<property name="wife">
<null/>
</property>
<!-- properties注入 -->
<property name="info">
<props>
<prop key="学号">20200808</prop>
<prop key="性别">男</prop>
<prop key="姓名">小明</prop>
</props>
</property>
</bean>
3、测试
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
Student student = (Student) ac.getBean("student");
student.show();
}
4、结果
name=小李子,address=Address(address=北京),books=
<<三国演义>> <<水浒传>> <<西游记>> <<红楼梦>>
爱好:[听歌, 敲代码, 看电影, 玩游戏]
card:{手机号=1561325497, 邮箱=2378262946@qq.com}
games:[lol, cf]
wife:null
info:{学号=20200808, 性别=男, 姓名=小明}
3.1.2、 构造注入
使用前必须提供有参构造,在通过<constructor-arg />
标签完成注入;
<bean id="student" class="com.spring.pojo.Student">
<constructor-arg name="" value="" />
<constructor-arg name="" ref="" />
</bean>
name:指定参数名
index:指定参数对应有参构造的第几个参数,从0开始,注意,若参数类型相同或有包含关系,则需要保证赋值顺序要与有参构造的参数顺序一致。(不常用)
type:指定类型,(非基本数据类型要写全限定类名)
3.1.3、命名空间注入
P命名空间注入:是设值注入的方式,需要有相应的Set方法
C命名空间注入:是有参构造的方式,需要有相应的有参构造
使用P命名和C命名进行注入时,都需要添加对应约束
1、实体类user.java
public class User {
private String name;
private int age;
// 自行提供get/set
}
2、p命令空间注入
约束:xmlns:p="http://www.springframework.org/schema/p"
<!--p 命名空间注入,可直接注入属性值-->
<bean id="user" class="com.spring.pojo.User" p:name="大力" p:age="18" />
3、c命令空间注入
约束:xmlns:c="http://www.springframework.org/schema/c"
<!--c 命名空间注入,可直接注入属性值-->
<bean id="user2" class="com.spring.pojo.User" c:name="李青" c:age="22"/>
问题:如果爆红,没有写有参构造
解决:添加构造,c就是构造注入;
4、测试
@Test
public void testp(){
ApplicationContext context = new ClassPathXmlApplicationContext("userbean.xml");
User user = (User) context.getBean("user",User.class);
System.out.println(user);
User user2 = (User) context.getBean("user2",User.class);
System.out.println(user2);
}
3.1.4、域属性注入
通过Bean标签设置autowird属性值,进行隐式自动注入,根据自动注入判断标准的不同分为两种:
1、byName:自动在上下文中找,和对象set方法后面的值对应的bean标签的id、优化了字母缺漏和大小写错误
如果找不着id执行时报空指针异常。
2、byType:自动在上下文中查找,和对象属性类型相同的bean,保证类型要全局唯一才会自动装配、如果不唯一则抛出不唯一异常NoUniqueBeanDefinitionException
<bean id="address" class="com.spring.pojo.Address" autowire="byName">
<!-- autowire="byType" -->
<property name="address" value="北京"/>
</bean>
3.1.5、SPEL注入
SPEL,Spring Expression Language,即Spring EL表达式语言。
为Bean的属性注入值时,可使用SPEL表达式计算结果或转换。
语法:#开头后跟一对大括号,#{}
用法:<bean id="abc" value="#{..}">
3.1.6、抽象类和异类抽象注入
抽象类
抽象Bean用于让其它bean继承的,不能通过getBean获取,在Bean标签中设置abstract属性为true指定为抽象bean,默认false。
异类抽象
有多个不同的类对象具有相同属性且值也相同时,可使用异类抽象Bean。
添加属性:parent=“抽象bean的id名”
3.1.7、Spring配置
别名设置
:为Bean设置别名,可设多个,获取Bean对象时可用别名获取;
<alias name="bean的id名" alias="别名" />
import配置
如果有多个配置文件则都需要new
ClassPathXmlApplicationContext(“applicationContext.xml”);
获取多个Spring配置文件,这样会造成代码冗余;建议将所有配置文件通过
标签引入到一个总的配置文件中使用;
<import resource="{path}/其他文件.xml"/>
3.2、基于注解注入
开启Spring注解支持
<context:annotation-config>
使用步骤:
-
导入aop的jar包
-
在配置文件中导入aop约束
-
配置包扫描器
<context:component-scan base-package="要扫描的包" />
作用:将指定包下的类交给Spring来管理
其需要添加注解:@Component
@Component:让Spring扫描到,Spring还提供了和此注解等效的注解,目的为了区分是那些层下的类
@Repository :dao数据访问层类
@Service :service业务层类
@Controller :controller控制层类
作用域注解:@Scope(“指定作用域”):默认singleton
属性注入值注解:@Value(“值”):写在属性或set上,作用:指定注入的值
类型注入:@Autowired:按照类型自动装配
required=true:(默认)要求对象必须存在,true不能为null,false可为null;
@Qualifier
- 配合@Autowired一起使用、用于指定匹配的Bean的id值。
- 此注解不可单独使用
@Resource
- 如果设置name属性,则进行byName方式查找名称
- 如果找不到,则按照byType方式根据类型查找;
@Autowired与@Resource区别:
-
都用于装配Bean、都可写在字段或set方法上;
-
@Autowired默认按照类型装配、必须要求对象存在,可设置required,如果想用名称进行装配可结合@Qualifier
-
@Resource默认按照名称装配、名称可用name属性指定、此注解写在字段上时,默认按照字段名查找、如果是在setter上默认取属性名装配、
找不到与名称匹配的bean时才安装类型装配,如果name一旦指定,则只会按照名称装配;
Bean的声明始末注解
@PostConstruct:与属性init-method等效
@PreDestroy:与属性destroy-method等效
使用步骤:
1、导入jar包后在配置文件中引入约束(beans、context、aop)
1、配置扫描指定包下的注解
<!-- 指定注解扫描包-->
<context:component-scan base-package="包名"/>
<!--比如:扫描com.spring包下所有类-->
<context:component-scan base-package="com.spring"/>
2、在指定包下编写类并增加注解
@Component()括号中可写类名:“类名首字母小写” 相当于bean中的class属性
<bean id="" class="当前注解类">
@Component()
public class User(){....}
属性用注解注入
1、不提供set、而是在属性上添加@Value(“值”)注解
2、@Value(“值”):相当于<property name="name" value="值">
3、如果提供了set,也可在set上添加@Value注解
@Component("user")
public class User {
@Value("李四")
private String name;
}
基于Java类配置
JavaConfig原来是Spring的子项目,它通过类的方式提供Bean的定义信息,在Spring4中JavaConfig正式成为Spring4的核心功能。
使用步骤:
-
编写实体类,并将类标注为Spring的组件,放入容器中!
-
编写配置类、添加 @Configuration 注解表示这是一个配置类、
-
在这个配置类中,编写方法获取我们想要的对象;
-
方法用 @Bean 标注,返回值类型是Bean的class类型,方法名为Bean的id;
-
测试
// 1、忽略实体类.... // 2、 @Configuration public class MyConfig { @Bean public User user(){ return new User(); } } // 3、忽略测试...
-
导入其他配置类 @Import(配置类名.class)
-
Junit测试
在测试类上添加:
@RunWith(SpringJUnit4ClassRunner.class) 指定运行环境
@ContextConfiguration(“classpath:ApplicationContext.xml”) 写类路径下的Spring配置文件
在测试方法上添加@Test注解,其方法返回值必须是void,选中方法即可运行、不需要main方法
注解与XML
- 注解好处:配置方便,直观,但弊端:硬编码,其修改需要重新编译代码
- XML好处:对代码修改,无需编译代码,只需重启服务期即可。
- 如果注解与XML同用,XML优先级高于注解
四、AOP
4.1、代理模式
AOP底层机制是动态代理
代理模式分类:
- 静态代理
- 动态代理
动态代理是OCP开发原则的一个体现:在不修改业务的前提下,扩展和增强其他功能。
静态代理
- 抽象:接口或抽象类实现
- 真实:被代理的人物
- 代理:代理真实人物,代理真实人物后,有附属操作;
- 客户:使用代理人物进行一些指定操作;
示例:
Rent.java 接口/抽象人物:要租房
public interface Rent {
public void rent();
}
Host.java、实现类/真实人物:房东,出租房屋
public class Host implements Rent{
public void rent() {
System.out.println("房屋出租");
}
}
Proxy.java 代理人物
public class Proxy implements Rent{
private Host host;
public Proxy() {}
public Proxy(Host host) {
this.host = host;
}
public void rent() {
seeHouse();
host.rent();
fare();
}
public void seeHouse(){ // 附属操作
System.out.println("带客户看房");
}
public void fare(){ // 附属操作
System.out.println("中介收费");
}
}
Client.java:客户
public class Client { // 客户
public static void main(String[] args) {
Host host = new Host(); // 房东租房
Proxy proxy = new Proxy(host); // 中介帮房租
proxy.rent(); // 你找中介
}
}
分析:在过程中,你直接接触的是中介,你看不到房东,但你通过代理依然租到了房东的房子。
自动代理
默认advisor自动代理:DefaultAdvisorAutoProxyCreator:将所有的目标对象与Advisor自动结合,生成代理对象。
Bean名称自动生成:BeanNameAutoProxyCreator:根据bean的id,为符合相应名称的类生成相应代理对象,且切面可是顾问又可是通知。
property属性:beanNames:指定要增强的目标类的id
interceptorName:指定切面(顾问或通知)
4.2、AOP概述
AOP(Aspect Oriented Programming)面向切面编程、是OOP的一种补充,OOP是从静态角度考虑程序的结构的,而面向切面是从动态角度考虑程序运行过程。
AOP通过预编译方式和运行期动态代理(JDK动态代理与CGLIB)实现程序功能的统一维护的一种技术。
AOP将与主业务无关的(如:事务,日志,安全等)封装成切面,使用AOP将切面织入到主业务中。
AOP使业务逻辑各部分之间的耦合度降低,提高程序可重用性,提高开发效率;
若不用AOP导致大量代码冗余,高耦合,降低了代码的可维护性,可读性。
术语
- 切面(Aspect):对主业务的一种增强,指与主业务无关的,如:事务,日志,安全等就可理解为切面。
- 织入(Weaving):将切面代码插入到目标对象的过程,与invoke()方法相似。
- 连接点(JoinPoint):指可以被切面织入的方法,业务接口中的方法均为连接点。
- 切入点(Pointcut):指切面具体织入的方法,被标记的final方法是不能作为连接与切入点的。
- 目标对象(Target):值要增强的对象。
- 通知(Advice):完成织入功能,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前还是之后等,切入点定义切入的位置,通知定义切入的时间。
- 顾问(Advisor):以通知的方式织入到目标对象中,将通知保证为更复杂切面的装配器。
4.3、通知Advice
通知使用步骤:
-
导入jar包
aopalliance.包和aop包
-
在配置文件导入约束
<?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: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"> </beans>
-
定义目标类:被增强的Bean类
-
定义通知类:根据不同类型的通知,执行时机不同:分别有:前置通知,后置通知,环绕通知,异常通知。
-
在配置文件中注册目标类Bean
-
注册通知切面:通知对象bean
-
注册代理工厂Bean类对象ProxyFactoryBean
-
从容器直接获取代理对象即可
<bean id="proxyFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean"> <!-- 指定三部分:目标类,接口,切面 --> <property name="" ref="目标对象Bean的ID 或使用Value属性" /> <property name="" value="接口全限定名" /> </bean>
ProxyFactoryBean源码中:自动检测目标类的所有接口属性autodetectInterfaces:默认值true。
此时用的是JDK的Proxy动态代理
<!-- 主要配置: -->
<!-- 1、配置目标对象 -->
<!-- 2、配置切面:通知 -->
<!-- 3、配置代理 -->
前置通知:MethodBeforeAdvice
实现:MethodBeforeAdvice接口,before()方法
before()方法参数:
method:业务方法
args[] :业务方法的参数
target: 目标对象
前置通知特点:
- 目标方法执行之前执行
- 不改变目标方法的执行流程和结果
后置通知:AfterReturningAdvice
实现:MethodAfterReturningAdvice接口,afterReturning方法
后置通知特点:
- 目标方法执行之后执行
异常通知:ThrowsAdvice
实现:ThrowsAdvice接口,作用:在目标方法抛出异常后根据异常的不同作出对应的处理。
实现方法:afterThrowing(自定义的异常类 e);
环绕通知:MethodInterceptor
包位置:org.aopalliance.intercept.MethodInterceptor
实现MethodInterceptor接口,环绕通知也叫方法拦截器,可以再目标方法调用之前和之后做出处理,可改 变目标方法的返回值和程序的执行流程;
给目标织入多个切面
在配置代理对象的切面属性时,设为list即可
<property name="属性名">
<list>
<value>切面id名</value>
</list>
</property>
4.4、顾问Advisor
顾问是Spring提供的一种切面,只能将切面织入到目标类的所有方法中,无法将切面织入到指定目标方法中。
PointcutAdvisor是顾问的一种,可指定具体的切入点,顾问将通知进行了包装,根据不同的通知类型,在不同时机将切面织入到不同的切入点;
PointcutAdvisor接口的实现类:
-
NameMatchMethodPointcutAdvisor:名称匹配方法切入点顾问
<!-- 配置目标对象 --> <!-- 配置切面:通知 --> <!-- 配置切面:顾问 --> <bean id="顾问id名" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor"> <property name="" ref="通知id名" /> </bean> <!-- 配置代理 --> <bean id="proxyFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="" value="顾问id名" /> </bean>
-
RegexpMethodPointcutAdvisor:正则表达式匹配方法切入点顾问
正则常用运算符:
运算符:. 点号:任意单个字符
- 加号:前一个字符出现一次或多次
- *星号:前一个字符出现0次或多次
4.5、Aspectj
简介
AspectJ是面向切面的框架,扩展了Java语言,有专门的编译器来生成字节码规范的class文件。
通知类型
- 前置通知
- 后置通知
- 环绕通知
- 异常通知
- 最终通知
切入点表达式
使用expression属性后面跟表达式:
execution ( 访问修饰符 返回值类型 全限定类名 方法名 (参数名) )
常用符号:
星号:0或任意字符
两个点:表示方法中的所有参数
加号:类名或接口后,表示当前类或接口
expression=“execution(* com.aop.service.impl.StudentServiceImpl.add*(…))”
举例:指定切入点
任意公共方法:*execution(public * (…))
指定包下的所有方法:execution( com.aop.*(…))*
指定包下以那些方法开头:execution( com.aop.add(…))"**
基于注解实现AOP
步骤:
-
定义业务接口与实现类
-
定义切面类(前置通知或后置通知等)
-
在切面类添加**@Aspect**注解表示这个类是切面
-
在切面类的方法中添加注解,其注解有:
-
@Before():前置增强,括号内写切入点表达式、前置通知的方法内参数可有
- JoinPoint类型参数:用于获取切入点表达式,方法签名目标对象等。
- 常用方法:获取方法签名:getSignature();、获取目标对象:getTarget()
- JoinPoint类型参数:用于获取切入点表达式,方法签名目标对象等。
-
@AfterReturning():目标方法执行完之后,可获取目标方法的返回值。
- returning="参数名"属性:指定接收方法返回值的变量名,最好设定为是Object,因为目标返回值可能是任何值
-
@Around():环绕通知、为环绕增强的方法必须要有返回值、Object类型,方法可包含ProceedingJoinPoint类型的参数、其**procedd()**方法,用于执行目标方法,若目标方法有返回值,则该方法返回值就是目标对象的返回值,最后将返回值返回,该增强是为了拦截目标方法的执行。
-
@AfterThrowing:异常通知
- throwing属性:抛出异常后执行、属性用于指定发生的异常类对象,可包含Throwable参数名为throwing指定的名称,表示异常对象。
-
@After:最终通知,无论是否有异常,都会执行。
-
@Pointcut:定义切入点表达式:当有较多增强方法使用execution切入点表达式时,维护比较麻烦、
用法:将次注解标在一个方法上,之后所有的通知方法execution的value属性值均可用该方法名作为切入点。
-
-
在配置文件中注册目标对象与切面类
-
注册AspectJ的自动代理:
<aop:aspectj-autoproxy>
<aop:aspectj-autoproxy>
底层是AnnotationAwareAspectJAutoProxyCreator实现的。- 工作原理:通过扫描找到@Aspect定义的切面类,在由切面类根据切入点找到目标类的目标方法、在由通知类找到切入的时机。
-
测试类中使用目标对象的id
基于XML实现AOP
步骤
-
定义业务接口与实现类
-
定义切面类
-
注册目标对象与切面类
-
在Spring配置文件中定义AOP配置
<!-- 配置aop:config--> <aop:config> <aop:aspect ref="MyAdvise"> <!--配置切入点--> <aop:pointcut expression="execution切入点表达式" id="MyPointcut"/> <!-- 前置通知 --> <aop:before method="beforeAdvise" pointcut-ref="MyPointcut"/> <!-- 后置通知 --> <aop:after method="afterAdvise" pointcut-ref="MyPointcut"/> <!-- 后置返回通知 --> <aop:after-returning method="afterReturningAdvise" pointcut-ref="MyPointcut"/> <!-- 后置异常通知 --> <aop:after-throwing method="afterThrowingAdvise" pointcut-ref="MyPointcut"/> <!-- 环绕通知,常用 --> <aop:around method="方法名" /> <!-- 最终通知 --> <aop:after method="方法名"/> <!-- 引入通知,不常用 --> <aop:declare-parents types-matching="" implement-interface=""/> </aop:aspect> </aop:config>
method属性用于指定通知的方法名,如果有重载方法,则需要方法的参数类型名;
五、DAO
5.1、JdbcTemplate
概念
jdbcTemplate是Spring提供的模板,目的是为了简化JDBC、避免了JDBC的冗长代码,而数据源与模板都可在配置文件中。jdbcTemplate只需了解即可。
步骤
一、搭建环境
- 搭建数据库和创建表
- 导入jdbc和事务的jar包
- 创建实体类
- 定义dao接口和impl
- 定义service接口和impl
- 定义测试类
二、配置数据源
数据源不同,配置方式则不同,常见的数据源配置:
1、Spring默认数据源
2、DBCP数据源
3、C3P0数据源
-
Spring默认数据源
DriverManagerDataSource类继承AbstractDriverBasedDataSource
其name属性值有:
DriverClassName:加载DB驱动,在value属性上写即可
url:连接数据库
username:用户名
password:密码
-
DBCP数据源BasicDataSource
DBCP(DataBase Connection Pool)是Apache下的项目,用该数据源,需要导入commons.bdcp和commons.pool的jar包,其中的属性和Spring默认数据源等效。
-
C3P0数据源ComboPooledDataSource
用该数据源,需要导入c3p0的jar包,其属性有:
driverClass:加载驱动
jdbcUrl:连接数据库
user :用户名
password:密码
<!-- 配置C3P0数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="com.mysql.jdbc.Driver" /> <property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/数据库名" /> <property name="user" value="用户名" /> <property name="password" value="密码" /> </bean>
从属性文件读取数据库连接信息
将数据库连接信息写入属性文件中,让Spring从中读取数据,便于维护,。
步骤:
-
创建properties属性文件,名称随意,一般放在src底下
-
属性文件中不能以分号结尾
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/数据库名 jdbc.user=root jdbc.pwd=root
-
在Spring配置文件中读取属性文件:location指定文件位置
<context:property-placeholder location="属性文件名.properties"/>
或使用PropertyPlaceholderConfigurer
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="location" value="classpath:文件名.properties" /> </bean>
<!-- 引入外部文件 --> <context:property-placeholder location="属性文件名.properties"/> <!-- 声明数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="${jdbc.driver}" /> <property name="jdbcUrl" value="${jdbc.url}" /> <property name="user" value="${jdbc.user}" /> <property name="password" value="${jdbc.pwd}" /> </bean>
四、实现类继承JdbcDaoSupport类
jdbcDaoSupport类的jdbcTemplate属性用于接收JDBC模板、所以继承此类也具有模板属性,只需在配置 文件中将模板对象注入即可。
dataSource属性,setDataSource()方法,如果jdbc模板为null,则会自动创建模板对象,所以配置jdbc模 板可省略。
@Autowired private JdbcTemplate template;
<!-- 声明template --> <bean id="template" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean>
五、对数据库的增删改操作
增删改操作都是通过 update()方法实现的、该方法的重载方法:
public int update (String sql );
public int update (String sql , Object... args);
参数一:要执行的SQL语句。
参数二:SQL中包含的动态参数。
返回值:受影响行数。
查询操作
根据返回对象类型不同,可将查询分为两类:简单对象查询,自定义对象查询。
简单对象查询:结果为:String、Integer等对象类型,或元素的集合类型。
自定义对象查询:结果为一个类对象等。
简单对象查询
查询方法:
public T queryForObject (String sql, Class<T> type, Object... args);
:查询结果为单个值,值返一条数据,如果没有查询到这条数据,那么返回null的同时出现异常; 如果出现IncorrectResultSizeDataAccessException这个异常表示:数据库中有多个相同信息的数据,不知道找那个,解决:通过get(0),找第一个返回。
public List<T> queryForList (String sql, Class<T> type, Object... args);
:查询结果为List集合; 自定义对象查询
public T queryForObject (String sql, RowMapper<T> m, Object... args);
:查询结果为单个对象
public List<T> query (String sql, RowMapper<T> m, Object... args);
:查询结果为List集合;**注意:**RowMapper为记录映射接口,用于将查询结果集每条记录包装为指定对象,该接口有一个方法要实现:
public Object mapRow(ResultSet rs,int rowNum)
参数rowNum为当前行的记录所定义的结果集,仅仅是当前行的结果,一般该方法体重就是实现将查询结果中当前行的数据包装为一个指定对象。template.query(sql, new BeanPropertyRowMapper(对象类型.class));
JDBCTemplate对象时多例的,即系统会为每个模板对象的线程方法创建一个JdbcTemplate实例,该线程结束时,会自动释放JdbcTemplate实例。
六、Transaction
将事务提升到业务层,目的为了使事务的特性来管理具体的业务。
Spring实现事务的管理方式:
- 事务代理工厂管理事务
- 事务注解管理实务
- AspectJ的AOP配置事务;
6.1、Spring事务管理API
一、事务管理器接口
事务管理器是PlatFormTransactionManager接口对象。
用于完成事务的提交,回滚,获取事务状态信息。
实现类:
DataSourceTransactionManager: 使用jdbc或ibatis进行持久化数据时使用;
HibernateTransactionManager: 使用Hibernate进行持久化数据时用;
二、回滚方式
默认回滚方式:发生运行时异常和发生受查异常时提交回滚。
三、错误与异常
Throwable类:是所有错误与异常的超类,只有当对象是此类实例时,才能通过虚拟机或throw语句抛出。
Error:运行过程中无法处理的错误,JVM会终止线程。
异常分为运行时与受查时异常:
运行时异常:RuntimeException类:只有在运行时才出现的异常,比如NullPoioterException,ArrayIndexOutOfBounds等,运行时异常可避免。
受查时异常 :也叫编译时异常,要求必须捕获或抛出的异常,若不处理则无法通过编译,比如SQLException,ClassNotFoundException、IOException等。
四、事务接口
TransactionDefinition:常量:事务隔离级别、传播行为,默认超时时限,等
事务隔离级别常量:
这些常量以ISOLATION_开头,即:ISOLATION_XXX
- DEFAULT:数据库默认,MySQL默认为:REPEATABLE_READ; Oracle默认为:READ_COMMITTED
- READ_UNCOMMITTED:读未提交、未解决任何并发问题(脏读)
- READ_COMMITTED:读已提交、解决脏读,存在 不可重复读与幻读
- REPEATABLE_READ:可重复读、解决脏读、不可重复读、存在幻读。
- SERIALIZABLE:串行化、不存在并发问题
事务传播行为常量:
传播行为指处于不同事务中的方法在相互调用时,执行期间事务的维护情况。
事务行为常量以PROPAGATION_开头,即:PROPAGATION_XXX
- REQUIRED:(默认)指定的方法必须在事务内执行,若当前存在事务,会加入当中,若当前没事务,则创建事务。
- SUPPORTS:指定方法支持当前事务,若没事务,也可以非事务方式执行。
- MANDATORY:指定方法必须在当前事务内执行,若没事务,则抛出异常。
- REQUIRES_NEW:总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
- NOT_SUPPORTED:指定方法不能再事务环境中执行,若当前存在事务,则将事务挂起。
- NEVER:指定方法不能再事务环境下执行,若存在事务,直接抛异常。
- NESTED:指定方法必须存在事务,若当前存在事务,则在嵌套事务内执行,若没有,则创建。
定义默认事务超时时限
常量TIMEOUT_DEFAULT:定义了事务默认的超时时限,及不支持事务超时时限设置的none值,
该值一般默认即可。
6.2、代理工厂管理事务
该方式需要目标类,即业务层的实现类创建事务代理,事务代理的类是TransactionProxyFactoryBean该类需要初始化一些属性:
- transactionManager:事务管理器
- target:目标对象,即业务实现类对象
- transactionAttributes:事务属性设置
要求:购买股票时,Account中扣除相应金额,然后在Stock中增加相应的股票数量。
步骤
-
导入jar包
-
定义数据库及表:银行账户表Account和股票账户Stock、
-
定义实体类
-
定义dao接口和实现类,实现jdbcDaoSupport
-
定义service接口和实现类
-
在容器中添加事务管理器
<!-- 配置事务 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 配置数据源 --> <property name="dataSource" ref="dataSource" /> </bean>
-
在容器中添加事务代理TransactionProxyFactoryBean
<bean id="myTransactionProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager" ref="transactionManager" /> <property name="target" ref="业务实现类对象" /> <property name="transactionAttributes"> <props> <prop key=""></prop> </props> </property> </bean>
-
测试
6.3、事务注解
通过 @Transactional 注解,需要在配置文件中加入tx标签,告诉Spring使用注解完成事务织入。
<!-- 通过注解方式实现事务、开启事务 -->
<tx:annotation-driven/>
@Transactional可选属性:
- propagation:设置事务传播,枚举类型,默认值:Propagation.REQUIRED
- isolation:设置事务隔离级别,枚举类型,默认值:Isolation.DEFAULT
- readOnly:设置该方法对数据库操作是否只读。boolean类型、默认值:false
- timeout:设置与数据库连接的超时时限、单位秒、类型int、默认:-1,即没有时限。
- rollbackFor:指定要回滚的异常、类型为Class[],默认值:空数据、一个异常类时,可不用数组。
- rollbackForClassName:指定要回滚额异常类类名,类型为String[]、同上。
- noRollbackFor:指定不需要回滚的异常类。
注意:此注解只能用于public的方法上、如果不是public修饰的方法,虽然不会报错,但不会讲事务织入到该方法中,如果此注解在类是,表示该类下所有方法在执行时织入事务。
6.4、AspectJ的AOP配置事务管理
步骤:
-
导入jar
-
在容器中添加事务管理器DataSourceTransactionManager
-
配置事务通知
<!-- 配置事务通知,*代表所有方法 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*"/> </tx:attributes> </tx:advice>
-
配置顾问 通知+切入点,必须指明切入点在业务层,否则抛出异常或循环调用;
<!-- AOP --> <aop:config> <!-- 配置事务切点 --> <aop:pointcut expression="execution(* )" id="txAdvice"/> <!-- 切面 --> <aop:aspect ref="notice"> <!-- 切点 --> <aop:pointcut expression="execution (* com.sp.service.impl.CommodityServiceImpl.all_Commodity())" id="myAll"/> <!-- 前置通知 --> <aop:before method="beforeInfo" pointcut-ref="myAll"/> <!-- 后置通知 --> <aop:after method="afterInfo" pointcut="execution(* com.*.*..CommodityServiceImpl.all_Commodity())"/> <!-- 返回值通知 --> <aop:after-returning method="returningInfo" pointcut-ref="myAll"/> <!-- 异常通知 <aop:after-throwing method=""/> --> <!-- 环绕通知 --> <aop:pointcut expression="execution(* com.sp.service.impl.CommodityServiceImpl.buy(..))" id="MyBuy"/> <aop:around method="arounds" pointcut-ref="MyBuy"/> </aop:aspect> </aop:config>
感谢观看,如果您觉得写得的不错,麻烦请点赞或收藏,如果有不同意见或建议,请留言或私信,谢谢。