Spring Boot 实践

Stella981
• 阅读 792

#### 一、Spring Boot重要特性

  1. 独立的Spring应用程序,嵌入式Tomcat/Jetty容器,无需部署War包
  2. 尽可能使用自动化配置,Spring Auto Configuration
  3. 提供一批'starter' POM 简化Maven及Gradle配置
  4. 提供一系列可以用到生产环境的应用度量、健康检查等特性(Actuator)

二、Spring Boot 快速上手

访问http://start.spring.io/,使用SPRING INITIALIZR选择需要的模块快速初始化

![spring start](images/start.spring.io.jpg "start.spring.io")

选择Web模块,快速创建项目

```xml

4.0.0

<groupId>com.example</groupId>
<artifactId>myproject</artifactId>
<version>0.0.1-SNAPSHOT</version>

<!-- Inherit defaults from Spring Boot -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.3.RELEASE</version>
</parent>

<!-- Add typical dependencies for a web application -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>x
</dependencies>

<!-- Package as an executable jar -->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

``` 生成的项目结构

```

myproject +- pom.xml +- src +- main +- java | +- com.example.myproject | +- Application.Java |
+- resources | +- application.properties | +- test +- java | +- com.example.myproject | +- ApplicationTests.java

```

三、项目分层结构及模块划分方式

![项目分层](images/springbootlayer.jpg "项目分层")

1. 名称规范
  • **包名规范**

``` com.pingan.haofang.${产品}.${模块}.${层次}.${className}.java ```

  • **特殊类名规范**

