Spring Framework 学习
- 1. Spring 核心概念
-
- 2. 入门案例
-
- 3. IOC 基本内容
- 3.1 bean 基础配置
- 3.2 bean 实例化
- 4. DI 依赖注入
- 4.1 setter 注入
- 4.2 构造器注入
- 4.3 依赖自动装配
- 4.4 集合注入
- 4.4.1 数组
- 4.4.2 List
- 4.4.3 Set
- 4.4.4 Map
- 4.4.5 Properties 类型
- 5. IOC/DI 配置管理第三方 bean
-
- 6. 核心容器
- 7. IOC/DI注解开发
- 7.1 注解开发定义 bean
- 7.2 配置文件的注解
- 7.3 注解开发依赖注入
- 7.4 细节
- 8. Spring 结合 MyBatis
- 9 AOP
- 9.1 快速入门
- 9.2 AOP 配置方式
- 9.2. 1 切入点表达式
- 9.2.2 AOP 通知类型
- 9.2.3 通知中获取参数
- 9.3. AOP 事务管理
-
- n 总出错
1. Spring 核心概念
Spring 官网:https://spring.io
1.1 IOC、IOC容器、Bean、DI
- IOC(Inversion of Control)控制反转
- 使用对象时,由主动new产生对象转换为由外部提供对象,此过程中对象创建控制权由程序转移到外部,此思想称为控制反转。
- Spring技术对IOC思想进行了实现
- Spring提供了一个容器,称为IOC容器,用来充当IOC思想中的"外部"
- 被创建或被管理的对象在IOC容器中统称为Bean
- DI(Dependency Injection)依赖注入
- 在容器中建立bean与bean之间的依赖关系的整个过程,称为依赖注入(eg: 如业务层需要依赖数据层,service就要和dao建立依赖关系)
小结: 这两个概念的最终目标就是:充分解耦
- 最终结果为:使用对象时不仅可以直接从IOC容器中获取,并且获取到的bean已经绑定了所有的依赖关系.
2. 入门案例
2.1 IOC入门案例
- 创建Maven的java项目
- pom.xml添加Spring的依赖jar包
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
- 创建BookService,BookServiceImpl,BookDao和BookDaoImpl四个类
public interface BookDao {
public void save();
}
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
}
public interface BookService {
public void save();
}
public class BookServiceImpl implements BookService {
private BookDao bookDao = new BookDaoImpl();
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
- resources下添加spring配置文件 applicationContext.xml,并完成bean的配置
注意:此时可能没有Spring Config,可能是因为没有右击模块,然后 Add Framework Support,点开后 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.xsd">
<bean id="bookDao" class="com.qxd.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.qxd.service.impl.BookServiceImpl"/>
</beans>
- 使用Spring提供的接口完成IOC容器的创建
- 从容器中获取对象进行方法调用
public class app2 {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bookDao = (BookDao)ctx.getBean("bookDao");
bookDao.save();
System.out.println("==============");
BookService bookService = (BookService) ctx.getBean("bookService");
bookService.save();
}
}
2.2 DI 入门案例
- 去除代码中的new
- 为属性提供setter方法
public class BookServiceImpl implements BookService {
private BookDao bookDao;
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
public void setBookDao (BookDao bookDao) {
this.bookDao = bookDao;
}
}
- 修改配置 ( applicationContext.xml ) 完成注入
<bean id="bookDao" class="com.qxd.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.qxd.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>
</beans>
- 如果一个地址 bean 了两个,id名不同,这会是两个不同的实例,一个 bean 一个(或者多个)实例。
- bookService 里面的 bookDao 和他引用的那个 bookDao 是同一个实例
3. IOC 基本内容
3.1 bean 基础配置
- 别名配置
<bean id="bookService" name="service service4 bookEbi" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>
<bean id="bookDao" name="dao" class="com.itheima.dao.impl.BookDaoImpl" scope="prototype"/>
</beans>
3.2 bean 实例化
Spring 创建 bean 时底层使用的是类的无参构造方法。
Spring的IOC实例化对象的三种方式分别是:
- 构造方法就是上面介绍过的
- 下面写一个 FactoryBean的例子
public class BookDaoFactoryBean implements FactoryBean {
public BookDao getObject() throws Exception {
return new BookDaoImpl();
}
public Class<?> getObjectType() {
return BookDao.class;
}
public boolean isSingleton() {
return true;
}
}
<bean id="bookDao2" class="com.qxd.factory.BookDaoFactoryBean"/>
4. DI 依赖注入
Spring为我们提供了两种注入方式,分别是:
4.1 setter 注入
前面已经讲过了怎么将 bookDao 注入到 bookService,如果需要注入属性,只需要把 property 中的 ref 换成 value
eg:
private int connectionNumber;
<property name="connectionNumber" value="10"/>
4.2 构造器注入
4.3 依赖自动装配
- 按照类型
只要提供了 setter 方法,配置这边只需要加上 autowire 即可。 - 按名称
有的时候一个地址 bean 了多个,按照类型就找不到是匹配哪一个 bean 了,此时就需要按照名称的方法
- 配置特征:
- 自动装配用于引用类型依赖注入,不能对简单类型(属性值这些)进行操作
- 使用按类型装配时(byType)必须保障容器中相同类型的bean唯一,推荐使用
- 使用按名称装配时(byName)必须保障容器中具有指定名称的bean,因变量名与配置耦合,不推荐使用
- 自动装配优先级低于setter注入与构造器注入,同时出现时自动装配配置失效
4.4 集合注入
public class BookDaoImpl implements BookDao {
private int[] array;
private List<String> list;
private Set<String> set;
private Map<String,String> map;
private Properties properties;
4.4.1 数组
<bean ...>
<property name="array">
<array>
<value>100</value>
<value>200</value>
<value>300</value>
</array>
</property>
</bean>
4.4.2 List
<property name="list">
<list>
<value>itcast</value>
<value>itheima</value>
<value>boxuegu</value>
<value>chuanzhihui</value>
</list>
</property>
4.4.3 Set
<property name="set">
<set>
<value>itcast</value>
<value>itheima</value>
<value>boxuegu</value>
<value>boxuegu</value>
</set>
</property>
4.4.4 Map
<property name="map">
<map>
<entry key="country" value="china"/>
<entry key="province" value="henan"/>
<entry key="city" value="kaifeng"/>
</map>
</property>
4.4.5 Properties 类型
<property name="properties">
<props>
<prop key="country">china</prop>
<prop key="province">henan</prop>
<prop key="city">kaifeng</prop>
</props>
</property>
5. IOC/DI 配置管理第三方 bean
5.1 加载 properties 文件
- 准备properties配置文件
resources下创建一个jdbc.properties文件,并添加对应的属性键值对
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db
jdbc.username=root
jdbc.password=qxd
- 开启context命名空间
在applicationContext.xml中开context命名空间
<?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>
- 加载properties配置文件
在配置文件中使用context命名空间下的标签来加载properties配置文件
<context:property-placeholder location="jdbc.properties"/>
- 完成属性注入
使用${key}来读取properties配置文件中的内容并完成属性注入
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
注意:
1. 不加载系统属性
<context:property-placeholder location="jdbc.properties“ system-properties-mode="NEVER"/>
2. 加载多个 properties 文件
<context:property-placeholder location="jdbc.properties,jdbc2.properties"/>
3. 加载所有 properties 文件
<context:property-placeholder location="*.properties"/>
4. 加载 properties 的标准格式
<context:property-placeholder location="classpath:*.properties" system-properties-mode="NEVER"/>
5. 从类路径或 jar 包种搜索并加载 properties 文件
<context:property-placeholder location="classpath*:*.properties" />
最后长这个样子哦
6. 核心容器
- 容器创建的两种方式
- ClassPathXmlApplicationContext[掌握]
- FileSystemXmlApplicationContext[知道即可]
- 容器类层次结构
- 只需要知晓容器的最上级的父接口为 BeanFactory即可
- 获取Bean的三种方式
- getBean(“名称”):需要类型转换
- getBean(“名称”,类型.class):多了一个参数
- getBean(类型.class):容器中不能有多个该类的bean对象 (不然此时不知道找哪一个)
- BeanFactory
- 使用BeanFactory创建的容器是延迟加载(了解)
- 使用ApplicationContext创建的容器是立即加载
7. IOC/DI注解开发
7.1 注解开发定义 bean
@Component("bookDao")
public class BookDaoImpl implements BookDao {
}
@Component
public class BookServiceImpl implements BookService {
}
<context:component-scan base-package="com.qxd"/>
7.2 配置文件的注解
步骤
-
- Java类替换Spring核心配置文件
-
- @Configuration注解用于设定当前类为配置类
-
- @ComponentScan注解用于设定扫描路径,此注解只能添加一次,多个数据请用数组格式
@ComponentScan({“com.itheima.service”,“com.itheima.dao”})
-
- 读取Spring核心配置文件初始化容器对象切换为读取Java配置类初始化容器对象
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
7.3 注解开发依赖注入
- 将 @Autowired写在属性上方(此时不需要 setter 方法)
- 若 BookDao 接口如果有多个实现类,此时可能会找不到它注入的是哪一个,解决方案如下:(通常不需要用到)
- (1)此时@Component注解(或者另外三个)后面带名称,这样就会找到和 bookDao 名称一样的那个 bean 了
- (2)如果不叫作 bookDao,则可以加一个按名称查找的注解 @Qualifier(“bookDao1”) ,如下:
- 简单数据类型注入 @Value
@Value一般会被用在从properties配置文件中读取内容进行使用,详细看 细节第四点
7.4 细节
- 对于@Component注解,还衍生出了其他三个注解@Controller、@Service、@Repository
这三个注解和@Component注解的作用是一样的,为什么要衍生出这三个呢?
方便我们后期在编写类的时候能很好的区分出这个类是属于表现层、业务层还是数据层的类。 - 要想将BookDaoImpl变成非单例,只需要在其类上添加@scope注解
- 生命周期的注解 (在BookDaoImpl中添加两个方法,init和destroy ,方法名可以任意)
只需要在对应的方法上添加@PostConstruct和@PreDestroy注解即可
注意: destroy只有在容器关闭的时候,才会执行,所以如下(非单例也不会执行)
如果@PostConstruct和另个标红,加入以下依赖即可(@PostConstruct和@PreDestroy注解位于 java.xml.ws.annotation包是Java EE的模块的一部分。J2EE已经在Java 9中被弃用,并且计划在Java 11中删除它。)
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
4. @Value一般会被用在从properties配置文件中读取内容进行使用
- 步骤1:resource下准备 properties 文件 jdbc.properties (自己命名)
name=itheima888
- 步骤2: 使用注解加载properties配置文件,在配置类上添加 @PropertySource 注解,如果有多个则 @PropertySource({“qxd1.properties”,"qxd2.properties "}) (注意: 这里不可以用通配符 * )
- 步骤3:使用@Value读取配置文件中的内容
- 注解开发管理第三方bean
可以在配置文件下写一个 @bean的代码,但是都放在一起不好管理,所以如下:
(1)我们新建一个JdbcConfig配置类,并把数据源配置到该类下。(扩展:下面那些String可以用细节4的配置文件搞定)
(2)在Spring的配置类上添加包扫描 @ComponentScan(“com.itheima.config”)
注意: 和那些 bean扫描一个单词
JdbcConfig类要放入到com.itheima.config包下,需要被Spring的配置类扫描到即可)
- 方案2
优点:此方案可以不用加@Configuration注解,但是必须在Spring配置类上使用@Import注解手动引入需要加载的配置类
(1)步骤1:去除JdbcConfig类上的 @Configuration 注解
(2)在 Spring 配置类中引入 @Import (此时对于该配置类的扫描可以不要)
- 注解小结
8. Spring 结合 MyBatis
- pom.xml (其实就在 上面学的 Spring + 之前学的 MyBatis 配置基础上 加了最后两个配置)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>spring_myBatis</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
</project>
- 将原来的 mybatis-config.xml 改成第三步,此时这个配置文件可以删除 (注意:2 框上有个 type,后面事务处理会学习)
- 改之后:
9 AOP
AOP是在不改原有代码的前提下对其进行增强。
可以被增强的方法叫做连接点,想要增强的方法叫做切入点,通知就是切入点执行的操作,通知类就是定义通知的类,切面是描述通知与切入点的对应关系。(切入点就是被通知追加功能的连接点)
9.1 快速入门
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
context 里面包含了 AOP 的,所以不用单独添加的
-
-
- 代码
@Configuration
@ComponentScan("com.qxd")
@EnableAspectJAutoProxy
public class SpringConfig {
}
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save ...");
}
public void update() {
System.out.println("book dao update ...");
}
}
public interface BookDao {
void save();
void update();
}
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new
AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.qxd.dao.BookDao.update())")
private void pt() {}
@Before("pt()")
public void MyMethod() {
System.out.println("hi...");
}
}
SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知 [如:MyAdvice中的MyMethod方法] 内容加进去,就实现了增强,这就是我们所说的代理 (Proxy)。
9.2 AOP 配置方式
9.2. 1 切入点表达式
9.2.1.1 语法格式
切入点:要进行增强的方法
切入点表达式:要进行增强的方法的描述方式
-
切入点的描述
-
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
execution(public User com.itheima.service.UserService.findById(int))
-
- execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
- public:访问修饰符,还可以是public,private等,可以省略
- User:返回值,写返回值类型
- com.itheima.service:包名,多级包使用点连接
- UserService:类/接口名称
- findById:方法名
- int:参数,直接写参数的类型,多个类型用逗号隔开
- 异常名:方法定义中抛出指定异常,可以省略
切入点表达式就是要找到需要增强的方法,所以它就是对一个具体方法的描述,但是方法的定义会有很多,所以如果每一个方法对应一个切入点表达式,想想这块就会觉得将来编写起来会比较麻烦,所以学到了下面的 通配符。
9.2.1.2 通配符
- * :单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
方法
execution(public * com.itheima.*.UserService.find*(*))
- ..多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
execution(public User com..UserService.findById(..)) 1
- +:专用于匹配子类类型(很少用)
execution(* *..*Service+.*(..))
9.2.1.3 书写技巧
- 所有代码按照标准规范开发,否则以下技巧全部失效
- 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
- 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
- 包名 书写 尽量不使用… 匹配,效率过低,常用*做单个包描述匹配,或精准匹配
- 接口名/类名书写名称与模块相关的采用 * 匹配,例如UserService书写成*Service,绑定业务层接口名
- 方法名书写以动词进行精准匹配,名词采用匹配,例如getById书写成getBy,selectAll书写成selectAll
- 参数规则较为复杂,根据业务方法灵活调整
- 通常不使用异常作为匹配规则
9.2.2 AOP 通知类型
- 前置通知
- 后置通知
- 返回后通知(了解)
- 抛出异常后通知(了解)
- 环绕通知(重点)(这个得加上ProceedingJoinPoint 和 抛出异常,不然电脑咋知道你怎么去环绕咧~)
@Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("around before advice ...");
pjp.proceed();
System.out.println("around after advice ...");
}
注意:如果原函数含有参数,那么环绕的这个函数也得有参数并返回,例子如下:
public int select() {
System.out.println("book dao select is running ...");
return 100;
}
@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 ...");
}
环绕通知更多注意:
- 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法
调用前后同时添加通知 - 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
- 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
- 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成
Object - 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常
9.2.3 通知中获取参数
- 获取切入点方法的参数,所有的通知类型都可以获取参数
- JoinPoint:适用于前置、后置、返回后、抛出异常后通知
- ProceedingJoinPoint:适用于环绕通知
- 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
- 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
9.3. AOP 事务管理
- 事务作用:在数据层保障一系列的数据库操作同成功同失败
- Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败
eg:不仅加了注解还有
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
9.3.1 事务配置
- 上面这些属性都可以在@Transactional注解的参数上进行设置。
- readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true.
- timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间。
- rollbackFor:当出现指定异常进行事务回滚
- noRollbackFor:当出现指定异常不进行事务回滚 (并不是所有的异常都会回滚事务)
- rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串
- noRollbackForClassName等同于noRollbackFor,只不过属性为异常的类全名字符串
- isolation设置事务的隔离级别\
- DEFAULT :默认隔离级别, 会采用数据库的隔离级别
- READ_UNCOMMITTED : 读未提交
- READ_COMMITTED : 读已提交
- REPEATABLE_READ : 重复读取
- SERIALIZABLE: 串行化
- propagation: 事务传播行为:事务协调员对事务管理员所携带事务的处理态度。
eg: 下面这样就是开了新的事物,其他的不成功这个成功了也会保存数据
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
事务传播行为的可选值:
对于我们开发实际中使用的话,因为默认值需要事务是常态的。根据开发过程选择其他的就可以了,例如案例中需要新事务就需要手工配置。其实入账和出账操作上也有事务,采用的就是默认值。
n 总出错
- 老说我语言有问题,加上这个把它定死
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)