Spring是什么
- Spring是分层的java SE/EE应用轻量级框架,以IOC(Inverse Of Control:控制反转)和AOP(Aspect Oriented Programming:面向切面编程)为内核,提供了展现出springMVC和持久层SpringJDBC以及业务层事务管理等众多的企业家应用技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的Java EE企业应用开源框架
Spring的优势
-
方便解耦,简化开发
通过spring提供的IOC容器,可以将对象间的依赖关系交由spring进行控制,避免硬编码所造成的过度程序耦合.用户也不必再为模式类,属性文件解析等这些底层的需求编写代码,可以更专注于上层的应用. -
AOP编程
通过spring的AOP功能,方便进行面向切面的编程,如多不容易用传统OOP实现的功能可以通过AOP轻松应对 -
声明事务支持
可以将我们从单调的烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务的管理,提高开发效率和质量 -
方便程序的测试
可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是昂贵的操作,而是随手可做的事情 -
方便集成各种优秀的框架
spring可以降低各种框架的使用难度,提供了对各种优秀框架的支持(Struts、Hibernate、Hessian、Quartz等) -
降低了JAVAEE API的使用难度
Spring对JAVAEE API进行了轻量的封装,使这些API的使用难度大大降低 -
JAVA源码是经典学习范例
spring的源码设计精妙,结构清晰,处处体现着大佬对JAVA设计模式灵活运用以及对JAVA技术的高深造诣
高内聚低耦合 -
在软件工程中,耦合指的就是对象之间依赖性,对象之间的耦合越高,维护成本越高.因此对象的设计应使类和构件之间的耦合最小.软件设计中通常用耦合度和内聚度作为衡量模块独立程度的标准.模块划分的一个准则就是高内聚低耦合
-
内聚标志一个模块内各个元素彼此结合的紧密程度,它是信息隐蔽和局部化概念的自然扩展。内聚是从 功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系。
-
耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。 程序讲究的是低耦合,高内聚。就是同一个模块内的各个元素之间要高度紧密,但是各个模块之 间的相互依存度却要不那么紧密。
-
内聚和耦合是密切相关的,同其他模块存在高耦合的模块意味着低内聚,而高内聚的模块意味着该模块同其他模块之间是低耦合。在进行软件设计时,应力争做到高内聚,低耦合。
Spring核心之IOC
Ioc—Inversion of Control,即"控制反转",是一种设计思想.我们在编写程序时,通过控制反转,将对象的创建交给了spring,但是代码中不可能出现没有依赖的情况,IOC解耦只是降低他们的依赖关系,但不会消除.例如,业务层仍会调用持久层的方法,那这种业务层和持久层的依赖关系,在使用spring之后,就交给spring来维护.也就是说,坐等框架把持久层对象传入业务层,而不用我们自己去获取
IOC细节
容器对象的类结构
-
beanFactory是spring容器的顶层接口
-
接口ApplicationConetext是beanFactory子接口
实现类:ClassPathXmlApplicationContext --从类路径之下读取配置文件(常用) -
BeanFactory与ApplicationContext区别
Resource resource=new ClassPathResource("beans.xml");
//BeanFactory:创建容器对象时,只是加载了配置文件,没有创建对象
//获取对象时:创建对象
BeanFactory beanFactory=new XmlBeanFactory(resource);
object userDao =beanFactory.getBean("userDao");
Syetem.out.println(userDao);
//创建spring的IOC容器
//在创建容器对象时,创建对象(常用)
//ApplicationContext:在创建容器时只创建单例模式的对象
//多例模式的对象,在获取时创建
ApplicationContext ac=new ClassPathXmlApplicationContext("beans.xml");
object userDao2 =ac.getBean("userDao");
System.out.println(userDao2);
getBean方法
ApplicationContext ac=new ClassPathXmlApplicationContext("applicationContext.xml");
//根据名称获取该对象
//object userDao =ac.getBean("userDao");
//System.out.println(userDao);
//根据类型获取该对象
//如果该类型有两个实现类,会执行异常
//UserDao userDao=ac.getBean(UserDao.class);
//System.out.println(userDao);
//得到是id为userDao1的对象,类型为UserDao接口的实现类
UserDao userDao = ac.getBean("userDao2",UserDao.class);
System.out.println(userDao);
bean对象的范围和声明周期
springIOC容器默认是单例模式:单例模式的对象在创建容器时创建,销毁容器时销毁
scope:prototype 原型模式(多例模式):获取时创建,当对象长时间不再被引用,则被垃圾回收机制回收
scope:singleton:单例模式
scope:request:会话范围
scope:global session :全局范围 --spring 5的新特性中被删除
UserDao userDao1 =ac.getBean("userDao",UserDao.class);
System.out.pringln(userDao1);
UserDao userDao2=ac.getBean("userDao",UserDao.class);
System.out.println(userDao2);
实例化bean的三种方法
- getBean(name)
- 根据静态工厂获取对象
/**通过静态工厂获取对象*/
public class StaticFactory{
public static UserDao getUserDao()
return new UserDaoImpl();
}
}
<!-- 通过静态工厂创建UserDao对象-->
<!-- factory-method:工厂方法,返回UserDao对象的方法名-->
<bean id="userDao" class="com.ayyy.factory.StaticFactory" factory-method="getUserDao">
</bean>
- 根据实例(非静态)工厂获取对象
/**实例工厂创建对象*/
public class InstanceFactory{
public UserDao getUserDao(){
return new UserDaoImpl();
}
}
<!-- 创建实例工厂对象-->
<bean id="instanceFactory" class="com.ayyy.factory.InstanceFactory" factory-method="getUserDao"></bean>
依赖注入
-
什么是依赖注入
业务层需要持久层的对象,在配置文件中给业务层传入持久层的对象,就是依赖注入 -
IOC
控制反转包含了依赖注入和依赖查找 -
构造方法注入
<!-- 依赖注入-->
value 属性只能赋值简单的类型:基本数据类型和String类型
ref: pojo类型,复杂类型,关联创建好的对象
<!--默认创建对象方式,使用默认的空的构造方法创建对象-->
<bean id="user" class="com.ayyy.domain.User">
<!--<constructor-arg index="0" value="1"></constructor-arg>-->
<!--<constructor-arg index="1" value="张三"></constructor-arg>-->
<!--通过构造方法参数类型赋值-->
<!--<constructor-arg type="java.lang.Integer" value="2"></constructor-arg>-->
<!--<constructor-arg type="java.lang.String" value="李四"></constructor-arg>-->
<!--通过构造方法参数名字赋值-->
<constructor-arg name="id" value="3"></constructor-arg>
<constructor-arg name="username" value="王五"></constructor-arg>
<constructor-arg name="sex" value="男"></constructor-arg>
<constructor-arg name="birthday" ref="birthday"></constructor-arg>
</bean>
<bean id="birthday" class="java.util.Date"></bean>
- set方法注入属性
<!--通过set方法注入-->
<bean id="user2" class="com.ayyy.domain.User">
<!--property :属性注入,先找到set方法,才能最终找到属性-->
<property name="username" value="王朝"></property>
<property name="birthday" ref="birthday"></property>
<property name="sex" value="男"></property>
<property name="id" value="4"></property>
</bean>
- p名称空间注入:基于set方法注入
在头部文件中引入p名称空间
使用p名称空间注入属性
<bean id="user3" class="com.ayyy.domain.User"
p:id="5" p:username="马汉" p:sex="男" p:birthday- ref="birthday"
></bean>
常用的注解
- 配置文件:applicationContext.xml
<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
引入context名称空间-引入约束
base-package:指定要扫描的包, 扫描的是包及其子包
-->
<context:component-scan base-package="com.ayyy"></context:component-scan>
<!--
context:include-filter :指定包含过滤
type="annotation": 按照类型过滤
expression: 过滤的表达式
只扫描标记了Controller注解的类
-->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"></context:include-filter>
<!--
context:exclude-filter:指定排除过滤
type="annotation": 按照类型过滤
expression:过滤的表达式
排除标记了Controller的注解都会扫描
-->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</beans>
- 在需要创建对象的类上添加注解
@Component --标记在类上,不能用在方法上
作用:创建对象,只要标记了,扫描了该包,对象就会创建衍生了三个子注解
@Controller :一般用于web层(控制层)
@Service :一般用于业务层
@Repository :一般用于持久层
相当于xml
<bean id="" class="全限类名"></bean>
属性:value=“userDao” 相当于xml的id=“userDao”
如果没有指定value属性,默认的名称是简单类名,首字母小写
UserDaoImpl --userDaoImpl
-
@Autowired --自动注入
可以标记在属性和set方法上,如果标记在属性上,可以没有set方法
特点:默认自动按照类型注入
流程:当属性set方法标记了@Autowired,会自动在容器中查询该属性类型的对象,如果有且只有一个,则注入@Qualifiter:必须与@Aotuwored结合使用
作用:如果自动注入按照类型注入失败,则按照指定的名称注入
如果没有@Qualifiter,类型注入失败,则按照属性名称按照名称注入 -
@Resource --自动注入
流程:当属性set方法标记了@Resource,会自动按照名称注入,如果名称没有找到,则根据类型注入,如果类型有多个,则抛出异常 -
@Autowired:默认按照类型注入,如果类型有多个,则按照名称注入–spring提供
-
@Resource:默认按照名称注入,没有名称没有找到,按照类型注入–jdk提供
-
@Configuration:标记该类为配置文件类
可以替换applicationContext.xml -
@ComponentSacn(“com.ayyy”)
相当于:<context:component-scan base-package=“com.ayyy”> -
@Import:引入其他配置文件
-
@Bean–通过方法创建对象,一般用于创建别人提供的类
-
@Scope(“singleton|prototype”)
配置对象的范围:相当于:bean标签中的属性
scope=“singleton|protptype” -
声明周期
@PostConstruct:相当于bean标签的属性,init-method,指定初始化方法
@PreDestroy:相当于bean标签的属性,destroy-method,指定对象的销毁方法 -
@Value给属性赋值–只能赋值简单类型
PropertySource:引入外部属性文件
相当于:<context:property-placeholder location=“classpath:jdbc.properties”></context:property-placeholder>
Spring核心AOP
什么是AOP
AOP:全程是Aspect Oriented Programming即:面向切面编程
简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强
AOP的作用及优势
作用:在程序运行期间,不修改源码对已有方法进行增强
优势:减少重复代码,提高开发效率,维护方便
AOP的实现
使用动态代理
动态代理的特点
字节码随用随创建,随用随加载
它与静态代理的区别也在于此,因为静态代理是字节码一上来就创建好,并完成加载,装饰者模式就是静态代理的一种体现
动态代理常用的有两种方式
基于接口的动态代理
提供者:JDK官方的Proxy类
要求:被代理类最少实现一个接口
基于子类的动态代理
提供者:第三方的CGLIB,如果报asmxxxx异常,需要导入asm.jar
要求:被代理类不能用final修饰的类
JDK官方的Proxy类创建代理对象
public interface IProxyProducer {
/**
* 销售商品
* @param money
*/
public void saleProduct(Float money);
/**
* 售后服务
* @param money
*/
public void afterService(Float money) ;
}
/**
* 一个生产厂家
*/
public class Producer implements IProxyProducer{
/**
* 销售商品
* @param money
*/
public void saleProduct(Float money) {
System.out.println("销售商品,金额是:"+money);
}
/**
* 售后服务
* @param money
*/
public void afterService(Float money) {
System.out.println("提供售后服务,金额是:"+money);
}
}
public class Consumer {
public static void main(String[] args) {
Producer producer = new Producer();
// producer.saleProduct(5000f);
// producer.afterService(1000f);
/**
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 分类:基于接口的动态代理,基于子类的动态代理
* 作用:不修改源码的基础上对方法增强
* 基于接口的动态代理:
* 提供者是:JDK 官方
* 使用要求:被代理类最少实现一个接口。
* 涉及的类:Proxy
* 创建代理对象的方法:newProxyInstance
* 方法的参数:
* ClassLoader:类加载器。用于加载代理对象的字节码的。和被代理对象使用相同的类加载器。固定写法。
* Class[]:字节码数组。用于给代理对象提供方法。和被代理对象具有相同的方法。和被代理对象实现相同的接口,就会具有相同的方法。固定写法
* InvocationHanlder:要增强的方法。此处是一个接口,我们需要提供它的实现 类。通常写的是匿名内部类。
* 增强的代码谁用谁写。
* */
IProxyProducer proxyProducer = (IProxyProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {
/**
* 执行被代理对象的任何方法都都会经过该方法,该方法有拦截的作用
* 参数的含义
* Object proxy:代理对象的引用。一般不用
* Method method:当前执行的方法
* Object[] args:当前方法所需的参数
* 返回值的含义
* 和被代理对象的方法有相同的返回值
* */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
//1.获取当前执行方法的钱
Float money = (Float)args[0];
//2.判断当前方法是销售还是售后
if("saleProduct".equals(method.getName())) {
//销售
rtValue = method.invoke(producer, money*0.75f);
}
if("afterService".equals(method.getName())) {
//售后
rtValue = method.invoke(producer, money*0.9f);
}
return rtValue;
}
});
proxyProducer.saleProduct(8000f);
proxyProducer.afterService(1000f);
}
}
使用CGLIB的Enhancer类创建代理对象
还是那个演员的例子,只不过不让他实现接口。
/**
* 一个生产厂家
*/
public class Producer{
/**
* 销售商品
* @param money
*/
public void saleProduct(Float money) {
System.out.println("销售商品,金额是 cglib:"+money);
}
/**
* 售后服务
* @param money
*/
public void afterService(Float money) {
System.out.println("提供售后服务,金额是 cglib:"+money);
}
}
/**
* 一个消费者
*/
public class Consumer {
public static void main(String[] args) {
Producer producer = new Producer();
/**
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 分类:基于接口的动态代理,基于子类的动态代理
* 作用:不修改源码的基础上对方法增强
* 基于子类的动态代理
* 提供者是:第三方 cglib 包,在使用时需要先导包(maven 工程导入坐标即可)
* 使用要求:被代理类不能是最终类,不能被 final 修饰
* 涉及的类:Enhancer
* 创建代理对象的方法:create
* 方法的参数:
* Class:字节码。被代理对象的字节码。可以创建被代理对象的子类,还可以获取 被代理对象的类加载器。
* Callback:增强的代码。谁用谁写。通常都是写一个接口的实现类或者匿名内部 类。
* 我们在使用时一般都是使用 Callback 接口的子接口: MethodInterceptor
*/
Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过该方法
* 方法的参数:
* 前 3 个和基于接口中方法的参数含义一样。
* MethodProxy methodProxy:当前执行方法的代理对象。
* 方法的返回值:
* 和被代理对象中方法有相同的返回值
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object rtValue = null;
//1.获取当前执行方法的钱
Float money = (Float)args[0];
//2.判断当前方法是销售还是售后
if("saleProduct".equals(method.getName())) {
//销售
rtValue = method.invoke(producer, money*0.5f);
}
if("afterService".equals(method.getName())) {
//售后
rtValue = method.invoke(producer, money*0.8f);
}
return rtValue;
}
});
cglibProducer.saleProduct(8000f);
cglibProducer.afterService(1000f);
}
}
AOP
-
Joinpoint(连接点):
连接点就是指哪些被拦截到的点.在spring中,这些点指的是方法,因为spring只支持方法类型的连接点 -
Pointcut(切入点):
切入点是指我们要对哪些Joinpoint进行拦截的定义 -
Advice(通知/增强)
通知是指拦截到Joinpoint之后所要做的事情就是通知
通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知 -
Introduction(引介)
引介是一种特殊的通知在不修改类代码的前提下,Introduction可以在运行期为类动态的添加一些方法 -
Target(目标对象)
代理的目标对象 -
Weaving(织入)
是指把增强应用到目标对象来创建新的代理对象的过程
spring采用动态代理织入,而AspectJ采用编译器织入和类装载期织入 -
Proxy(代理)
一个类被AOP织入增强后,就产生一个结果代理类 -
Aspect(切面)
是切入点和通知(引介)的结合
AOP的xml配置
- 依赖
<!--引入spring的核心-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!--引入spring的测试包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!--引入单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!--配置aop,必须引入一个包:版本必须要1.8.7以上-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
- 配置文件
<!--
aop 的配置步骤: 第一步:把通知类的创建也交给 spring 来管理
第二步:使用 aop:config 标签开始 aop 的配置
第三步:使用 aop:aspect 标签开始配置切面,写在 aop:config 标签内部 id 属性:给切面提供一个唯一标识 ref 属性:用于引用通知 bean 的 id。
第四步:使用对应的标签在 aop:aspect 标签内部配置通知的类型 使用 aop:befored 标签配置前置通知,写在 aop:aspect 标签内部 method 属性:用于指定通知类中哪个方法是前置通知 pointcut 属性:用于指定切入点表达式。
切入点表达式写法: 关键字:execution(表达式) 表达式内容: 全匹配标准写法: 访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表) 例如: public void com.ayyy.service.impl.AccountServiceImpl.saveAccount() -->
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--扫描表,创建bean对象-->
<context:component-scan base-package="com.ayyy"></context:component-scan>
<!--通知对象: 拦截到方法时,通知执行的对象-->
<!--
通知的类型
前置通知: 方法之前执行
后置通知: 方法执行完之后执行- 返回之前执行-如果有异常,则不执行
最终通知: 方法执行完后总会执行- finally
异常通知: 方法出现异常则执行
环绕通知: 前置通知+后置通知+最终通知+异常通知
-->
<bean id="logger" class="com.ayyy.log.Logger"></bean>
<!--配置aop-->
<aop:config>
<!--配置切面= 切入点 + 通知
指定通知对象是谁
-->
<aop:aspect ref="logger">
<!--配置切入点
id:唯一的标志
expression: 表达式
* com.ayyy.service.impl.*.*(..)
* com.ayyy.service..*.*(..)
第一个*:代表方法任意返回值类型
第二个*: 类名任意,impl包中所有的类
第三个*: 任意方法名
(..) : 参数任意,个数任意,类型任意,顺序任意
其他的配置方式
public void com.ayyy.service.impl.UserServiceImpl.findAll()
void com.ayyy.service.impl.UserServiceImpl.findAll()
* com.ayyy.service.impl.UserServiceImpl.findAll()
* com.ayyy.service..UserServiceImpl.findAll() : .. 代表的是包,及其子包
-->
<aop:pointcut id="pointcut" expression="execution(* com.ayyy.service.impl.*.*(..))"></aop:pointcut>
<!--织入: 告诉通知对象执行,具体执行哪一个方法-->
<!--前置通知-->
<!--<aop:before method="before" pointcut-ref="pointcut"></aop:before>-->
<!--后置通知-->
<!--<aop:after-returning method="afterReturning" pointcut-ref="pointcut"></aop:after-returning>-->
<!--最终通知-->
<!--<aop:after method="after" pointcut-ref="pointcut"></aop:after>-->
<!--异常通知-->
<!--<aop:after-throwing throwing="e" method="afterThrowing" pointcut-ref="pointcut"></aop:after-throwing>-->
/**
* 环绕通知
* 问题:
* 当配置完环绕通知之后,没有业务层方法执行(切入点方法执行)
* 分析:
* 通过动态代理的代码分析,我们现在的环绕通知没有明确的切入点方法调用
* 解决:
* spring 框架为我们提供了一个接口,该接口可以作为环绕通知的方法参数来使用
* ProceedingJoinPoint。当环绕通知执行时,spring 框架会为我们注入该接口的实现类。
*它有一个方法 proceed(),就相当于 invoke,明确的业务层方法调用
*spring 的环绕通知:
*它是 spring 为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
*/
<!--环绕增强-->
<aop:around method="around" pointcut-ref="pointcut"></aop:around>
</aop:aspect>
</aop:config>
</beans>
AOP的注解配置
package com.ayyy.log;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
*@Component: 创建类对象
* @Aspect:配置该类为切面
* 切面是:切入点 + 通知
*/
@Component
@Aspect
public class Logger {
/**
* 配置切入点
*/
@Pointcut("execution(* com.ayyy.service.impl.*.*(..))")
public void pointcut(){};
/**
*
* @param joinPoint 连接点-- 拦截到的方法
*/
// @Before("pointcut()")
public void before(JoinPoint joinPoint){
//被代理的对象
Object target = joinPoint.getTarget();
//拦截的类的名称
String className = target.getClass().getName();
System.out.println("拦截到的类名:" +className);
//获取方法对象
Signature signature = joinPoint.getSignature();
//获取方法名
String methodName = signature.getName();
System.out.println("拦截到方法名:" + methodName);
System.out.println("前置通知");
}
// @AfterReturning("pointcut()")
public void afterReturning(){
System.out.println("后置增强");
}
// @After("pointcut()")
public void after(){
System.out.println("最终增强");
}
// @AfterThrowing(value = "pointcut()",throwing = "e")
public void afterThrowing(Exception e){
System.out.println("执行的方法的异常:"+e);
System.out.println("异常通知");
}
/**
* ProceedingJoinPoint 可以执行拦截到方法的连接点对象
* @param joinPoint
*/
@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint){
try {
System.out.println("前置通知");
//执行原来的方法: 可以获取方法的返回值
Object result = joinPoint.proceed();
System.out.println("后置增强");
} catch (Throwable e) {
System.out.println("异常通知");
// e.printStackTrace();
} finally {
System.out.println("最终增强");
}
}
}