Spring配置类: configuration/*Configuration,例如WebConfiguration/DatasourceConfiguration

Properties类: properties/*Properties,例如FtpProperties

dao类:dao/*Dao或者dao/*Repository

service类:service/*Service

  • ** 分层命名规范 **

domain: 数据库PO

dao/repository:数据库访问层

service:业务逻辑层

dto:数据传输对象

constants:枚举常量

controller:web控制器

form:web请求对象

vo:web响应对象

validator:校验器

batch:批处理Job类

2. 项目划分

对于每个项目可以按照如下方式进行划分为4个模块,独立为4个maven模块

  • **Parent**

负责依赖管理,公用的maven依赖

  • **Service (lib)**

包含整个项目的业务逻辑/数据访问代码

```

com.pingan.haofang +- myproject +- customer +- domain | +- Customer.java |
+- dao | +- CustomerDao.java | +- service | +- CustomerService.java | +- impl | +- CustomerServiceImpl.java | +- dto | +- CustomerDto.java | +- constants | +- CustomerConstants.java | +- CustomerStatus.java

```

  • **Exportapi (lib 或 app)**

项目对外API提供,RPC等。依赖Service,可单独部署也可打进web包进行部署。

  • **Web (app)**

项目web接口暴露代码,包括前后端接口暴露,文档,拦截器。依赖service、exportapi

``` com.pingan.haofang +- myproject +- WebApplication.java +- customer +- controller | +- CustomerController.java |
+- form | +- CustomerForm.java +- vo | +- CustomerVo.java | +- validator | +- CustomerValidator.java

```

  • **Batch (app)**

项目批处理任务,常驻进程任务或者定时任务,依赖service

``` com.pingan.haofang +- myproject +- BatchApplication.java +- customer +- batch | +- CustomerExportTask.java | +- CustomerExportRunner.java

```

四、Spring常用模块应用

1. Spring MVC
  • **接口定义规范**

``` Http Method

GET:读取数据,不允许有数据的修改等操作 列表URL设计:GET:/web/custmer 单条数据URL设计:GET:/web/customer/{custmerId} POST:新建数据 POST:/web/custmer PUT:修改数据 PUT:/web/custmer/{custormId} PUT:/web/custmer/status/{custormId} DELTE:删除数据 DELETE:/web/custmer/{custormId}

请求体与响应体

请求与响应除QueryString及PathVariable外,其余数据交互应以Json格式进行交互

Controller配置为@RestController, 前后端ContentType:application/json; charset=UTF8

```

  • **Swagger应用**

所有controller都用swagger annotation进行注解,springfox嵌入以提供接口文档及try out调试功能

  • **TraceFilter**

添加TraceFilter,对于每个请求随机生成RequestID并放入MDC进行日志打印,便于排查

  • **异常消息定义及ExceptionHandler**

自定义完善的异常处理器,按照和前端定义好的接口产生异常消息体。通过HTTP CODE定义各类状态

``` 200 成功 409 校验失败,例如非空、长度、格式等 400 客户端请求格式错误,例如不是合法的Json 401 未授权即未登录 403 无权限访问 404 不存在,未找到响应对象 500 服务器内部错误

```

异常消息体定义

```json { "errorCode": 1, // 保留错误码字段 "message": "全局异常消息", "fieldErrors": [ { "name": "名称不允许重复" }, { "desc": "描述太短" } ] }

```

2. JPA用法
  • **数据源及数据库连接池配置**

建议使用Alibaba Druid连接池配置,见 com.pingan.haofang.myproject.common.configuration.DataSourceConfiguration,

同时建议配置DruidStat,即可通过web管理数据源监测数据,见

```java

@Bean
public ServletRegistrationBean druidServlet(DruidStatProperties druidStatProperties) {
    ServletRegistrationBean reg = new ServletRegistrationBean();
    reg.setServlet(new StatViewServlet());
    reg.addUrlMappings("/druid/\*");
    reg.addInitParameter("loginUsername", druidStatProperties.getUsername());
    reg.addInitParameter("loginPassword", druidStatProperties.getPassword());
    return reg;
}

```

  • **继承Repository接口**

    查询可以使用QueryMethod/@Query/Example/Specification四种方式,前面三种适合做简单查询时用,Specification(即CriteriaQuery)建议在复杂查询,例如分业列表有较多筛选条件时使用

    demo见com.pingan.haofang.myproject.customer.service.impl#CustomerServiceImpl#queryList,CustomerSpecs.pageListSpec(dto)

  • **合理使用实体关联**

  • **使用Auditing**

    Auditing提供了如下四个annotation可以方便设置创建人、最后修改人、创建时间、最后修改时间。需要和@EnableJpaAuditing,AuditingEntityListener配合使用

    @CreatedBy,@LastModifiedBy,@CreatedDate,@LastModifiedDate

```java

@Bean public AuditorAware auditorProvider() { return () -> Optional.ofNullable(SearchThreadContext.getSessionVisitor()) .map(Visitor::getUserId) .orElse(0L); }

@MappedSuperclass public abstract class BaseDomain {

@Column(name = "create\_time")
@CreatedDate
private Date createTime;

@Column(name = "create\_by")
@CreatedBy
private Long createBy;

@Column(name = "update\_time")
@LastModifiedDate
private Date updateTime;

@Column(name = "update\_by")
@LastModifiedBy
private Long updateBy;

```

3. 使用Spring Boot Actuator/Spring Boot Admin

Spring Boot actuator可以帮助我们提供方便的健康页面、jmx等监控和排查功能。故期望所有项目的Actuator满足如下规范。

  • **Spring Boot Actuator的Context-path为/actuator**

context-path配置为统一前缀,方便未来配置内部管理域名时proxy的统一配置。

```properties

endpoints.sensitive=false endpoints.enabled=true endpoints.actuator.enabled=true endpoints.shutdown.enabled=true endpoints.shutdown.sensitive=false

management.security.enabled=false management.context-path=/actuator management.address=127.0.0.1

```

```xml

org.springframework.boot spring-boot-starter-actuator

```

  • **为便于管理和排查Spring Boot App,每个APP都配置spring boot admin**

引入Jar

```xml

de.codecentric spring-boot-admin-starter-client 1.5.0 org.springframework.boot spring-boot-maven-plugin build-info
    <!--如下插件生成git信息,包括构建的git分支,最后提交人及注释,版本号-->
    <plugin>
        <groupId>pl.project13.maven</groupId>
        <artifactId>git-commit-id-plugin</artifactId>
        <executions>
            <execution>
                <goals>
                    <goal>revision</goal>
                </goals>
            </execution>
        </executions>
        <configuration>
            <!--日期格式;默认值:dd.MM.yyyy '@' HH:mm:ss z;-->
            <dateFormat>yyyyMMddHHmmss</dateFormat>
            <!--,构建过程中,是否打印详细信息;默认值:false;-->
            <verbose>true</verbose>
            <!-- ".git"文件路径;默认值:${project.basedir}/.git; -->
            <dotGitDirectory>${project.basedir}/.git</dotGitDirectory>
            <!--若项目打包类型为pom,是否取消构建;默认值:true;-->
            <skipPoms>false</skipPoms>
            <!--是否生成"git.properties"文件;默认值:false;-->
            <generateGitPropertiesFile>true</generateGitPropertiesFile>
            <!--指定"git.properties"文件的存放路径(相对于${project.basedir}的一个路径);-->
            <generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties
            </generateGitPropertiesFilename>
            <!--".git"文件夹未找到时,构建是否失败;若设置true,则构建失败;若设置false,则跳过执行该目标;默认值:true;-->
            <failOnNoGitDirectory>true</failOnNoGitDirectory>
        </configuration>
    </plugin>
</plugins>

```

配置注册admin server地址

```properties

#spring admin ##目前st/ci将注册到ci环境的admin,ga将注册到ga环境的admin spring.boot.admin.url=http://actuator.a.pa.com/ ##如果dev及其他开发机可能网络不通,请使用下面配置 #spring.boot.admin.url=http://10.59.72.187:9596

##下面配置是spring-boot的name,配置后才能在admin有漂亮的名称,请自行取名 spring.application.name=${applicationName}

##默认如果hosts中配置了当前IP的hostname可能无法访问,所以可以加上如下设置 spring.boot.admin.client.prefer-ip=true

management.info.git.mode=full

```

  • **访问SpringAdminServer进行管理**

```

st/ci/开发: http://actuator.anhouse.com.cn/ 用户名:admin 密码:admin-st

anhouse http://actuator.an2.ipo.com/

ga: http://actuator.proxy.ipo.com/

```

![spring boot admin ui](images/springbootadminui.png "spring boot admin ui") ![spring boot admin ui](images/springbootadminui-logging.png "spring boot admin ui")

4. 校验

jsr303

functional validation

fail fast/ fail over

5. 单测
  • **内存数据库H2**

请使用内存数据库模拟数据库,初始化脚本请添加SchemaSQL,初始化数据可使用对应的data.sql,或者使用testEntityManager

详见myproject-service/src/test

  • **Mock/MockBean**

需要配置MockitoTestExecutionListener,见BaseTest

当单测测试逻辑类有依赖其他的逻辑类,这个时候如果只想测试自己的逻辑,可以使用@MockBean,mock掉依赖的逻辑类,这里mock的对象如果没有使用mockito指定 相应逻辑,则都会返回null

见com.pingan.haofang.myproject.customer.service.impl.CustomerServiceImplTest#isCustomerBuyProduct

  • **Spy/SpyBean**

需要配置MockitoTestExecutionListener,见BaseTest

与Mock和MockBean不同之处在于,mock的对象如果没有对方法使用mockito指定相应逻辑,则会执行真实代码,但是@Spy中如果先when,再ThenReturn则还是会先执行 一次mock方法的真实逻辑,可能会因为不可预知的错误而失败。

```java

// when去设置模拟返回值时,里面的方法object.callMethod()会先执行一次 when(object.callMethod()).thenReturn("result");

// 使用doReturn则不会产生上面的问题 doReturn("result").when(object).callMethod();

```

更多请参照com.pingan.haofang.myproject.customer.service.impl.CustomerServiceImplTest#getOne

6. Spring Session

@EnableRedisHttpSession

为避免redis共享时出现问题,建议设置redisNamespace区分key, 否则默认都是spring:session,不好区分, 同时按照需要设置session过期时间

@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 43200, redisNamespace = "search_cloud")

``` 配置redisNamespace后的rediskey "spring:session:search_cloud:sessions:5259b7fb-c882-4f57-8d32-d967148b1338"

未配置namespace后的rediskey "spring:session:sessions:expires:c1698de0-618b-455d-a63b-b4809decb1fd"

```

7. ThreadContext

线程上下文,请扩展使用com.pingan.haofang.module.common.ThreadContext

8. 日志规范

日志请使用Spring-Boot默认提供的模板,springboot默认提供的模板已经预定义了变量,可以进行赋值扩展,主要分console-pattern和file-pattern, 两者格式相同,console-pattern还包含颜色美化,便于阅读

如果无额外appender配置,可以直接在application.properties中配置,SpringBoot提供的日志级别自定义 可按照如下配置示例扩展

``` logging.pattern.level=%X{REQUEST_ID} %p

```

但如果日志比较复杂,可使用SpringBoot提供的logback base.xml,defaults.xml进行组合配置,spring boot base提供的fileAppender不支持按时间滚动, 这块可以自己写

```xml

<jmxConfigurator/>
<property name="LOG\_FILE" value="${LOG\_PATH}/myproject.log"/>

<property name="ADDITIONAL" value="%X{REQUEST\_ID} %X{TRACE\_ID}"/>
<property name="LOG\_LEVEL\_PATTERN" value="${ADDITIONAL} %5p"/>

<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

<appender name="FILE"
          class="ch.qos.logback.core.rolling.RollingFileAppender">
    <encoder>
        <pattern>${FILE\_LOG\_PATTERN}</pattern>
    </encoder>
    <file>${LOG\_FILE}</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>${LOG\_FILE}.%d{yyyy-MM-dd}</fileNamePattern>
    </rollingPolicy>
</appender>

<!--customer专用appender-->
<appender name="customerAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG\_PATH}/customer.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>${LOG\_PATH}/customer-%d{yyyy-MM-dd}.log</fileNamePattern>
    </rollingPolicy>
    <encoder>
        <pattern>${ADDITIONAL} %d{HH:mm:ss.SSS} - %msg%n</pattern>
    </encoder>
</appender>

<logger name="com.pingan.haofang.myproject.customer.controller.CustomerController" level="INFO" additivity="false">
    <appender-ref ref="customerAppender"/>
</logger>

<root level="INFO">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="FILE"/>
</root>

```

日志文件路径指定,对于主程序日志,请通过配置文件或者Jvm参数指定logging.path和logging.file

五、好房Spring Boot Starter

基于各种开发场景,好房framework模块开发了若干开发组件,例如历史操作记录,批处理框架,校验工具等。需要使用请先引入如下pom

```xml

com.pingan.haofang.framework pinganfang-framework-dependencies 1.0.0-SNAPSHOT pom import

```

1. pinganfang-common-module,通用工具模块

该模块主要封装了各种常用UTIL类库,Exception定义,例如StringUtils,ThreadContext,BaseException等。

```mvn

com.pingan.haofang.framework pinganfang-common-module

```

2. pinganfang-rpc-starter, RPC封装

该模块封装了好房各业务模块通信的RPC,包括服务端开放RPC服务,以及RPC客户端调用。

```mvn

com.pingan.haofang.framework pinganfang-rpc-starter

```

要启用RPC,请在Application.java,或者配置类上面添加@EnableHaofangRPC,并定义如下Filter

```java

@Bean @Order(Ordered.HIGHEST_PRECEDENCE) public FilterRegistrationBean rpcFilter() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(new RPCFilter()); filterRegistrationBean.addUrlPatterns("/rpc"); filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); return filterRegistrationBean; }

```

**声明RPC服务**,详见com.pingan.haofang.myproject.demo.rpc.DemoRPCExportService

```java

@RPCExporter(value = "findByIds", defaultErrorMessage = "rpc error", successCode = "0") public List findByIds(List ids, int type) { List list = new ArrayList(); list.add(new DemoDTO(101, "demo1", Arrays.asList(1, 2, 3, 4, 5))); list.add(new DemoDTO(102, "demo2", Arrays.asList(1, 2, 3, 4, 5))); list.add(new DemoDTO(103, "demo3", Arrays.asList(1, 2, 3, 4, 5))); return list; }

```

**声明RPC客户端**,详见com.pingan.haofang.myproject.demo.rpc.DemoRPCService

```java

@RPCClient(value = "User\\User.getMobileByUserIDs", config = "rpc.user", successCode = "0000") public Map<Integer, String> getUserInfo(List ids);

```

3. pinganfang-validator-starter, 校验器封装

该模块封装了jsr303校验器,扩展了现有的校验器,既支持hibernate validator,同时校验器可以配置fail over/fail fast等高级特性

```xml

com.pingan.haofang.framework pinganfang-validator-starter

```

若要使用,请先在Application.java上或者配置类配置@EnableHaofangValidator

在需要校验的controller方法上配置如下注解

```java

@Valid(CustomerValidator.class) public List queryList(CustomerQueryForm form) {

```

同时写好validator

```java

@Component public class CustomerValidator {

@ValidHandler
public void queryList(ValidationResult result, CustomerQueryForm form) {

    /\*\*
     \* countryId为40的时候cityId不能>20
     \*/
    if (form.getCountryId() == 40 && form.getCityId() > 20) {
        result.addError(ValidationError.of("cityId", "cityId > 20"));
    }
}

}

```

demo见com.pingan.haofang.myproject.customer.controller.CustomerController#queryList

4. pinganfang-jpa-starter, JPA封装

该模块封装了BaseDomain, BaseRepository,对Spring Data Jpa 进行了进一步扩展

```xml

com.pingan.haofang.framework pinganfang-validator-starter

```

  • BaseDomain封装了createTime,createBy,updateTime,updateBy,推荐在定义domain时继承

  • 对于Repository,可以继承BaseRepository,提供了众多新的数据库操作方法,例如返回Map, listMap, Java 8支持等

  • 提供了PageQueryDTO等基础类

5. pinganfang-history-starter, 历史操作记录封装

history封装了历史操作记录handler bean注册,相应切面等逻辑,但是按照何种格式记录日志,则由具体业务而定,在HistoryOpHandler中实现即可。

```xml

com.pingan.haofang.framework pinganfang-history-starter

```

  • 1.在项目中配置注解@EnableHaofangHistory,启用history功能

  • 2.定义HistoryOpHandler

  • 3.在需要记录日志的地方配置注解HistoryOpLog,这里注解配置参数如下,需要注意

```

value: 对应的处理器方法名称 beanName: 处理器在spring中的BeanName errMessage: 异常消息 ignoreError: 如果为true,则写入日志时失败会抛出异常,否则会忽略继续执行主体流程 force: 如果为true,则不论请求是否成功均会记录日志,否则只会在正常返回时才记录日志

```

示例见 com.pingan.haofang.myproject.customer.service.impl.CustomerLogService, com.pingan.haofang.myproject.customer.service.impl.CustomerServiceImpl

6. pinganfang-batch-starter, 批处理封装

batch封装了批处理定义基础类库,支持一次性任务,常驻任务类型两种

```xml

com.pingan.haofang.framework pinganfang-batch-starter

```

如果使用batch,请配置@EnableHaofangBatch

batch 建议两种用法

  • 一次性任务

见com.pingan.haofang.myproject.demo.batch.DemoCronTaskRunner

  • 常驻进程任务

见com.pingan.haofang.myproject.demo.batch.DemoScheduleTaskRunner

如上任务启动类均为BatchMain,如要启动某作业,启动JVM参数为-DrunnerName=${batchName}

7. pinganfang-web-common, web基础工具类库

该模块主要提供web程序要的通用基础类库Utils等

```

com.pingan.haofang.framework pinganfang-web-common

```

目前提供的基础类有

  • ContextFilter,提供请求ID生成并写入MDC,可在logback中打印

六、项目构建及部署

打包方式

``` sh build.sh ${mvn_profile}

```

打包为

``` output/myproject-web.tar.gz output/myproject-batch.tar.gz ```

包结构为

```

tar.gz +- bin | app_control.bash +- conf | logback.xml | application.properties +- myproject-web.jar

```

App启动方式

``` bash app_control.bash start|shutdown|kill|force|restart|status

start 启动app shutdown 关闭app kill 杀掉app进程 force 强制杀掉app进程 restart 重启app status 查看app状态

```

war 包使用方式

见项目myproject-web-war,目前需要将配置文件打进war包,相关配置都配置在application.properties中

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Easter79 Easter79
2年前
swap空间的增减方法
(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这